C++で妙なリンクエラーに遭遇した話

最近、自作ライブラリに機能を追加したのだが、実装中に妙なリンクエラーに遭遇して右往左往したので、メモを残しておく。当初、へなちょこC++使いの私には原因がつかめず、とりあえず回避策でお茶を濁していた。

追加したのはstd::this_thread::sleep_for()のラッパー関数だ。見ての通り、非常に簡単な内容である。

これらの関数は、当初は引数の型をconst参照にしていたのだが、謎のリンクエラーが発生したため、ひとまず参照ではなく実体を渡すようにしたバージョンをコミットした、という経緯がある。例えば関数cun::sleep::millis()の引数の型は、前掲のソースコードではconst std::chrono::milliseconds::repになっているが、初期実装ではconst std::chrono::milliseconds::rep&だった。

// 初期実装時のプロトタイプ宣言
extern void millis(const std::chrono::milliseconds::rep& r) noexcept;
// コミットした版のプロトタイプ宣言
extern void millis(const std::chrono::milliseconds::rep r) noexcept;

この関数を含むライブラリcunは、ユーティリティ類をまとめた静的ライブラリlibcunをビルドした上で、個々のテストアプリにてビルド済みライブラリをリンクして使用する構成になっている。

問題は、テストコードをコンパイルした後、静的ライブラリをリンクする時に発生した。Ubnutu 22.04のGCC 11.4.0がこんなリンクエラーを出力したのだ。

/usr/bin/ld: test_sleep.o: in function `main':
test_sleep.cpp:(.text.startup+0x182): undefined reference to `cun::sleep::millis(long const&)'

興味深いことに、Visual Studio 2022でもリンクエラーが発生した。

test_sleep.obj : error LNK2001: 外部シンボル "void __cdecl cun::sleep::millis(__int64 const &)" (?millis@sleep@cun@@YAXAEB_J@Z) は未解決です
test_sleep.exe : fatal error LNK1120: 1 件の未解決の外部参照

最初は関数名などのスペルミスを疑ったのだが、ミスはなかった。というか、そもそもVisual Studio Code上で関数の定義元に正しくジャンプできるので、名前は正しいはずだ。

一体、何が起きているのだろうか? 疑問はオブジェクトファイル内のシンボル名を見たら半分だけ氷解した。

cun::sleepにはsecs()millis()micros()nanos()の4個の公開関数が定義されているのだが、そのうちmillis()のみ、ライブラリ側とテストコード側とでシンボル名が食い違っていた。

$ nm ../../../build/linux/libcun.a | grep -F sleep
sleep.o:
0000000000000000 T _ZN3cun5sleep4secsERKl
0000000000000000 t _ZN3cun5sleep4secsERKl.cold
00000000000001e0 T _ZN3cun5sleep5nanosERKl
000000000000002d t _ZN3cun5sleep5nanosERKl.cold
0000000000000130 T _ZN3cun5sleep6microsERKl
000000000000001e t _ZN3cun5sleep6microsERKl.cold
0000000000000080 T _ZN3cun5sleep6millisERl
000000000000000f t _ZN3cun5sleep6millisERl.cold
                 U nanosleep
$ nm test_sleep.o | grep -F sleep
                 U _ZN3cun5sleep4secsERKl
                 U _ZN3cun5sleep5nanosERKl
                 U _ZN3cun5sleep6microsERKl
                 U _ZN3cun5sleep6millisERKl
$ _

シンボル名の末尾を見てみると、テストコード側は全てERKlであることを期待しているのだが、ライブラリ側にてなぜかmillis()のみERlとなっている。これでは確かにリンクエラーとなるはずだ。

Visual Studio 2022でも同様に、シンボル名が食い違っていた。エラーメッセージから推測するに、テストコード側は?millis@sleep@cun@@YAXAEB_J@Zというシンボル名を期待していたようだが:

> dumpbin /NOLOGO /LINKERMEMBER ..\..\..\build\msvc\libcun.lib | findstr /l sleep
    C71F4 ??$sleep_for@_JU?$ratio@$00$00@std@@@this_thread@std@@YAXAEBV?$duration@_JU?$ratio@$00$00@std@@@chrono@1@@Z
    C71F4 ??$sleep_for@_JU?$ratio@$00$0DLJKMKAA@@std@@@this_thread@std@@YAXAEBV?$duration@_JU?$ratio@$00$0DLJKMKAA@@std@@@chrono@1@@Z
    C71F4 ??$sleep_for@_JU?$ratio@$00$0DOI@@std@@@this_thread@std@@YAXAEBV?$duration@_JU?$ratio@$00$0DOI@@std@@@chrono@1@@Z
    C71F4 ??$sleep_for@_JU?$ratio@$00$0PECEA@@std@@@this_thread@std@@YAXAEBV?$duration@_JU?$ratio@$00$0PECEA@@std@@@chrono@1@@Z
    C71F4 ??$sleep_until@Usteady_clock@chrono@std@@V?$duration@_JU?$ratio@$00$0DLJKMKAA@@std@@@23@@this_thread@std@@YAXAEBV?$time_point@Usteady_clock@chrono@std@@V?$duration@_JU?$ratio@$00$0DLJKMKAA@@std@@@23@@chrono@1@@Z
    C71F4 ?micros@sleep@cun@@YAXAEB_J@Z
    C71F4 ?millis@sleep@cun@@YAXAEA_J@Z
    C71F4 ?nanos@sleep@cun@@YAXAEB_J@Z
    C71F4 ?secs@sleep@cun@@YAXAEB_J@Z
        3 ??$sleep_for@_JU?$ratio@$00$00@std@@@this_thread@std@@YAXAEBV?$duration@_JU?$ratio@$00$00@std@@@chrono@1@@Z
        3 ??$sleep_for@_JU?$ratio@$00$0DLJKMKAA@@std@@@this_thread@std@@YAXAEBV?$duration@_JU?$ratio@$00$0DLJKMKAA@@std@@@chrono@1@@Z
        3 ??$sleep_for@_JU?$ratio@$00$0DOI@@std@@@this_thread@std@@YAXAEBV?$duration@_JU?$ratio@$00$0DOI@@std@@@chrono@1@@Z
        3 ??$sleep_for@_JU?$ratio@$00$0PECEA@@std@@@this_thread@std@@YAXAEBV?$duration@_JU?$ratio@$00$0PECEA@@std@@@chrono@1@@Z
        3 ??$sleep_until@Usteady_clock@chrono@std@@V?$duration@_JU?$ratio@$00$0DLJKMKAA@@std@@@23@@this_thread@std@@YAXAEBV?$time_point@Usteady_clock@chrono@std@@V?$duration@_JU?$ratio@$00$0DLJKMKAA@@std@@@23@@chrono@1@@Z
        3 ?micros@sleep@cun@@YAXAEB_J@Z
        3 ?millis@sleep@cun@@YAXAEA_J@Z
        3 ?nanos@sleep@cun@@YAXAEB_J@Z
        3 ?secs@sleep@cun@@YAXAEB_J@Z
> _

ライブラリ側のシンボル名は?millis@sleep@cun@@YAXAEA_J@Zだった。

さて、シンボル名が食い違っていることまでは分かったのだが、なぜシンボル名が食い違う状態に陥ったのか、へなちょこC++使いの私には分からなかった(C++のベテランなら、おそらくERKlERlの違いの意味とかを調べる手立てを知っていると思われるのだが)。

そこで、この問題の回避策として、コミット済みコードから分かるように、引数の型として参照を使うのを止めた。これでシンボル名の食い違いは発生しなくなった。

$ nm ../../../build/linux/libcun.a | grep -F sleep
sleep.o:
0000000000000000 T _ZN3cun5sleep4secsEl
0000000000000000 t _ZN3cun5sleep4secsEl.cold
00000000000001e0 T _ZN3cun5sleep5nanosEl
000000000000002d t _ZN3cun5sleep5nanosEl.cold
0000000000000130 T _ZN3cun5sleep6microsEl
000000000000001e t _ZN3cun5sleep6microsEl.cold
0000000000000080 T _ZN3cun5sleep6millisEl
000000000000000f t _ZN3cun5sleep6millisEl.cold
                 U nanosleep
$ nm test_sleep.o | grep -F sleep
                 U _ZN3cun5sleep4secsEl
                 U _ZN3cun5sleep5nanosEl
                 U _ZN3cun5sleep6microsEl
                 U _ZN3cun5sleep6millisEl
$ _

この現象は異なるコンパイラ(そして異なる標準ライブラリ実装)で発生した。処理系の不具合とかではなく、もっと別の要因、ハッキリ言うと自分自身のヘマに起因しているような気がしたが、明確な根拠はなかった。

あと、どの関数も似たようなコードなのに、なぜかmillis()だけシンボル名の食い違いが発生した――という点もヒントになりそうだった。他との違いは型(std::chrono::milliseconds::rep)だけだ。しかしライブラリ側もテストコード側も同じヘッダファイル(≒同じプロトタイプ宣言)を参照していて、かつコンパイル時に警告すら出ていない。それなのに生成されるシンボル名が食い違うとは……。

原因は2日後に分かった。関数millis()だけ、ライブラリ側のプロトタイプ宣言と関数定義とで引数の型が食い違っていたのだ。

// ヘッダファイルに書かれていたプロトタイプ宣言
extern void millis(const std::chrono::milliseconds::rep& r) noexcept;

// ソースファイルに書かれていた関数定義。
void millis(milliseconds::rep& r) noexcept
{
    delay<milliseconds>(r);
}

プロトタイプ宣言では、引数の型はconst参照だった。なので、ヘッダファイル中のプロトタイプ宣言を参照したテストコード側は、const参照型を前提としたシンボル名を生成した。

一方でライブラリ側は、ソースファイルに書かれている通りに普通の参照型を前提としたシンボル名を生成した。

これにより、両者の間でシンボル名の食い違いが発生した。

なるほど、確かに理屈は通る。実際に、関数定義側の型をconst参照にしてみたら、リンクエラーは発生しなくなった。

「妙なリンクエラー」だと思っていたものは、案の定、自分が埋め込んだバグだった訳だ。

原因は分かったものの、なお個人的に納得できなかったのは、ライブラリをビルドする時に仮引数の型の食い違いが検出されなかったことだ。ソースファイルsleep.cppではヘッダファイルsleep.hppをインクルードしている。宣言と定義とで仮引数の型が異なることを検出できなかった(それも異なる2つのコンパイラで!)だなんて……。

――と、まあこんなことを考えたあたりに、C++に慣れていない(そして予想以上にC言語の影響を受けている)プログラマの後ろ姿が透けてみえるだろう。

多分、C言語なら*1、宣言と定義とで仮引数の型が異なることを検出できた可能性がある。なぜならば、C言語では関数を多重定義(オーバーロード)できないからだ。関数名が同じならば、引数や戻り値の型も一致していなくてはならない。

でもC++では関数の多重定義が可能だ。宣言と定義とで仮引数の型が食い違っているのか、それとも「const参照を引数にとるバージョン」の関数宣言と「普通の参照を引数にとるバージョン」の関数定義が存在するだけなのか、コンパイラには見分けがつかない。だから警告しないというか、警告できないというか。

うーん、C++では「宣言と定義の食い違い」にどう対処すればよいのだろうか? この問題、絶対にすでにつまづいた人がいるはずなんだよなあ。先人たちの解決策を知りたい。

やっぱり、なるべくヘッダファイルのみで完結させる(ソースファイルとヘッダファイルに分けない)ようにするべきなのだろうか? でも、それはそれでビルドにかかる時間が長くなりそうだ。

*1:もちろんC言語の言語仕様に参照は無いので、これは思考実験の類だと思ってほしい。

今までどのくらいプログラミング言語を触ってきたか(3秒で挫折したものものも含む) Ver.16

2024-06-23現在のステータス。昨年(2023-06-18)から1年経て、こうなっている。

eel3.hatenablog.com

なおCSS、HTML、XMLはひとまず除外する。人工言語ではあるけれども「プログラミング言語」という括りに含められるか否かは議論が分かれる気がする。*1

まあでも、XMLについてはメタ言語だから――XSLTみたいに、XML上に構築されたものがプログラミング言語的なことはあるよね。MSBuildXMLスキーマプログラミング言語っぽい部分があったりするし。

よく使っている

AWK (Gawk)
単純なテキストレコードの処理はAWKで十分間に合う。今の時代、自作ツールをnawkやGNU awk単体で実装するのは苦行すぎて*2皆無なものの、シェルスクリプトMakefileAWKのコードを埋め込むなどして他のコマンドと組み合わせて使う機会は依然として多い。シェル上でワンライナーでテキスト処理する時にも重宝している。これはこれで十分AWKらしい使い方ではないだろうか? ところで、はやくオプション--csvが使える処理系が広まってくれないかなあ……。
C++
最近のお仕事の主力言語で、C言語のコードをC++に移行する作業も多いのだが、しかし未だに本職のC++使いのレベルに到達できていない。まだまだC++17止まりではあるが、C++11以降は非常に便利で、better Cでも使う価値があると思っている。C言語使いからすると、C++03時代よりも充実度が進んだ標準ライブラリや、ラムダ式やautoによる型推論に始まるモダンな言語機能は、便利で羨ましい限りだ*3。あと、Swift時代のクロスプラットフォームC++ライブラリ作者は、どうあがいてもARCから逃れられないので、C++11以降のスマートポインタは必須だ*4正規表現とスレッドとファイルシステムが標準ライブラリに加わったので、あとはソケット(低水準ネットワークAPI)をサポートしてくれないだろうか。低水準の処理を行いつつも便利な機能で実装時間を短縮できる点は便利で、少なくともシステムプログラム言語としての利点だと思う。だけど機能多すぎ/複雑すぎなところはなんとかならないものか。強力な反面、使い手を選ぶ言語だ。
C言語
お仕事での主力言語だった――ここ最近は使ってないなあ(C++のコードの一部にC言語寄りのコードを埋め込むことはあるけど)。シンプルかつ低水準の世界が垣間見れるところが割と好きだが、同時にどうしようもなく面倒にも感じる。最近の他の言語と比較すると、シンプルすぎて安全機構が欠けていたり、標準の便利機能が少なかったりするので、入門用の言語としては薦められない。にもかかわらず、かつてはプログラミング未経験者向けのC言語の本が盛んに出版されていた――あれ、何だったのだろうか? 謎だ。クロスプラットフォームなモジュール屋としては、2023年1月にVisual Studio 2012がEOLに到達したことで、ようやく大手を振ってC89からC99に移行できることを喜びたい。あとVisual Studio 2019 version 16.8以降にて本格的にC11/C17のサポートが始まったことも。一方で、macOSXcodeでthreads.hが見つからない問題はまだ直らないのかいな? ま、まあ、まずはC89時代からの手癖をC99向けにアップデートするところから始めたいと思う。
DOSバッチファイル
プログラミング言語に含まれるかどうか不明だが、含めてしまう。ちょっとした自動化や、複数ツールを組み合わせて使うときのラッパーとして、今でもよく使う。コマンドプロンプトはシバン(shebang)に対応していないので、スクリプト言語で書いたツールを起動するラッパーとしても多用している。意外と色々なコマンドが用意されているので、単純にそれらを叩く分には十分だが――言語機能がショボいので、バッチファイルでifやforのような制御構文系コマンドが必要になってきたら、何か決定的に間違えていないか、考え直すようにしている。
make (Makefile)
プログラミング言語に含まれるかどうか不明だが、DSL扱いで……いやGNU Makeはそこそこプログラミング言語的か。GNU Make 4.0はさらにプログラミング言語的だな、特にGNU Guileなところが。GNU MakeとNMAKEが主力。昔は稀にNetBSD Make(pmake)を使うこともあった。3者いずれも独自拡張アリアリで使っている。もう素のmakeではMakefileを書けない :) 最近はLinux向けツールのインストール・スクリプトを書く時にシェルスクリプト(特にBashアリアリとか)や高水準なスクリプト言語Pythonとか)が選ばれがちで、なかなかMakefileを見かけることが少なくて、ちょっとだけ寂しさを感じている。
Objective-C, Objective-C++
時代はSwiftだと言われて久しい――どころか場所によっては「Flutter + Dartがメインで、Swiftはサブ」みたいなこともありそうだけど、どっこいObjective-CObjective-C++は生きている。というかSwiftのコードにC++で書かれたライブラリを直接組み込むことができない以上、両者を繋げるグルー言語として生き残ることになるよね。一定以上のリアルタイム性が求められるアプリをSwiftだけで書くのは厳しくて、どうしても部分的にC言語C++を使うことになり、グルー言語としてObjective-Cが召喚されることになる。最近流行の言語と比べると良くも悪くも80年代的だが、アプリケーションプログラミング用としてはC言語よりマシだし、C++ほど複雑怪奇*5ではない。そしてC言語C++で書かれた既存のライブラリをそのまま使える。Objective-Cのハイブリッドな所は好きだが、Objective-C++はハイブリッドすぎて――C++のクラスとObjective-Cのクラスを、C++ラムダ式Objective-Cのブロック構文を同時に使うのは大変だ。便利ではあるんだけどね。
Python
最近、ようやくドキュメンテーション文字列と型ヒントを覚えた。型ヒントは便利で興味深い機能だと思う。Pythonではlazyなスタイルでのコーディングが許されず、整然とコードを記述する必要がある。その辺は、Perl 5やRubyとは随分と雰囲気が異なるように思う。少し気になるのは、インデントが必須な言語仕様であるために、シェルスクリプトに埋め込んで使うのが苦痛な点だ。Pythonだけでコードを書く分には気にならないのだけど……。
Ruby
自作ツールを実装する時、最近はPythonを使うことが多いのだが、時々Rubyを選択することもある。多言語化(文字エンコーディング)が絡むテキスト処理や、処理速度は考慮しなくてよいが桁あふれが気になる数値計算を行う場合だ。あとirb(1)も時々使っている。to_s(16)to_s(2)で基数変換して表示できるところが割と好き。
シェルスクリプト (/bin/sh)
プログラミング言語に含まれるかどうか不明だが……いや、私的にはシェルスクリプトは立派なプログラミング言語だ。基本的な用途は、バッチファイルと同じくちょっとした自動化や複数コマンドを組み合わせて使うときのラッパーだが、実現できる内容は遥かに多い。言語本体(?)がバッチファイルよりも高機能だし、Unixユーザランドはコマンドが充実している。その意味では、WindowsではMSYSよりもCygwinで――いやむしろWSL(Windows Subsystem for Linux)で環境構築すべきだろう。Cygwinでは、主要な処理をシェルスクリプトで記述しておき、bashからはシェルスクリプトを利用し、コマンドプロンプトではラッパーのバッチファイル経由でシェルスクリプトを叩く使い方をしている。ただWindows上では処理速度が妙に遅くなる点が不満だ。まあしかし、Unixのシェルは言語設計もシステム開発技法も未成熟だった大昔に「プアな環境でも問題なく動作する、プログラマブルな対話型コマンドインタプリタ」として開発された代物なので、言語設計の研究が進んでから作られたプログラミング言語と比較してはならない。なお自分自身が落とし穴に嵌らないようにShellCheckを活用すべし。

あまり使っていない

Scheme
GaucheWindowsネイティブ環境用バイナリは実験版だが、私が触る分には何の支障もない*6ことに気づいて久しい今日この頃。『Scheme手習い』と『Scheme修行』を購入したので、とりあえずCommon LispではなくGaucheScheme)の勉強をする方向に転換しようか検討*7しているうちに何年たったのやら。Gaucheはフィルタ・ライクな小ツールの実装用としても良い感じだ。しかし最も多い利用方法はREPLを電卓代わりにすることだ*8。うーん、作業環境がmacOSLinuxに移ったなら、大手を振ってGaucheでフィルタを書くのだが。
sed
プログラミング言語に含まれるかどうか不明だが、DSL扱いで*9。テキスト処理用。シェルスクリプトMakefileにて他のコマンドと組み合わせて使う。というか正規表現でのテキスト置換以外の機能を使った記憶が……あったな、dとiとpと=とブレースによるグループ化ぐらいだが。私の技術レベルではsedFizzBuzzを書けないので、sedで難しい処理を記述しないようにしている。
Windows PowerShell
時代はPowerShell Coreらしいが、現行のWindows 10でデフォルトで利用できるv5.1に留まったままである。スクリプト言語としてのPowerShellは、オブジェクト指向.NET Frameworkを叩けてダイナミックスコープでスクリプトブロック(という名の無名関数)と、無茶でピーキーで完全にプログラマ向けな代物だ。Microsoftもよくもこんなエライ代物を出したものだ。残念なことに、コマンドプロンプトの代替という観点では、外部ツールとの親和性が微妙にイマイチだ(特に文字コードとか)。でもPowerShell内で閉じている分には問題ないので、私の手元では「Windows専用のGUI付き小ツールを作るためのスクリプト言語」か「Excel COMとか叩く用のスクリプト言語」か「Windows Serverの管理スクリプトを書くためのスクリプト言語」扱いしている。ところで、いい加減『Windows PowerShell イン アクション』並みの言語解説書の最新バージョン対応版を出版してくれないだろうか。

最近使ってないが、縁は切れてない

bash
最近はデフォルトシェルがbashな環境も多いので、自分用のツールぐらいは素の/bin/shではなくbashで書いても大丈夫な気がしてきた。shよりbashの方が遥かに便利だからなあ――PerlRuby等には負けるけど。bashスクリプトを書くときの唯一の欠点は、メジャーバージョンごとの差異や各ディストリでのビルドオプションの違いにより、同じbashという名前でも実は千差万別なところだと思う。PerlRubyのバージョンは気にするけど、これがシェルになると意外とバージョンに無頓着になってしまう。なんでだろう?
C#
かつて、勉強を兼ねてC# 2.0を少し触ろうとするも未完に終わり、数年後にあらためてVisual Studio 2013をインストールして少しだけ触った*10けどほんの少しだけで終わった過去をもつ私。変数の型推論ラムダ式LINQ・デフォルト引数は便利だなあと思っていたら、いつの間にかC# 8.0になってKotlinやSwiftに見られる流行を取り入れてますな。おっちゃん、付いてくのが大変だよ。.NET Frameworkの機能数は反則ものだが、所々に微妙に抽象化が行き過ぎたAPIが見られるのは気のせいだろうか? それにしても、クラスが必須ではないC言語C++に慣れてしまった弊害か、アプリケーション・メインエントリすらclass内に定義しなくてはならないC#には、なかなか慣れない。
Free Pascal
お試しで触っているのだが、微妙にDelphi/Free Pascal初心者(ただし他言語の経験者)向けの良い資料が少なくて難儀している。玉石混交なのだ。いっそのこと『OBJECT PASCAL HANDBOOK: マルチデバイス開発ツ-ルDelphiのためのプログラミング言語完全ガイド』を買ってしまおうかしら……と思っていたら絶版っぽい。
Go
寡作ながらもいくつか小ツールを書いてみたが、標準ライブラリが充実しているコンパイラ型言語っていいっすね。C言語に比べればC++の標準ライブラリも充実しているが、どちらかといえばプリミティブな機能が中心だ。PythonRubyばりの標準ライブラリを抱えているGoには及ばない。その辺は、やはりCプログラマ(特にCでフィルタやデーモンの類を書く層)には受けそうな言語だと思う。並列処理周り(goroutines)とかARM対応とかが気になる。ソフトリアルタイム限定だが「組み込みLinux + Goで書いたデーモン」とかどうだろう? ただメモリを食うらしいという噂がどうなったか気になる――64bit環境では解消されるという話だったようだが、32bit環境でも解消されるようになったのだろうか? 組み込みでは現時点では逆立ちしたって64bit CPUはありえないからなあ、スマホタブレット以外では。
Java
生まれて初めて触れたプログラミング言語その2。実のところ、職業プログラマとして本格的に使用することは一生ないと思っていた。Androidアプリ開発も、Kotlin採用後に本腰入れて関わるようになったので、Kotlinメインだ。だが、なぜかぬるい感じに時々Javaのコードを触っている。先にコレクションの操作方法が充実した他の言語を学んでからJavaを本格的に触るようになったので、Java 8以降のStream APIが使えないと身体が拒否反応を示す。少なくとも、構文の見た目こそ保守的なオブジェクト指向プログラミング・スタイルで書かれたC++に似ているけど、中身はC++とは似ても似つかない代物だということは体感している。
JavaScript(クライアントサイド)
ものすごく久しぶりにクライアントサイドJavaScriptのコード触った――いまどき珍しい、DOM直叩きスタイルだけど。収穫は、最近のECMAScriptのスタイルに触れたぐらいだろうか? フレームワークもTypeScriptも触っていないので、クライアントサイド開発のスキルは依然として賞味期限切れのままだ。
JavaScript(サーバサイド?)
初めてお仕事でNode.js向けのJavaScriptのコードを触った。Web開発の外の人からみた印象としては、ブラウザ以外のJavaScript処理系はNode.jsに収斂しちゃった感があるなあ――Denoもあるけど、エコシステム的に今後どうなるんだろう?
Kotlin
本格的にAndroidアプリ開発に関わるようになったのがGoogle I/O 2017直後の過渡期なので、JavaよりもKotlinでの経験値の方が多い。モダンな「強い静的型付け」の、割とええ感じの言語やね。ただ、使い始めが「Swift 3をつまみ食いして半年以上経ってからKotlinをつまみ食いした」みたいな経緯だったこともあり、未だに両者の概念・機能が頭の中でごった煮になっている。それと、NDK絡みの作業が多いので、C++17・Java 7/8・Kotlinを行ったり来たり。泣けるぜ。Swiftもそうだが、最近のメジャーな「強い静的型付け」の言語は「開発環境込み」で高い生産性とコードの安全性を両立させる方向に進んでいる気がする。
Lua
Wiresharkのパケット解析スクリプトを書いたことも、C言語で書かれたUnixデーモンの設定ファイル用に処理系を組み込んだこともあった*11。あれから数年経ったが、今はどんな感じなんだろう?
Perl 5
時々、やむをえない事情で触ることがある。だが基本的によく分からない。何というか、あの記号の羅列っぽさに中々慣れないというか、自分は余りに自由度が高すぎる言語は苦手だと気づいたというか。(言語仕様に慣れているなら)半ば使い捨てなテキストフィルタとかをさっと書くに分には悪くない言語だと思うのだけど。
SQL
生まれて初めて触れたプログラミング言語その3ぐらいに位置する。組み込みの人なのでSQLとは無縁だと思っていたが、まさかTransact-SQLを少しだけ触ることになるとは。最近はAndroidアプリ絡みでSQLiteに触れることもあるが、AndroidXのRoom経由だったり、ContentResolverのqueryだったりと、フルセットのSQL文ではなく局所局所でDSL的に使う感じである。
Swift
コンパイラによる強力な型推論と型安全性のチェック」がお仕事用のメジャーな言語にまで降りてきたという点で、Swiftは静的型付け言語界のJavaScript*12だと思っている。でもユーザ数的には、Kotlinが「静的型付け言語界のJavaScript」ポジションなのかもしれない。割と好感が持てる言語だが、知識が中途半端にKotlinとごった煮になっているので、ついうっかりif式を書こうとしてコンパイルエラーになったり、「varval」と「varlet」の振る舞いの差異につまづいたりしてしまう*13
Tcl/Tk
Tclは書き方を忘れた頃にテキスト処理ツールを書いている気がする。Tclは結構独特な言語だ。構文がシェルスクリプトばりに全てコマンドだったり、値が全て文字列だったり、実はリスト構造だったり、意外とTCPソケット通信が得意だったり……。それでも慣れれば結構使いやすい。意外とプロトタイピングに向いている気がする。8.6以降ではオブジェクト指向プログラミングもOKだが、それよりも例外処理用のtry末尾呼び出しの最適化用のtailcallの方が興味深い。しかし、これからメジャーになる可能性は低そうだ。Tkは……小規模なGUIツールをさくっと構築できるところは便利だが、Webアプリ全盛の時代にどれだけ訴求力があるのやら。
Visual Basic .NET
Visual Basic .NET 2003で書かれたコードを時々メンテ中。流石に開発環境はVisual Studio 2013移行したけど。
XSLT
よく考えてみたら生まれて初めて触れたプログラミング言語その4ぐらいに位置する言語だった。縁が切れたと思いきや、仕事でXHTMLから特定要素を抜き出す作業に使うことがあったり……。XMLからテキストレコードに変換してしまえば、後はUnix流テキストフィルタの世界が待っている。餅は餅屋というもので、定型的なXMLの変換はXSLTで記述するべきか。唯一気に入らないのは、xsl:sortでアルファベットの大文字と小文字を区別してソートすることができないこと。ぐぬぬぬ。

これから(また)使うかもしれない

Alloy
形式手法の中では比較的カジュアルに使えそうなので期待中。入門書も処理系も入手した。私の場合、先に何か論理型の言語をかじった方がよいのかも。
bison (yacc)
プログラミング言語に含まれるかどうか不明だが、DSL扱いで。やっぱり構文解析系統のコードを自作するのは割に合わない――だなんてうそぶきつつ、LALR法とか全く知らないままに、既存のyaccのコードを切り貼りして遊んでみた。簡易電卓レベルだが便利さを体感しつつ、さっそくtypo 1文字で痛い目(shift/reduce)に遭った。とりあえず、flexと組み合わせた上でのエラー処理(エラーメッセージの改善)が課題だ。
Common Lisp
2009年に勉強しようと思い立ったものの、未だに進んでいない。階乗とかハノイの塔とかiotaぐらいは書いたが、目標は「ちょっとしたツールを自作する」だ。まだ道は遠い。最近は時々CLISPを簡易電卓代わりにしている。
Coq
ソフトウェアの基礎が気になるので、処理系だけ入手。
F#
OCamlは「Windows上で日本語を扱う」という視点では処理系がちょっと微妙なので、いっそのことF#に乗り換えようかと……。『実践F#』が積読状態になっている。
flex (lex)
プログラミング言語に含まれるかどうか不明だが、DSL扱いで。字句解析用のツールという印象が強かったのだが、よく考えてみたら、flexは「sed(1)のよくある使い方」と同様に「正規表現でパターンマッチング --> 何らかのアクション」という内容を記述するためのツールだった。ただ単に、「何らかのアクション」をC言語で書けることと、flex自体ではパターンマッチングをせずに「パターンマッチングするC言語のコード」を生成することが少々風変わりなだけ。grep(1)やsed(1)その他で小ツールを実装して運用しつつ、性能が求められたらflexで専用ツール化する――とか考えたけど、普通にgrep(1)やsed(1)を使う方が高速だった。
Forth
pForthをMinGWでビルドしたので処理系は手元にある。スタック指向の言語はいつか勉強したい。
Io
プロトタイプベースである点を除けば、何となくSmalltalk的であるような――公式ドキュメントらしきIo Programming Guideでも影響を受けた言語として真っ先にSmalltalkが挙げられているから、あながち思い違いでもないだろう。今更ながら『7つの言語 7つの世界』のIoの章を読み終えたので、ちょっとしたコード片を書いているが……Windows版のバイナリが古いためか、リファレンス通りなのに動作しないコードに直面している。
LOGO
そういえばLOGOを触ったことがない。とりあえずUCBLogo(Berkeley Logo)だろうか? Windows上でUCBLogoばりにGUI無しで動作する処理系はないだろうか?
Object REXX
思うところがあって処理系とIBM謹製のドキュメントを入手したものの、そこから先の進展は無いまま。ReginaでClassic REXXっぽい感じで触っているからなあ。
OCaml
Common Lispを勉強するはずが、いつの間にか触っていた言語。一応、階乗ぐらいは書いた。時間が取れたらもうちょっとしっかりと勉強したいが、面倒なのでF#に移行しようか検討中。
Oz
Scheme手習い』の次はCTMCP片手にOzで勉強かなあ。道は遠いな……。
PostScript
これかForthか、どちらに手を出すべきか? 悩ましい。
Processing
入門書も処理系も入手して、あとは弄る時間をつくるだけ。
Prolog
『7つの言語、7つの世界』の地図の色分けプログラムには衝撃を受けた。何というか「正しい問い」を見つけられるか否かが肝なのか。この辺は、根底の部分でAlloyに通じる何かがあるように思う。ひとまず、Prologで論理プログラミングと宣言的なスタイルに慣れておけば、形式手法にて「論理で宣言的に」記述するときに戸惑いが減るのではないかと期待している。
Rust
仕事柄「C/C++の次のシステムプログラミング言語」はそれなりに興味の対象で、今のお仕事的にはGo言語やD言語よりもRustがド直球の本命に近いはず……まあ、本筋とは外れたところでGo言語を使うことになるとか、そういうパターンはありそうだけど。ちなみに、これら3言語と同列にSwiftが挙げられることもあるようだが、個人的見解としては、システムプログラミング言語としてのSwiftには全く期待していない。あれは、Appleというしがらみからは逃れられないでしょうな。
VBA (Visual Basic for Applications)
今までVBAから逃げ回っていて、今のところ追いつかれてはいないのだが、どこかで使う(というか解読する)ことになりそうな予感。たぶん、Excel VBA 8割にAccess VBA 2割ぐらいかなあ。

今は全く使っていない

Active Basic
VBScripを触りだした影響で、時々思い出しては弄くっていた。ほんの少しだけ触って放置して、すっかり忘れてからまた触る――これを繰り返していた気がする。なので毎度初めて触るのと同じ状態だった。String型をバシバシ使用 :)
bc
その昔、Windows標準の電卓アプリの代わりに使おうとして色々あって挫折した。今はirbclisp/goshで計算しているからなあ。
CASL II
生まれて初めて触れたプログラミング言語その1。何だかんだで、後でCプログラマになってからも低水準での思考ツールとして微妙に役に立っている。まあ考えるための言語であって実用言語ではない。仮に実用的な処理系*14があったとしても余りに命令がシンプル過ぎて悶絶するなあ、なんてFizzBuzzしてみて思った。
Clojure, Scala
JDKがなくてもJava APIを叩くスクリプトを書けるので非常に便利。Scala型推論とか、便利っすね。言語仕様はJavaよりも好みだ。とはいえ、IoT時代にJava VMベースでどこまでメインストリームに居残ることができるのか? ちょっと興味深い。サーバサイドに活路を見出すのだろうか?
COBOL
FizzBuzzするためだけにOpenCOBOL 1.0をWindows上に用意して触ってみた。なんというか、COBOLの名前と生まれた時代が示すように基幹業務(というかお金や帳簿が絡んでくるところ)向けの言語だよなあ、といった感じ。COBOL 2002のフリーフォーマットを採用するだけでも使い勝手が変わる気がしたが、世の中にはまだ広まらないのだろうか。
CoffeeScript
仕事で使う予定はない。RubyPythonその他の影響を受けているだけあり、その手のスクリプト言語っぽい感じでコードを書けるので、慣れれば素のJavaScriptで直接コーディングするよりは楽だ。しかし標準ライブラリ回りや処理系絡みの機能やサードパーティのライブラリなど、結局はJavaScriptを知らないとCoffeeScriptでコードを書けないと思う。それに生成されたJavaScriptのコードを見て「うわぁ、これあまり効率的でないなあ」と感じる時もあって、高速化が必要な部分では生成されるコードを気にしながら記述したりCoffeeScriptを諦めてJavaScriptで書くことになるので、やはりJavaScriptを知らないとマズイ。とはいえ便利なのは確かだ。CoffeeScriptのコードは即Node.jsで実行できるので、その辺りから「CoffeeScriptでテキストフィルタ」的な文化が生まれると面白いかも。気になるのはECMAScript 6の存在で、今までCoffeeScript独自の機能だった部分の多くがES6で取り込まれるので、今後ES6対応が進むにつれてCoffeeScriptの立場がどうなっていくのか、少々興味深い。
D言語 2.x
仕事柄「C/C++の次のシステムプログラミング言語」はそれなりに興味の対象で、Go言語ほどではないが、D言語も気になる存在だ。D言語シンタックスがC・C++に近いだけでなく、コーディングしている時のアプローチ・判断についても、CやC++での流儀がそこそこ通用しやすい気がする。少なくとも、Go言語でコーディングするよりは、文化的背景の違いによるモヤモヤは感じにくい。あと、標準ライブラリを使ってテキストフィルタを書いたところ、エラー処理を1~2ヶ所のtry - catchにスッキリまとめることができて、ちょっと驚いた。throwされる例外のメッセージ文字列が、ちょうどよい塩梅の内容だったため、メッセージを変更する(いったんcatchして、再throwする)必要がなかった。ちょっと残念なのは、マルチバイト対応だが……。
Emacs Lisp
.emacsにコピペ」限定で。Common LispSchemeを触ったためか、何となく内容を追えるようになってきた気がしていたが、勘違いだった。
Fortran
Fortran 90やFortran 95あたりは結構近代的な言語だと思う。用途次第ではC言語よりもFortranの方が遥かにマシな選択だろう。配列がらみの処理はFortranの方が得意だし、言語機能としてのモジュール化の方法はC言語には存在しない。可変長な文字列の扱いに微妙な制限がある点はマイナスな気もするが、まあ基本的に数値計算プログラム用の言語だからなあ。
GDB (GNU Debugger)
……いやGDBはデバッガとして使っているが、GDBスクリプトを書く機会は(FizzBuzz以外に)ない。勉強不足なだけかもしれない。
Groovy
JDKがなくてもJava APIを叩くスクリプトを書けるので非常に便利。動的型付け言語っぽくいくもよし、@CompileStaticや@TypeCheckedで型推論するもよし。言語仕様はJavaよりも好みだ。コンソールアプリを書く人としては、オプション引数解析用の機能を標準で持っている点で、GroovyはClojureScalaよりもポイントが高い*15。個人的には、IoT時代に「Java VMベース」の言語としてどこに活路を見出すのが、興味深く見守りたいところ。やはりサーバサイドだろうか?
HSP (Hot Soup Processor)
FizzBuzzで楽しんでみたが、何というか他言語経験者には受けが悪そうな命令体系だと思う。もっとも初心者がプログラミングという行為に深入りせずにWindows用のGUIな何かを作る分には、あの命令体系でも十分な気がしないでもない。ところで元々は「HSPで職業プログラマ的な良いコードを書くと、どんな感じになるか?」というネタを思いついて処理系を用意したのだけど、そちらは全く進展がないまま。
JScript on WSH
他人が使うテキスト処理ツールの実装に使って以来、時々触ってきた。Windows用の配布可能な小ツールを実装する時の定番言語だった。でもそろそろ潮時だろう。HTAと組み合わせてクライアントサイドJavaScriptなノリで簡易なGUIツールを実装できる点も、PowerShell + WPF + XAMLで代替できそうだ。他のメリットは「JavaScriptECMAScript)でフィルタを書ける」だったが、WSHのなかなか目的にたどり着けないオブジェクト階層にイライラするよりも、Node.jsやPhantomJSを使ったほうが精神衛生的にマシだ。
m4
その昔テキスト処理用に触ろうとして、Windows用のどの処理系も日本語の置換に何かしらの問題を抱えていたので泣く泣く諦めた。思うところがあって改めて少し触ってみたが――なるほど、確かに中毒性のある言語*16だ。
QML
宣伝文句のとおり、QMLはGUIの記述に非常に向いている。それも、単に標準のUI部品(エレメント)を使うだけでなく、少し改造して使うとか、オリジナルのUI部品を作って使うとか、それらを別のアプリケーションに使いまわすとか、そういう時に威力を発揮する。あと、プロパティバインディングやレイアウトのアンカー指定など、画面サイズの変更に追随するUIを作りやすい機能も揃っている。JavaScriptでちょっとした処理も記述できる――とはいえ、やりすぎるとパフォーマンスの罠が……。少なくとも、JavaScriptでゴリゴリコードを書くのはQML的ではない。QMLは宣言的に、シンプルに書くものだ。力技でロジックでゴリ押しすると、色々と罠に嵌る言語だ。
REXX
Open Object REXXの処理系を入手したのに、何故かReginaを入れてClassic REXXっぽい方向に走っていた。何というか、COMコンポーネント.NET Frameworkと無関係でいられるのなら、バッチファイルの代替としてはREXXあたりがほどよい塩梅だと感じる。しかし最近流行の言語とは随分と勝手が違うし、日本語の情報も少ない。メインフレーム以外の世界で流行る可能性は少ないだろう。
Smalltalk (Squeak, Pharo)
Smalltalkは有名な古典的プログラミング言語だというのに、触ったことがない。ということでSqueakとPharoの処理系のみ準備完了。うーん、「環境」付きなのが気になる――言語を弄くる基準が「コンソール上でテキストフィルタ」という変な人種な私だからなあ。
Smalltalk (GNU Smalltalk)
個人の思想信条による理由よりSqueakとPharoにわだかまりを感じてしまう変人なので、邪道だと思いつつもコンソールでテキスト処理もOKなGNU Smalltalkも用意してみた。これで言語としてのSmalltalkの勉強に集中できる……か?
T4 Text Template
「へえ、こんなものがVisual Studioに入っていたのか。機能多すぎで色々と便利なツールを見逃しているんだな、やっぱり」と思いつつ触ってみた。テンプレート変換の用途ではピカ一だと思う。ただ処理系を手に入れる方法が「Visual Studioをインストールする」or「MonoDevelopをインストールする」なので、何となく「単体で手軽に使えるツール」ではないというイメージが……。まあC#VBで処理を記述するので、それらの処理系が必要だという面での制約なのだろう。
VBScript on WSH
JScriptほどではないが「Windows上で他人も使えるツールを書くためのLL」扱いしていた言語。Windows Server管理の関係で触っていた。というかWebで入手可能なWSHのサンプルの大半がVBScriptで書かれていたり、ADSI関連のコレクションをJScriptで舐めれなかったりして、結局は必要に駆られて使用することに。明快に記述できる文法は評価に値するが、スクリプト言語としては少々冗長だ。配列は自動拡張しないし、組み込み関数はプリミティブ気味だし、冗長気味な文法との合わせ技でコードがさらに冗長になっていく……。文法や言語仕様の詳細なドキュメントが見つからないのだが、どこにあるのだろうか?*17
Vim script
少し触ってみた分には、exコマンドの拡張(=コマンドの羅列)とは気づかない程度にはプログラミング言語らしいと思う。とはいえ妙なところで嵌ったり微妙に一貫性のない部分があったりするので、その辺りで好き嫌いが別れる気がする。
秀丸マクロ
7年ほど秀丸エディタを使っていたが、マクロを書く機会はなかった。一念発起してFizzBuzzしてみて感じたのは、最近の便利な言語に慣れた身としては色々とモヤモヤ感がある言語仕様だということ(歴史的経緯的に仕方ないのだが)。とはいえちょっとした拡張ツール的なものを手軽に作れそうではあった。

*1:HTML5 + CSS」の組み合わせなら、チューリング完全の疑惑があったり、JavaScript使わずにCSSでWebチャットを作った猛者がいたりと、色々と怪しいのだけど。

*2:「独立性の高い単体のツールを実装する」という視点では、現代ではAWKよりも便利な言語が山ほどある。

*3:しかし標準ライブラリの充実度をJavaC#.NET Framework含む)と比較したり、型推論まわりをKotlinやSwiftと比較してはいけない。以前よりも随分と便利になったのだけど、だけど、隣の芝生を見てしまうと、うーん……。

*4:SwiftではC++の機能を直接呼び出すことができないので、Objective-Cでラッピングして利用することになる(インタフェースはObjective-Cで、内部実装はObjective-C++)。この時、Objective-Cクラスのインスタンス変数として「C++クラスのインスタンスを保持するスマートポインタ」を持つ構成にしておくと、Objective-Cクラスのインスタンスがdeallocされる時に、スマートポインタ経由でC++クラスのインスタンスもdeleteされる。

*5:少なくともC++の言語/ライブラリ仕様は私の手には余る。自分が把握している範囲の機能でコードを書くのは好きなのだけど。

*6:支障がある部分を触るほど深入りするには、あと20年ぐらい掛かるのではないか?

*7:Schemeの勉強というよりも、再帰の勉強なのか?

*8:現状はirbclispかgoshの3択だ。

*9:これでもsedチューリング完全な言語だと証明されているらしい。

*10:言語仕様的にはC# 5.0の環境だが、ライブラビまわりはC# 4.0相当だったはず。

*11:Windowsのことを考えなければ、自前でライブラリをビルドしてアプリに組み込むのは結構簡単だった。

*12:私の認識では、JavaScriptは、第一級関数やクロージャがお仕事用のメジャーな言語に組み込まれて、少なくない人が使う契機となった言語だ。

*13:Kotlinのvalは「再代入不可の変数」だ(定数はconstで定義する)。Kotlinのプリミティブ型以外のデータ型はclass(つまり参照型)なので、valで定義した変数を破壊的操作する行為は割と普通だと思う。一方でSwiftのletは定数であるし、値型が中心の言語仕様である影響かstructやenum(つまり値型として振る舞うデータ型)が多用されるので、letで定義した変数を破壊的操作できるケースとできないケースが生じる。

*14:――といってもシミュレータだけど。

*15:ClojureScalaに関しては、同様の機能を私が見逃している可能性も高いのだが。

*16:m4はマクロプロセッサなのでプログラミング言語ではないはずだけど……。

*17:MSDNの資料では物足りない。もうちょっと掘り下げた内容のものが欲しい。

書籍購入:『実務で使えるメール技術の教科書』

スパムメール対策絡みの話についていけないので、まずはメール(電子メール)の全体像について知識を仕入れることを画策してみた。

こういう本が出版されるようになったという点に、時代の流れを感じる。

2024年の今となっては、普通のシステム管理者がオンプレミス環境で新規にメールサーバを構築して運用することは割とレアだと思うのだが、例えばクラウド型のサービスを利用するにしても、設定の過程で「全体像を理解していないと、よく理解できない」項目が出てきたりする訳だ。

便利なメール・サービスの類は、あくまでも偶有的な部分を覆い隠して手間がかからないようにしているだけだ。メールの本質的な「難しい部分」は依然として残っていて、管理者はその辺を理解した上でサービスを利用する必要がある。

時代の流れと共にメールと歩んできた管理者なら、本質的な部分について自身の経験として刻まれている。一方で最近システム管理に関わるようになった人は、そういった経験をすっ飛ばして成熟したシステムに触れる訳だ。

ここで、後発者が知識や経験のギャップを埋めるためにも、メールの技術的な全体像について体系的にまとめられた本は、非常に有用だと言える。

――まあ、自分自身のことなんだけど。メール自体は20年以上使っているし、個々の技術要素については一部だけつまみ食いしたけど、メールのシステムを構築・運用した経験がないので、例えばSPFDKIM・DMARCが具体的にどんなものなのか分からない。分からないままでは、設定した内容が適切なのか判断できないよね。

C言語が難しいのか、それともCプログラミングが難しいのか?

問い:C言語は難しいか?
答え:C言語の言語仕様自体は難しくないが、C言語でのプログラミングは難しい(私見)。

C言語が簡単なのか難しいのか問われた場合、C言語の「なに」の難易度について問われているのか考えなくてはならない。

C言語そのものは難しくない

C言語の言語本体や標準ライブラリ部分──すなわちC言語という「抽象化された計算モデル」とでもいうべき部分については、さほど難しくない。

C言語の仕様はシンプルだ。C言語オブジェクト指向プログラミングのための機能を追加することから始まった言語であるC++Objective-Cと比較すれば、そのシンプルさは一目瞭然だろう。

C++の仕様は大きく複雑だ。C++の全ての側面に精通しているプログラマは、決して多くはない*1C言語には、C++で追加された様々な機能が存在しない。つまり、それだけシンプルだ。

Objective-Cは、C言語Smalltalk流のオブジェクト指向プログラミング言語を追加したものだ。実質的に2つの言語を抱えている。C言語は、Objective-Cの半分だ。それだけ言語仕様が小さく済んでいる。

C言語の仕様はシンプルで、さほど大きくない。言語仕様的に微妙なところがあるので、やさしいとは言えない。しかし難しい訳でもない。C言語自体は、微妙な表現だが、難しくはない。

C言語でのプログラミングは難しい:仮説「低水準だから?」

しかしながら、いざC言語による現実のプログラミングを学ぼうとすると、これがなかなか難しい。C言語で実用的な何かを書いてみると、その難しさは疑惑から確信に変化する。

C言語自体は難しくないのに、C言語で何かを成すのは難しい。なぜだろうか?

C言語は高水準言語の中では低水準の部類に属する。この辺が原因だろうか? ――影響はあるが、主要な原因ではないだろう。

C言語の言語本体や標準ライブラリで提供される機能は少ないが、しかし世の中にはC言語で利用できるライブラリが山ほどある。C言語ガベージコレクションはないが、例えばBoehm GCがある。C言語にはオブジェクト指向プログラミングのための機能はないが、例えばGLibのGObjectがある。C言語には正規表現の機能はないが、例えばPOSIXregexがある。

最近のプログラミング言語のようなパッケージ管理システム*2は存在しないが、しかし例えばUnixユーザランド環境はCプログラミングにおいてある種のプラットホームとして機能する。ディストリビューションのパッケージ管理システムを使用して手軽にライブラリを導入できるだけでなく、TCP/IP通信やカーネルの管理機能をC言語で利用することもできる。

Common LispSchemeのマクロにははるかに及ばないが、しかしC言語の枠内にも合法的に機能を拡張する仕組みが存在する*3。それらの機能を使用して、C言語にさらなる抽象化層を提供するライブラリその他が揃っている。そういったライブラリ等を用いることで、Cプログラミングにおける抽象化の水準を高くすることが可能だ。

抽象化層を提供するライブラリを用いることで、C言語でのプログラミングはある程度は容易になる。抽象化の度合いが変化することで、プログラミングの難易度も変化する。

ただし、C言語の低水準な部分がCプログラミングの難しさの主な原因である、とまでは言えないだろう。もし低水準であることが原因ならば、便利なライブラリを積極的に使用して抽象度を上げることで、Cプログラミングの難易度は低下するはずだ。しかし実際には、抽象度を上げることで問題が解決される範囲は限られている。

これは、どういうことだろうか?

C言語」という抽象化層にあく穴

C言語で何かを成すのは難しい一番の理由は、C言語が提供する抽象化層が低水準だからではなく、その抽象化層が脆く容易に穴が開いてしまう代物だという点にある。

例えばC言語学習において悪名高いポインタだが、C言語C++以外にも存在する。有名な言語としてはC#・Go・Swiftがある。

このポインタという機能、C#・Go・Swiftにおいては、C言語C++のそれよりも安全になっている。しかし実際に使用すると、それぞれの言語の抽象化層に穴があいてしまうことがある。

C#での例を示そう。C#にはガベージコレクションがあり、普段はオブジェクトが使用するメモリの確保や解放のことなど全く気にする必要がないし、オブジェクトのアドレスとは無縁で済む。しかしポインタを使用すると、抽象化層に隠されたメモリ管理の部分とつきあわなくてはならなくなる。C#の変数が固定変数と移動可能変数に分類可能であることや、その2種類の変数の振る舞いの差異は、普段は抽象化層に隠されている。しかしポインタを使用すると抽象化層に穴があき、白日の下に晒されてしまう。だからfixedステートメントが存在するのだ。

C言語C++においても、ポインタは抽象化層に穴をあける代物だ。しかも悪いことに、C#・Go・Swiftのポインタよりも強力で野蛮だ。抽象化層にあく穴は、より大きく深いものとなる。

C言語C++にはポインタ以外にも、例えば配列の二級市民ぶりだとか、文字列についてのあれこれだとか、動的メモリ管理まわりだとか、抽象化層に穴があく契機となるものが山のように存在する。C++においては、参照や標準ライブラリの機能を積極的に使用することで、これらの側面を回避できる余地がある。しかしC言語でのプログラミングにおいては、いずれの機能も使わずに済まそうとしたら何もできなくなる。つまり、危険を承知で使わざるをえない。

ここにC言語でプログラミングする時の難しさがある。抽象化層の特定の層にて安穏としていることができず、頻繁に下位層の事情を気にしなくてはならない。プログラミングという高度な知的活動においては、非本質的(偶有的)な部分の障害を減らすことが鉄則だが、C言語ではそれがままならない。

C言語という抽象化層が、たとえ低水準であっても、丈夫で穴が開きにくい代物だったならば、事情は違っていただろう。しかしその代償として、カーネルデバイスドライバファームウェアなどの低水準の領域でC言語が使われることもなかったに違いない。抽象化層に自由に穴を開けて下位層にアクセスできるからこそ、低水準の領域でのプログラミングが可能なのだから。

学習コスト

Cプログラミングにて絶えず下位層の事情を気にしなくてはならないという点は、Cプログラミングの学習コスト増加につながっている。すなわち、C言語の言語機能やライブラリ以外にも山ほど学ぶことがあり、それらを学んでおかないと思わぬところで自分の足を撃ち抜いてしまう、ということだ。

「良いプログラミング・スタイル」という観点では、どのような言語を用いても「言語機能やライブラリ以外にも学ぶべきこと」が出てくるものだが、C言語が特異なのは「コンピュータの振る舞い」を学ばないとまともなコードを書けないことだ。特にメモリの使われ方を(学習用に抽象化された内容でもよいから)押さえておかないと、C言語の変数とポインタにまつわるセマンティクス絡みでバグを仕込んでしまう確率が高くなる。

間が悪いことに、C言語の初学者からすれば、どこまでがC言語の範疇でどこからが下位層の話になるのか、境界が明確ではない。この問題は学習者を惑わせるだけでなく、C言語を教える立場の人の悩みのタネでもある。

まとめ

C言語自体はシンプルでさほど難しくないが、C言語でのプログラミングを学んだり、C言語で実用的なソフトウェアを書いたりすることは難しい。

この難しさは、C言語という抽象化層が薄く容易に穴が開きやすいことから生じている。この「抽象化層の薄さ」はC言語のメリットであり、デメリットである。低水準の領域にてC言語でプログラムを記述できる点が、「抽象化層の薄さ」のメリットである。C言語で何かをなすのが難しい点は、「抽象化層の薄さ」のデメリットである。

*1:かといって無茶苦茶少ない訳でもないが。

*2:例えばRubyのgemやNode.jsのnpmのような機能のこと。

*3:例えば関数、構造体、共用体、不完全型、列挙体、typedef、プリプロセスによるマクロ置換など。

TP-Link ER605 (TL-R605) V2 雑感

故あってTP-LinkのVPNルーターER605を触ったので、感想を書き残しておく。ハードウェア・バージョンはV2.0だ。

業務用相当の機能を持つ新品で安価な有線LANルーターが欲しくて、3年ぐらい前だったらEdgeRouter Xを買っていたところなのだが、どうも最近はEdgeRouter Xの入手経路が不明瞭だし価格も上昇してしまったし――ということで、少々悩んでいたのである。

TP-Linkの製品には、何というか「行儀が悪い」印象があって、今でもあまり積極的に買おうとは思えないのだが……でもAmazonでの実売価格が1万円を切るという事実に我々はあまりにも無力だった……。

良かった点

  • 安い。
  • 筐体が比較的コンパクト。
    • 「NETGEARの8ポートのスイッチングハブ」とほぼ同じぐらいの大きさ。
    • EdgeRouter Xよりは大きいけど、ヤマハRTX810と比べたらはるかに小さい。とはいえRTX810が電源内蔵なのにたいしてER605はACアダプタ前提、という点には留意すべきだろう。
  • Web管理画面だけでも十分に設定できる。

微妙だった点

  • 起動が遅い。
    • 業務用ルーターの類はブートにそこそこ時間がかかるものだと思っているが、それにしても起動に時間がかかりすぎてないだろうか?
  • Web管理画面の操作中にタイムアウトになったことが、変更した設定を適用してみないと分からない。
    • 設定項目をあれこれ入力して、さあ適用――とSaveボタンを押したタイミングで「実はタイムアウトしてました。再ログインしてね」で入力がご破算になるのは、精神的に辛いものがある。もっと早く通知してよ……。
    • この件については、もしかしたらブラウザの影響もあるかもしれない(Firefoxだし、10年以上熟成されたプロファイルを使っているし……)。
  • NTPサーバをIPアドレスでしか設定できない。
    • FQDNでも設定させてほしい。
    • というかCLIでもIPアドレスでしか設定できないのかあ。
  • パケットフィルタリングを設定する時に、直接IPアドレスやネットワークアドレス等を指定することができない。
    • 事前に「Preferences」の画面で利用したいネットワークアドレス等に名前を付けておき、その名前を「FirewallAccess Control」の画面でフィルタリングを設定する時に使用しなくてはならない。
    • この件については、もしかしたら本職のネットワーク技術者にとってはごく当たり前のことかもしれない。しかし、凝ったフィルタリング設定をするのならともかく、簡単な設定で十分な場合には、かえって面倒な仕組みじゃないかなあ。
  • 思ったよりも日本語の情報が少ない。
    • EdgeRouter Xの方が、有志による非公式情報を含めて、日本語での情報が多くないかなあ?

つまづいた点

  • 購入時のファームウェア(2.0.0)から最新版(2.2.4)に一気にアップグレードできなかった。
    • 「2.0.0 → 2.2.2 → 2.2.4」と2段階に分けてアップグレードしたら成功した。
    • なおファームウェアのダウンロードページには「2.2.3からじゃないと2.2.4にアップグレードできないよ」と書いてあったが、肝心の2.2.3は非公開でダウンロードできなかった。2.2.2からアップグレードして成功したっぽいけど、これで良かったのだろうか?
  • NTPサーバのIPアドレスを入力中ないし変更を適用する前に、入力した値が「0.0.0.0」に戻ってしまう。
    • 何やら周期的に元に戻されるようだったので、コピペを駆使してタイミングを見計らってIPアドレスを設定した。
    • この件については、新しいファームウェアでは修正されているかもしれない。

感想

業務用レベルの有線LANルーターは3台目で3メーカー目(Ubiquiti EdgeRouter X、Yamaha RTX810、TP-Link ER605)だけど、メーカーによって癖が違うなあ。

ER605は、凝ったことをせずに「昔ながらの有線LANルーター」的な使い方をする分には、悪くないルーターだと思う(VPNは激重らしいけど)。

なんだけど、Web管理画面のタイムアウトの挙動とか、NTPサーバのIPアドレス入力の件とか、細かいところに粗があるように見えて……元々の「行儀が悪い」という印象と合わさって、本体内部の目に見えない部分の品質が少しばかり気になってしまう。

リピ買いするかどうか、正直迷うところだ。

なお自由度の高さについては圧倒的にEdgeRouter Xに軍配が上がるのだが、これについてはEdgeRouter Xが良い意味でおかしいだけである。

LANに接続している機器のIPv4アドレスを列挙する(ping絨毯爆撃) Windows無情編

続きというか、以前はUnix環境での話だったので、今回はWindowspingで絨毯爆撃してLANに接続している機器のIPアドレスを列挙してみたい。

eel3.hatenablog.com

まずはシンプルに、バッチファイルで逐次実行する方法。

@echo off
setlocal

set ADDRBASE=192.0.2.

for /L %%I in (1, 1, 254) do (
    ping -n 1 -w 100 %ADDRBASE%%%I >nul
    if not errorlevel 1 (
        echo %ADDRBASE%%%I
    )
)
endlocal

Windowspingタイムアウト値をミリ秒単位で設定する。LANに接続している機器の応答性能との兼ね合いはあるが、タイムアウト値を短くすることで実行時間を短縮することができる。

実際、このバッチファイル(タイムアウト100ミリ秒)は手元の環境では2分10秒弱で完了した。

とはいえ逐次実行である以上、どうしても時間がかかってしまう。何とかできないものか
? コマンドプロンプトでは無理そうだが、PowerShellではどうだろうか?

PowerShellで並列処理する方法はいくつかあるが、素のPowerShell 5.1の場合は、バックグラウンドジョブを使う方法とWorkflowを使う方法の2つが考えられる。

バックグラウンドジョブを使う方法で、何も考えずにシンプルにコードを書くと、こんな感じになる。

Set-StrictMode -Version Latest

1..254 | %{
    $addr = "192.0.2.$_"
    Start-Job -ScriptBlock {
        param($addr)
        $out = ping -n 1 -w 500 $addr
        if ("$out" -cnotmatch '100%') {
            $addr
        }
    }
} | Wait-Job | Receive-Job

このスクリプト、手元の環境では5分待っても処理が終わらないのである。タスクマネージャーで確認したところ、大量のプロセスが生成されて動作していた。プロセスの生成数を制御した方がよさそうだ。

ということで、アレコレと試行錯誤してたどり着いた、手元の環境で最も高速なスクリプトはこんな感じ。

Set-StrictMode -Version Latest

Set-Variable -Name ADDRBASE -Value '192.0.2.' -Option Constant

$jobs = @()
for ($i = 1; $i -le 254; $i++) {
    $jobs += Start-Job -ArgumentList "$ADDRBASE$i" -ScriptBlock {
        param($addr)
        $out = ping -n 1 -w 500 $addr
        if ("$out" -cnotmatch '100%') {
            $addr
        }
    }
    $tmp = $jobs | ?{ $_.State -eq 'Running' }
    if (($tmp | Measure-Object).Count -ge 5) {
        Wait-Job $tmp -Any | Out-Null
    }
}
Wait-Job $jobs | Receive-Job | Write-Host
Remove-Job $jobs

信じられないかもしれないが、for文を使っているのもループ中でWait-Jobを実行するタイミングもループ中のWait-Jobのオプション-Anyも、全て手元の環境での高速化に結びついている。現物合わせでこうなった。

このスクリプトは……プロセスを常時5個以上立ち上げてCPUをガンガン占有して処理を行う割に、手元の環境では2分16秒ほどかかる。バッチファイルによる逐次実行版の方が数秒速い上に、CPUをほとんど占有しない。どういうことだ!

バックグラウンドジョブに失望したので、PowerShell 3.0で導入されたWorkflowを使ってみた。

Set-StrictMode -Version Latest

workflow Ping-All {
    ForEach -parallel ($i in @(1..254)) {
        $addr = "192.168.24.$i"
        $out = ping -n 1 -w 500 $addr
        if ("$out" -cnotmatch '100%') {
            $addr
        }
    }
}

Ping-All | Sort-Object { [Version] $_ }

リソースの占有具合はマシになったものの、やはり手元の環境ではそれなりに時間がかかる。とはいえバッチファイルによる逐次実行版よりは少し高速になった。

Windows付属のPowerShellでの並列処理の遅さには不満がある。標準でThreadJob入りになるとよいのだが……今後、PowerShell Coreベースの実装が標準添付されるようになるのかどうかも含めて、少し気になるところである。

LANに接続している機器のIPv4アドレスを列挙する(ping絨毯爆撃)

たまにLANに接続している機器のIPアドレスを知りたい時がある。PCやゲーム機あたりなら本体の機能でIPアドレスを確認すれば済む。しかし相手がホームゲートウェイブロードバンドルータ無線LANアクセスポイントやUSBメディアサーバで、ハードウェア本体にはIPアドレス表示機能がなく、Web管理画面からログインしたいのだけどアドレスが分からないので管理画面にすらたどり着けない──そんな時に、とりあえずLANに接続している機器のIPアドレスを列挙して、そこから絞り込んでいったりする。

UPnPDLNABonjour経由でWeb管理画面にたどり着けるとは限らない。古い機器なのでUPnP的な機能を持っていなかったり、クライアントPCがWindowsでいうところの「ネットワーク探索」を無効にしていたりすることもあるのだ)

他には、例えばLANにネットワーク対応のプリンタを接続して固定IPを割り振りたいのだけど既存のサーバやプリンタその他が使用しているIPアドレスが分からないとか、Raspberry Pi OSを入れたRaspberry Piの初期設定をしたいのだけど諸事情によりディスプレイもキーボードも接続できないので初っ端からssh接続したいとか、そういう場合にも同じようなことをしたくなる。

安直な解法としては、LANの全IPアドレスに対してpingを実行して反応を調べればよい。ファイアーウォール等でpingに応答しない設定にしている機器は列挙できないが、大抵の場合、応答しないのはPCだ(PCだから普通にログオンしてIPアドレスを確認すればよい)。

という訳でping(8)で絨毯爆撃するシェルスクリプトを書いてみた(IPアドレスは各自の環境に合わせて調整すること)。

#!/bin/sh
# -*- coding: utf-8-unix -*-
# vim:fileencoding=utf-8:ff=unix
#
# Worked on Ubuntu 20.04

readonly addrbase=192.0.2.

for i in $(seq 1 254); do
    addr=$addrbase$i
    ping -c 1 -w 1 $addr >/dev/null
    [ $? -eq 0 ] && echo $addr
done

環境によってping(8)のタイムアウトを設定するオプションが異なるので注意。

このスクリプトは、安直に「タイムアウト1秒の設定で1回だけICMP Echoを送信する」という処理をIPアドレスごとに順番に実行している。そのため非常に遅い。LANに接続している機器が少なかったり、接続していてもpingに反応しない機器が多いほど――つまりICMP Echo Replyが返ってくる可能性が低いほど遅くなる。最大で1秒かかる処理を254回実行するので、ワーストで4分13秒ぐらいかかる計算になる*1

4分以上もかかるのは不便なので、高速化してみた。

#!/bin/sh
# -*- coding: utf-8-unix -*-
# vim:fileencoding=utf-8:ff=unix
#
# Worked on Ubuntu 20.04

readonly addrbase=192.0.2.

# do_ping <IP address>
do_ping () {
    ping -c 1 -w 1 $1 >/dev/null
    [ $? -eq 0 ] && echo $1
}

{
    for i in $(seq 1 254); do
        do_ping $addrbase$i &
    done
    wait
} | sort -V

バックグラウンドプロセスを使ってping(8)を並行実行させるようにしてみた。これなら1秒ちょっとで完了する。

並行実行の代償として、IPアドレスの出力順が不定となる。これをソートするのに、IPv4アドレスならGNU coreutilsのsort(1)のオプション-Vが使える。他の環境には-Vが無いので、別のオプションの組み合わせで対応することになる(例えばsort -t . -k 4,4 -nだろうか)。

なおIPv6のことは考えていない。自宅や会社のLANがIPv6になってから考えるつもり。

終わりに

最終的には、こんな感じになった。

https://github.com/eel3/eel3-scripts/blob/master/bin/linux/pingest4

*1:4分14秒ではなく4分13秒なのは、LANに接続している機器のうち1台は自分自身なので、ほぼ確実に数ミリ秒程度でレスポンスが帰ってくるからだ。

「プログラミング経験者向けのCプログラミングの入門書」が欲しい

タイトルに書いたことが全てだが、思考の整理を兼ねて、もう少し深掘りしてみたい。

以下に示す要求に合致した書籍の類を探しているのである:

  1. C言語(言語仕様)よりも「C言語を用いたプログラミング」に主軸を置いた内容。
  2. 最低でもC99、できればC11/C17の利用を前提とした内容とコーディングスタイル。
  3. プログラミング未経験者ではなく「すでに他のモダンな言語によるプログラミングを経験している人」を想定した内容。
  4. C言語(や古典的なC++)でのプログラミングに付き物の「変数と記憶領域」についての解説がある。

イメージとしては「『プログラミング言語C 第2版 ANSI規格準拠』と『Cプログラミング専門課程』を足して2で割って近代化改修して若干マイルドにしたもの」だろうか?

プログラミング言語C 第2版』のターゲット層は1970~1980年代基準の「コンピュータとプログラミングの基礎」を知っている人*1であるし、内容も「Unix系OSのシェルないし類似環境を想定した、ホスト環境向けのCプログラミング」に軸足が置かれている*2。想定読者はプログラミング経験者であるし、内容も「C言語の言語仕様」よりも「Cプログラミング」の入門書としての側面が大きいといえる。

残念なことに『プログラミング言語C 第2版』は進化が止まって久しい*3。Cプログラマの血と汗の結晶である「言語仕様の落とし穴を回避するための防御的なコーディングスタイル」や、モダンな言語仕様による「より楽で明快な書き方」といった側面が抜け落ちたままとなっている。それ故に、Cプログラミングの中級~上級者向けの奥義書としての価値はまだ残っているものの、Cプログラミング初級者に読ませるには問題がありすぎる、という扱いの難しい本になってしまっている。

『Cプログラミング専門課程』は、Cプログラミングとは切っても切れない「変数と記憶領域」について大々的に解説している本だ。細かい部分は置いておくとして、本書で学んだC言語におけるメモリの扱いについての基本的な考え方は、組込み向けのCプログラミングにも流用できるだろう*4

残念ながら本書は絶版しているらしく、入手が難しい。

あと「ANSI CではなくK&R Cでサンプルが書かれている」という特徴もある。C言語分かっている勢なら問題にならないのだけど、C言語の初学者に読んでもらうには問題がある。この点より、「本書を薦めても問題ない読者」のレベルが「本来、この本を読んで欲しい読者」のレベルよりも若干高めになってしまうという、読者層のミスマッチが起きているように思う。

「ぼくのかんがえたさいきょうのCプログラミング入門書」の内容は、『プログラミング言語C 第2版』と『Cプログラミング専門課程』の抱き合わせだ。どちらも、まずサンプルコードのスタイルを近代化(ANSI C対応+防御的なコーディングスタイルに書き換え)するだけでも、喜ぶ人がいると思うのだ。その上で、C11/C17に対応していたら、もう言うことはない。

対象読者については、最近の人がC言語でプログラミング入門するとは思えないので、そろそろ「プログラミング未経験者向け」という制約を取り除くべきだろう。今日日のC言語入門者って、PythonJavaScript/TypeScriptなどの「モダンなプログラミング言語」でプログラミングしたことがある人が大半だと思う。だから、手続き型プログラミング言語の諸々の基本概念についてすでに知っていると仮定した上で、「C言語でプログラミングする場合にはこう書く/こうアプローチする」みたいなスタイルで説明しても許されるように思うのだ。

一方で、『プログラミング言語C 第2版』の頃には考えにくかったシチュエーションだと思うのだが、Pythonなどの「モダンで安全なプログラミング言語」の経験者からすると、C言語でのプログラミングは「原始的で野蛮なプログラミング言語による縛りプレイ」である。その点について、教育用のコンテンツを作成するにあたり、従来とは違う角度からのアプローチが求められるのかもしれない。

開発環境については、素人のことを考えなくてもよいならば、2023年の時点では「Unix系のシェル環境」に一本化しても大丈夫かもしれない。WindowsにもWSLがあるし、何なら学習環境を一括導入できるDockerイメージを用意することもできる。どちらもWeb系のソフトウェア開発経験者なら触ったことがあるだろう。

つらつらと書き連ねてきたが、タイトルにもあるように、モダンな「プログラミング経験者向けのCプログラミングの入門書」が欲しいのである。何か良い本はないだろうか?

*1:コンピュータ科学の基礎知識を持っていて、かつ他の言語(時代的にBASIC・FORTRANPascalあたりだろうか)でのプログラミング経験がある人。

*2:だから、言語仕様の細かい話については、実のところ他の書籍を参照した方がよい。

*3:少なくとも日本語訳については。原書については知らない。

*4:n=1の感想なので、断言まではできないが、私に場合は流用できた。

書籍購入:『C言語本格入門 基礎知識からコンピュータの本質まで』

故あってC99以降に対応したC言語の入門書を探している(4回目)。自習用として問題なさそうなやつ。あと、プログラミング初心者向けの冗長な内容ではなく、プログラミング経験者でも読み進めやすい簡潔な内容のもの。

ぱっと見だが、難易度の面で、少しだけ評価が難しい本だと感じる。

うーん、「C言語でプロダクション向けのコードを書くことになった人」向けの副読本、だろうか?

プログラミング未経験者は、もっと平易なC言語の入門書で学習して、何かしらの「実用的なCLIツール」の劣化コピーを2~3個ぐらい書いてから、本書を手に取った方がよいだろう(それでも当初は手に余るかもしれない)。

プログラミング経験者でも、モダンな高水準言語での経験しかないようならば、他のC言語の入門書と併用した方が安全だろう。

C言語C++*1で何か実用的なものを書いたことがある人なら分かると思うのだが、一般的な高水準言語とは異なる点として、C言語によるプログラミングでは「言語という抽象化層」に穴を空けて下の層を覗き込むことが多々あるし、また下の層の事情を知ることでCプログラミングのレベルが上昇することもある。特に変数と記憶領域(メモリ)がそうだ。

「言語という抽象化層」に穴を空けられる点がC言語の特徴であり、メリットなのだが(でなければOSやデバイスドライバファームウェアの記述言語になれなかっただろう)、反面それがCプログラミングの難しさというデメリットの主要因ともなっている。

本書ではそういう「C言語の面倒くさい部分」も扱っている。お仕事でC言語を使う際に直面する現実でもあるので、C言語でプロダクション向けのコードを書くならば、本書を手に取ったほうがよいだろう。

ただし難易度に注意すること。本書の難しさって、C言語の難しさではなくて、プログラミング言語という抽象化層の下に隠されている「コンピュータ」そのものの面倒くささに起因する部分が多いので、プログラミングのベテランでも下位層の知識が不足していればつまづく可能性があるように思う。

追記:それにしても『Cプログラミング専門課程』復刊してくれないだろうか?

*1:特に古典的なスタイルのC++のこと(モダンC++ではない)。

書籍購入:『独習C 新版』

故あってC99以降に対応したC言語の入門書を探している(3回目)。自習用として問題なさそうなやつ。あと、プログラミング初心者向けの冗長な内容ではなく、プログラミング経験者でも読み進めやすい簡潔な内容のもの。

初めて『独習C』を手に取ったので旧版の実情は知らないのだが、この新版については、プログラミング経験者向けのC言語の入門書という観点では、悪くない選択肢だと思う。

1つの言語でも構わないので、モダンな汎用のプログラミング言語でそこそこ本格的なコードを書いた経験があるならば、C言語を学ぶ際に本書を手に取っても問題ないはずだ。

言い換えると、プログラミング未経験者や、経験者でも「雰囲気でコピペしてた」系の人だと、本書はやや手に余るだろう。

追記:本書の出版時期の都合より仕方ない部分ではあるが、(2023年の視点では)学習環境はUnix環境に統一してしまい、コンパイル実行例でcc(1)を使うようにしても問題ないかもしれない。今ではWindowsでWSLが使えるし、Linuxgcc(1)macOSclang(1)cc(1)で呼び出せる(エイリアスになっている)ことが多いからだ。