見出し画像

Retro-gaming and so on

だからC言語は腹立つんだよ、と言う話

ハマりにハマりまくった話。
んで解決してへん(苦笑)。

C言語にはマジで「何もない」。
言い換えるとこういう機能があったら随分とプログラミングはラクになるわけだ。

  • ガベージコレクション
  • 文字列
  • 配列
  • ハッシュテーブル
  • 連結リスト
「C言語には文字列がない」とか言うとビックリする人もいるだろうが、事実上無いのだ。あるのは数値だけ、ってのがC言語で、それを文字列と見做すか否かはコッチ側の都合による、と言う随分とデタラメな言語である。
配列も似たような意味で、C言語にある配列とはモダンな言語で言うトコの配列ではない。ただ、剥き出しのメモリがそこに転がってるだけだ。
それを「配列」と解釈するのはこれもコチラ側の都合による。
実際は、例えば直のメモリの一部を「長さnの配列」と見做して使っているだけ。
だから「見做し範囲」の境界線を簡単に超える事が出来、そしてセグメンテーションフォルトを喰らうのだ。
言い換えるとC言語の配列は危険な配列モドキである。安全なシロモノではない。

以前からガベージコレクション以外の問題を解決する、と言われるglibに興味があったんで、ちとやってみようと思ったのだが・・・。
何と最初の一歩から躓いている(苦笑)。
理由はハッキリしてる。

  1. Web上に、「使い方」に関する情報がまず存在しない。
  2. 公式サイトにさえチュートリアルが存在しない。
  3. バージョンが上がってドキュメントとの齟齬がワケワカメ。
もうこの三点だ。ホントに人気があるライブラリなんだか極めて疑わしい。

例えば海外サイトでこういうサイトを発見する。

「おお、しめしめ、さすがIBMだ」とか思って始めると、もう第一歩目から頓挫する。
一発目に紹介されてるコードはこれだけ、だ。

//ex‑compile.c
#include
int main(int argc, char∗∗ argv) {
 GList∗ list = NULL;
 list = g_list_append(list, "Hello world!");
 printf("The first item is '%s'\n", g_list_first(list)‑>data);
 return 0;
}

クソみたいな簡単なコードである。意味も明解だ。
Pythonで言うと

#!/usr/bin/python3.9

if __name__ == '__main__':
 lst = []
 lst.append("Hello world!")
 print("The first item is '{}'".format(lst[0]))

であり、Racketで言うと

#! /usr/bin/env racket
#lang racket

(define lst '())
(set! lst (cons "Hello world!" lst))
(for-each (lambda (x)
        (display x)
        (newline)) lst)

である。何も難しくない。
しかし、こんな簡単なコードでさえマトモに動かないのだ。
一体どーなってんの?


もうこの時点でエラー警告が出ている。glib.hが見つかりません、と。
自動補完で補完候補を探してもダメ。ネット上を徘徊すると、そもそもDebian系Linuxディストリビューションの問題らしい。クソが。
従って、件のページに書かれてるコマンドでコンパイルしようとしてもエラーの嵐である。

$ gcc ‑I/usr/include/glib‑2.0 ‑I/usr/lib/glib‑2.0/include  
   ‑lglib‑2.0 ‑o ex‑compile ex‑compile.c


ex-compile.c: In function ‘main’:
ex-compile.c:7:30: warning: format ‘%s’ expects argument of type ‘char *’, bu
t argument 2 has type ‘gpointer’ {aka ‘void *’} [-Wformat=]
  7 | printf("The first item is %s\n", g_list_first(list)->data);
   |           ~^   ~~~~~~~~~~~~~~~~~~~~~~~~
   |            |         |
   |           char *        gpointer {aka void
*}
   |            %p
/usr/bin/ld: /tmp/ccNkCuvC.o: in function `main':
ex-compile.c:(.text+0x23): undefined reference to `g_list_append'
/usr/bin/ld: ex-compile.c:(.text+0x33): undefined reference to `g_list_first'
collect2: error: ld returned 1 exit status
➜ Manage C data using the GLib collections

はぁ?」である。
そもそも、g_list_appendg_list_firstも「未定義」だと言われてる。
意味が分からん。glibをインストールしても全く使えないのだ。

件のページの下には別なコンパイル方法が紹介されている。

$ gcc 'pkg‑config ‑‑cflags ‑‑libs glib‑2.0' ‑o ex‑compile ex‑compile.c

なるほど、pkg-configを使えばいいのか、とやってみてもこれも上手く行かない。


全く同じエラーである。
どうなってんの?」と。
更に下の方を見てみると次のような記述がある。

Note that now you don’t have to specify the paths to the GLib header files anymore; pkgconfig’s --cflags option takes care of that. And the same goes for the libraries that are pointed to by the --libs option. Of course, there’s no magic involved; pkgconfig just reads the library and header file locations from a configuration file. On a Fedora Core 3 system, the pkgconfig files are located in /usr/lib/pkgconfig, and the glib-2.0.pc file looks like this:

$ cat /usr/lib/pkgconfig/glib‑2.0.pc
 prefix=/usr
 exec_prefix=/usr
 libdir=/usr/lib
 includedir=/usr/include

 glib_genmarshal=glib‑genmarshal
 gobject_query=gobject‑query
 glib_mkenums=glib‑mkenums

 Name: GLib
 Description: C Utility Library
 Version: 2.4.7
 Libs: ‑L${libdir} ‑lglib‑2.0
 Cflags: ‑I${includedir}/glib‑2.0 ‑I${libdir}/glib‑2.0/include
要は、/usr/lib/pkgconfigにglib-2.0.pcと言うpkg-configを自動でしてくれるように設定してるファイルがあるはずだ、とか書いてあるんだけど、果たしてどうなのか、って話がある。


結果まずそれがないのだ。
ウソだろ、と。
どうやらDebian系ディストロだとそうなってるみたいで、/usr/lib/pkgconfigにはglib-2.0用の設定ファイルが入ってない。

「え、みんなどうしてんだ?Debian系でもglib使ってプログラムするでしょ?」

と当然なるわな。
そこでネットを徘徊。しかし情報が見つかりづらいんだわ。
'pkg‑config ‑‑cflags ‑‑libs glib‑2.0'の代わりに$(pkg‑config ‑‑cflags ‑‑libs glib‑2.0)を使え、って情報があるんだけど敢え無く玉砕。
そのうち、また調べ回っていたら、引数順序が影響を与える、と言う情報を発見。
フツー、Cコンパイラの場合、引数の順序は影響を与えないんだけど、glib使用の場合、影響が出るらしい。何だそれ
つまり、Debian系Linuxディストロだと、要素自体は同じだが、

$ gcc  ‑o ex‑compile ex‑compile.c $(pkg‑config ‑‑cflags ‑‑libs glib‑2.0)

と書かなアカンと・・・どっかにまとめて書いとけよ(怒
考えてみれば海外だとまとめサイトって文化もねぇみたいだしな・・・。っつーか公式で面倒を見ろ(怒
これで「関数が見つかりません」ってエラーは無くなったが、エラーが全部無くなったわけじゃあない。
まだエラーが残ってる。

ex-compile.c: In function ‘main’:
ex-compile.c:6:3: warning: implicit declaration of function ‘printf’ [-Wimplic
it-function-declaration]
 6 | printf("The first item is %s\n", g_list_first(list)->data);
  | ^~~~~~
ex-compile.c:6:3: warning: incompatible implicit declaration of built-in functio
n ‘printf’
ex-compile.c:2:1: note: include ‘’ or provide a declaration of ‘prin
tf’
  1 | #include
 +++ |+#include
  2 |
ex-compile.c:6:30: warning: format ‘%s’ expects argument of type ‘char *’, bu
t argument 2 has type ‘gpointer’ {aka ‘void *’} [-Wformat=]
  6 | printf("The first item is %s\n", g_list_first(list)->data);
   |           ~^   ~~~~~~~~~~~~~~~~~~~~~~~~
   |            |         |
   |            char *       gpointer {aka void
*}
   |            %p

まぁ、そもそもしてねぇしな。
しかし名前が重複してるっつーのは何だろうな。
それはさておき、だ。
例題でああ書いてるけど、確かにprintf自体が大して機能があるわけじゃない。glib内で新しく定義されたgpointer等の型に対して、どういった反応を返すべきなのか分からん、って言われるのは納得なんだよな・・・。過去どうなってたんだ?
んじゃあ、printfじゃなくってglibで提供してるprintf代わりがあるんじゃねーの、って話になるんだけど。
リファレンスマニュアルによるとあるこたある。g_printfとか言うらしい。
じゃあ、それ使って書き換えるか、と。

#include
#include

int main(void) {
 GList* list = NULL;
 list = g_list_append(list, "Hello world!");
 g_printf("The first item is %s\n", g_list_first(list)->data);
 return 0;
}

ところがこれでもエラーが消えない。
平たく言うとどう考えても書式指定子が違う、って事なんだが、g_printfの説明には何も書いてないのだ・・・・・・。
しょうがないので、エラーメッセージに書いてる通り%s%pに書き換えればコンパイルは通る。
しかし実行すればこうなるわな。

The first item is 0x563ff8659004

そりゃそうだ。そりゃメモリ番号になるわ。

結果、例題の通りに動かん。と言うわけでお手上げである。
こんな簡単なプログラムなのに。

と言うカンジで、とにかくglibの情報がまとまったものがない。公式サイトにチュートリアルも何もない、ってのがどーしよーもない。
こんなにライブラリに関する情報がない、ってのも他の言語に比べると珍しい。結局、C言語だと車輪の再発明をするヤツが多いか、あるいは一見さんお断りだ、って事なんだよな。

実の事を言うと、C言語で目的にかなった一番良いライブラリ、と言えばCelloしかないように思う。
これならかなり関数型言語っぽく楽しいプログラミングが可能だ。


これならかなり思い通りのプログラムが可能だ。
ただし欠点もある。
このCelloと言うライブラリは、gccにheavily dependentで、他のコンパイラだと上手く行かない事もあるだろうから、である。
特にWindowsだと致命的になるかもしれない。
最近のMicrosoftのCコンパイラだと、2017年のC17と言う規格に準拠するようになったが、一方CelloはC99と言う古い規格に準拠している。
結果、整合性が取れるかどうか分からないのだ。

分かるか?Cはポータビリティを重視してる、っつーけど実際はポータビリティなんて全くない、と言う事実を(※)。移植性なんぞ大して高くないのだ。

もう一つ気づいたこともあるだろう。Cじゃ見かけないような変な書き方が跋扈している。
結果、Cを拡張する目的だと、型が全然違うものになるんで、「Cの素の型」と見分けも違わないとならないし、また頻繁な型変換が必要になるのだ。
結果、例えば、CのintとCelloの$I()は丸っきり別物で、こういうラッピングはプログラマを戸惑わせる事にはなるだろう。ハッキリ言えば書くのがメンド臭い

スティーヴ・イエギはバベル案内の中で、

C++は地上でもっともバカな言語だ。

と書いていた。
また、リーナス・トーヴァルズを始めとする著名なハッカーもC++には難色を示しているのは有名な話である。
しかし、関数型言語ファンから見るとテンプレートはそこそこ使い勝手が良く、上のglibみたいな面倒な事が起きないと思う。

#include
#include <algorithm>
#include <iostream>
#include <iterator>

int main(void) {
 std::forward_list<int> lst{2, 5, 8, 10, 19, 22};
 std::cout << "lstの長さは" <
 std::forward_list<int> result;
 lst.remove_if([](auto val) {
        return ((val % 2) != 0);
       });
 std::for_each(lst.cbegin(), lst.cend(),
       [](int x) {
        std::cout <
       });
 std::cout <
}


そこそこライブラリの使い方を調べる必要はあるが、関数型言語の知識さえあればC++全体を知らなくても記述が可能だ、と言うのは関数型言語ユーザーにとっては大きなメリットである。

いずれにせよ、glibみたいなもはや基礎的な外部ライブラリでさえ、容易に使い方が分からない、と言うのがC言語の腹立つところである。

※: 1970年登場のPascalと1972年登場のC言語は両者共「移植性」を前面に押し出した辺りがそれまでのプログラミング言語と大きく違った。
今じゃ信じられないかもしれないが、当時もアーキテクチャが全く違うミニコン/メインフレームが数多く存在し、ソフトウェアの移植はアタマの痛い問題だったのである。
ただし、PascalとC言語の「移植性」へのアプローチは大幅に違う。Pascalは元々、仮想マシン上で動くように設計されたプログラミング言語で、仮想マシンを各機種で別々に用意し、それさえあればバイトコンパイルされたプログラムはどこでも動くようになる。
一方、C言語は各機種によって大幅に違う部分(入出力等)を本体からとにかく切り離し、言語本体をコンパクトになるように設計している。
2021年現在、結果どっちのアプローチが正しかったのか、と言うと、プログラムを動かす、と言う意味では圧倒的にPascal方式が正しかった、としか言いようがない。
JavaのJVMを初めとしてPython、Ruby等は仮想マシン上で動き、マシン間移植はCに比べても問題が少ない。ついでに言うとC#でさえPascalと同様に仮想マシン上で動き、Microsoftが.NETの仮想マシンを共通言語基盤(Common Language Infrastructure)としてISO、及びECMAで認定を得た以上、LinuxやMacでも現Windowsで作られたソフトを動かせる、と言う事を意味する。
このように、アプリケーションプログラム、と言う話で限定すると(とは言いながら「殆どのケース」になる)、仮想マシン方式の方が「正しかった」のだ。

ちなみに、RPG「Wizardry」は家庭用ゲーム機用を除きPascalを用いてプログラミングされており、当時としては破格の9機種程に移植されている。
プログラマのロバート・ウッドヘッドによると、その全ては80%程ソースコードを共有しており、原則、入出力以外は一切変更が無い模様で(日本の国産PC版は「メッセージ」を日本語に変える、と言った事があったが、これも「メッセージ分離方式」でプログラムされていた為、問題はさほど大きくなかったようだ)、当時のPascalの「仮想マシン」でも移植性で言うと相当優秀だった模様である。
  • Xでシェアする
  • Facebookでシェアする
  • はてなブックマークに追加する
  • LINEでシェアする

最近の「プログラミング」カテゴリーもっと見る

最近の記事
バックナンバー
人気記事