11月の LLVM developer meeting で Apple の Dong Gregor 氏が,C言語へのモジュール機能の追加に関するプレゼンテーションを行った。その要旨は次のようなものだ。
プログラマやツール一般にとって,Cプリプロセッサは昔から悩みの種でした。プログラマは拡大するマクロ汚染や行儀の悪いヘッダのインクルード順などの問題と戦わなければなりません。これらの問題を軽減するため,いつもさまざまな回避手段が用いられてきました。LONG_MACRO_PREFIXES や include ガード,場合によってはライブラリマクロの #undef といった方法です。
一方,ツールは同じヘッダを繰り返し処理することに関連した,マクロ特有のスケーラビリティに関する問題への対処を余儀なくされます。実行ごとに異なるプリプロセス処理のコンテキストによって,ヘッダの解釈される方法が影響を受けるからです -- ほとんどのプログラマがそんなことは望まないにも関わらず。
今回提案するモジュール機能は,各ライブラリのインターフェースを分離した上で効率的なシリアライズ表現に(一度だけ) コンパイルしておいて,ライブラリ使用時にはそれをインポートすることにより,プログラマのエクスペリエンスとコンパイルプロセスのスケーラビリティを改善し,この問題を解決しようとするものです。
この背景には,非常にシンプルなファイルのコンパイル時でさえ,プリプロセッサを使用して大量のヘッダをインクルードしなければならない,という事実がある。これを回避してコンパイルの速度をアップするとともに,一度解析されたヘッダを再利用できるようにする,というのが提案の基本的な前提だ。有名な "Hello World" プログラムを例に取るならば,64文字のCプログラムがプリプロセス後は 11,074文字になり,81文字のC++プログラムでは1,161,033文字にも達する。加えて氏は,ヘッダの再解析がコードの不安定さに結び付く可能性についても指摘した。インクルード処理がその時点でのプリプロセッサの状態に依存する,というのがその理由だ (例えば #include <stdio.h>
の前に #define FILE "myfile.txt"
という定義があれば,プリプロセッサがヘッダを間違って解釈することによってビルドが失敗する)。
提案では,新しいキーワード import
を使ってモジュールをロードする。プリプロセッサのテキスト的なインクルードとは違い,コンパイラは修正バージョンを判断することができるので,モジュールの解析は一回行うだけでよい。同じモジュールが再度必要になった場合も毎回処理を行う必要はなく,先回解析した同じデータ構造を利用することができる。
モジュールにサブモジュールをインポートして,モジュールをネストすることも可能である。プレゼンテーションでは std
モジュール内に定義された stdio
サブモジュールをインクルードする import std.stdio
という定義が例として挙げられていた。インポートしたモジュールの API はすべてクライアントに公開されるが,非公開 API と指定して隠蔽することもできる。それには public キーワードを使用して,公開するものとしないものを宣言することが必要だ。
// stdio.c export std.stdio: public: typedef struct { … } FILE; int printf(const char*, …) { … }
この例で注目すべきなのは,ファイルの実装のみを提供すればよい – ヘッダは必要ないという点だ。export
にはモジュールの名称 (この例では std.stdio
) を指定する。public
は API の公開部分と非公開部分を分けるものだ。これをコンパイルすると,ライブラリに加えて,関数タイプとマクロに関する詳細な情報を持ったメタデータがクライアントコードから利用可能になる。
これらは当然ながら標準ではなく,将来に向けての提案だ。では,どうやって実現するのだろうか。提案された方法は,既存モジュールの公開 API としてヘッダを使用しながら,ヘッダのセットとしてモジュールを定義する,というものだ。
// /usr/include/module.map module std { module stdio { header "stdio.h" } module stdlib { header "stdlib.h" } module math { header "math.h" } exclude header "assert.h" } module ClangAST { umbrella "AST/AST.h" module * { } } // AST/Decl.h の代わりに "import ClangAST.Decl" が使用可能
以降のモジュール生成を簡単にする (もうひとつは Objective-C フレームワークへのモジュール公開を容易にする) ために,同じディレクトリにあるヘッダ全体をひとつのモジュールとしてエクスポートする ’アンブレラ (umbrella) モジュール' が用意されている。
コンパイラがモジュールを処理できれば,1パスでヘッダからモジュールを構築して,以降のヘッダにはそのモジュール情報を再利用できるというメリットもある。(コンパイル後のモジュールのフォーマットはまだ規定されていないので,コンパイラ依存になるかも知れない。) 実行時に必要なライブラリなどといった,付加的なメタ情報をモジュールに追加することも考えられる。 こうすれば,リンク実行時に -l フラグの長々とした羅列をユーザが定義しなくても,コンパイラが各モジュールに必要なリンク時フラグを選択できるようになる。
モジュールを利用するために利用者側で必要なのは,#include
を対応する import
に変更することだけだ。さらにプリプロセスされたモジュールには,どのモジュールがどの関数あるいは型をエクスポートしているか,という情報が含まれているので,従来より適確なコンパイラメッセージを出力することも可能だ。単にフェールオーバするのではなく,追加が必要なインポートを提示するようなコンパイルエラー表示や IDE でのクイックフィックスが実現できる。
モジュール情報の再利用のメリットとして最後に挙げられるのは,デバッグ情報のモジュールへの関連付けだ。すべてのオブジェクトファイルに複製を添付する必要がなくなることで,コンパイラとリンカが扱うデバッグ情報が少なくなり,結果としてコンパイル時間を短縮することができる。詳細な型情報をデバッガに渡すことも可能になるので,(各オブジェクトファイルにインラインテキストとして埋め込んでおかなくても) モジュールで定義された型を正確にデバッグ表示できるようになる。
モジュール提案の真の価値は,変更を最小限に留めるというユーザの要求と (おもにコンパイル速度の向上とエラーメッセージ/デバッグ情報の改善という面で) メリットを両立しながら,さらに既存ツールからの移行パスと互換性をも提供するという点にある。プリプロセッサ命令をひとつずつモジュールベースのインポートに切り替えながらファイルをアップデートしていく,というインクリメンタルな移行作業も可能だ。今回のプレゼンテーションではコンパイル速度の測定結果は公開されなかったが,LLVM を対象としたモジュールの実装作業はすでに進行中である。バージョン管理やネームスペース (主に後方互換性を持ったビルドを行うために) に関するメリットにも触れていなかったが,広範に利用されるようになれば,C および C++ ベースのプログラムのコンパイル速度は劇的に向上するはずだ。後方互換性に関しては設計上でも意識されていて,LLVM のブロック機能と同じように,他のコンパイラや標準で必要なインクルード定義の処理も可能となる予定である。しかしながら,広く使用されている C および C++ コンパイラの中で,このイノベーションを率先してリードしているのは現時点では LLVM コンパイラツールチェーンがあるのみだ。今回提案された機能が他のコンパイラにも導入されるかどうかは,LLVM 実装の成功とそれによって得られるメリットの如何によるだろう。