見出し画像

手探りでClangを触り始める。アーキテクチャ編

前回の記事で、Windowsから降りるための第一歩としてClangに移ろう、という話を書いた。

戦略は決まった。だが、正直に言うと、Clangそのものについて、私はよく知らなかった。名前と、LLVMというワードと、なんとなくOSS系のコンパイラらしい、という程度の認識。30年以上プログラムを書いてきたのに、この領域は触ったことがなかった。

だから、触り始める前に、まず何者なのかを理解するところから始めることにした。この記事は、その過程の記録になる。


Clangって何者?

最初の質問はこれだった。「Clangって、GCCみたいなコンパイラでしょ?」と思っていたら、半分正解で半分違った。

Clangは、コンパイラの"フロントエンド"だった。

コンパイラは、大きく二段階に分かれている。

  • フロントエンド: ソースコードを解析して、中間表現に変換する

  • バックエンド: 中間表現を、各CPU向けの機械語に変換する

GCCやMSVCは、この両方を一体化して持っている。モノリシックな設計。

一方、Clangはフロントエンドだけ。では、バックエンドは誰がやるのか。LLVMというプロジェクトがやる。Clang(フロントエンド) + LLVM(バックエンド)で、初めてGCCやMSVCと並ぶコンパイラになる。

中間のLLVM IRとLLVMがポイント

この分離設計が、Clangの一番の特徴だった。


分離設計が何を生むか

最初、「分離してるだけで何が嬉しいの?」と思った。でも調べていくと、ここから派生する利点が大きいことが分かってきた。

フロントエンドとバックエンドが**共通の中間表現(LLVM IR)**で繋がっている。だから、部品の付け替えができる。

  • 他の言語のフロントエンドもLLVMに繋げる → Rust、Swift、Juliaは全部LLVMバックエンドに乗っている

  • 静的解析ツール(clang-tidy) がフロントエンドを共有できる

  • エディタの補完サーバ(clangd) も同じ基盤で動く

  • サニタイザ(AddressSanitizer等) もLLVM経由で提供される

つまりClangは「コンパイラ」というより、C/C++のための解析・変換インフラ一式だった。MSVCを使っているだけだと見えない世界が、Clang/LLVMエコシステムには広がっている。


LLVMって、何の略?

ここで素朴な疑問。LLVMって何の略語?

調べると、元々は "Low Level Virtual Machine" だった。ただし今は正式には略語ではない。公式には「LLVMというプロジェクト名であり、もはや略語ではない」という扱い。

歴史的経緯がある。初期のLLVMは「中間表現を解釈実行する仮想マシン」という構想も持っていたらしい。だが実際に育ったのは静的コンパイラ基盤としての方向で、仮想マシンではなくなった。名前の由来と実態がズレたので、略語の解体が進んだ、ということらしい。

IRIntermediate Representation(中間表現)。これは一般的なコンパイラ用語で、LLVMに限らない。GCCにもGIMPLEというIRがある。


LLVM IRを見てみる

百聞は一見に如かず。LLVM IRが実際にどんなものか、見てみる。

Cで書いた int add(int a, int b) { return a + b; } をLLVM IRにすると、こんな感じらしい:

define i32 @add(i32 %a, i32 %b) {
  %sum = add i32 %a, %b
  ret i32 %sum
}

アセンブリっぽい。だが、特定のCPUには依存していない。i32という抽象型を使い、レジスタ割り当てもまだ行われていない。ここまでがClangの仕事で、ここから先のCPU特化処理はLLVMバックエンドの仕事。

なるほど、と思った。CPU差を吸収する層があって、そこから先が分かれるわけだ。


誤解1: LLVM IRはプラットフォーム非依存ではない

ここで、私は大きな誤解をしていた。

「LLVM IRって、JavaバイトコードみたいなWrite Once Run Anywhereじゃないの?」

違った。LLVM IRはプラットフォーム非依存ではない

LLVM IRのファイルを見ると、先頭にこんな記述がある:

target datalayout = "e-m:e-p270:32:32-..."
target triple = "x86_64-pc-windows-msvc"

ターゲットOSとABIが、IRの時点で明記されている。windows-msvc / linux-gnu / linux-musl でIRは変わる。構造体パッキングも、呼び出し規約も、例外処理方式も違う。

さらに致命的なのは、OS APIのシンボル名はそのままIRに載ること。CreateThreadというWin32関数を呼ぶコードは、LLVM IRに落としてもCreateThreadのままだ。LLVMはこれを「OS固有かどうか」を判定しない。単に「外部シンボルを呼ぶ」という情報として記録するだけ。

つまり、LLVM IRが抽象化しているのはCPU差だけ。OS差は何も抽象化しない。

比較表にするとこうなる:

.NET CIL も WebAssemblyも今回初めて知った。

LLVMはネイティブコード生成インフラであって、仮想マシンではない。名前に"VM"が残っているのは歴史的経緯で、実態は違う。

これが分かると、移植に対する期待値が調整される。「LLVMに乗せればOS差が消える」という甘い話ではない。OS API依存は、ソースコードレベルで剥がすしかない。


誤解2: Clangは一つじゃない

Windowsでの運用を調べていて、もう一つ戸惑ったことがある。

「Clangを使う」と言っても、実は2つの顔がある

  • clang-cl — MSVC互換ドライバ。/O2 /W4 /Fo: といったMSVCのオプション文法を受け付ける

  • clang — Unix風ドライバ。-O2 -Wall -o といったGCC的な文法


どっちも、Windowsで動くけど、軸をどちらに置いてるかって話。

中身のコンパイラは同じ。違うのは「顔」(ドライバ)と、それに伴うABIやリンカ周りの立ち位置。

clang-clは、MSVCの世界にそのまま乗る。Windows SDK、MSVCのABI、link.exe。既存のVisual Studioプロジェクトのコンパイラパスをcl.exeからclang-cl.exeに差し替えるだけで動く。

clang(素)は、UNIX風の別世界を作る。リンカはlld、ランタイムはMinGW-w64など、MSVC系を避けた構成。GCC/Itanium ABI互換。

この2つ、ABIが違うのでオブジェクトファイルに互換性がない。MSVCでビルドされた既存の.libとリンクしたいならclang-cl、Linux移行を見据えるなら素clang、という使い分けになる。

前回の記事の文脈で言うと、Linux移行を視野に入れるなら素clang一択。clang-clで中間ビルドを作ると、Linux移行時にもう一度ABI剥がしが必要になる。


マングリング、という概念

ここでマングリング(name mangling)という用語に出会った。正直、30年以上プログラムを書いてきて、初めて聞いた。

考えてみれば、これも自然な話だ。Cには原則マングリングがないので、Cを中心にやっていると一生出会わない。C++をやる人、あるいはコンパイラ間の.lib受け渡しで詰まる人が遭遇する概念らしい。

C++にはオーバーロードがある:

int add(int, int);
double add(double, double);
namespace math { int add(int, int); }

ソース上は全部「add」だが、リンカは文字列マッチングしかできない。そこで、コンパイラが型情報・名前空間を全部エンコードして、ユニークな文字列に変換してからリンカに渡す。これがマングリング、ということらしい。

実例を見ると、同じint add(int, int)が、ABIによってこう変わる:

  • GCC/Clang(Itanium ABI): _Z3addii

  • MSVC: ?add@@YAHHH@Z

全く別物。これが、clang-clと素clangで.obj/.libが互換性を失う直接の原因だった。

そして、extern "C"でマングリングを止められる、と知った。C++からCライブラリを呼ぶときのあれ。

#ifdef __cplusplus
extern "C" {
#endif
// C関数の宣言
#ifdef __cplusplus
}
#endif

FAのベンダー提供ヘッダで、よく見るやつ。これが何のための呪文なのか、ずっと「おまじない」として扱っていた。今回調べて、初めて意味を理解した。「C++から呼ばれてもマングリングしないで」という約束だった。

長年書いてきたコードの中に、意味を理解しないまま書いていた箇所があった、ということになる。ちょっと気恥ずかしい発見だったが、知らないまま通過していた概念に名前が付いたのは、収穫だ。


全体像の整理

ここまで調べて、Clang/LLVMの全体像が頭に入った。概念の地図、という感じ。

  1. Clangはフロントエンド、LLVMがバックエンド。分離設計

  2. LLVM IRはCPU差を吸収するがOS差は吸収しない。移植の期待値を下げる必要がある

  3. Clangには2つの顔(clang-cl / 素clang)がある。ABIが違うので混ぜられない

  4. マングリング方式がABIの境界線。C++の互換性問題の本丸

「Clang = GCCの別実装」ぐらいの認識だったところから、**「LLVMエコシステム全体を背負ったツール群」**という認識に上書きされた。これは、触る前に知っておいてよかった。


次は何をするか

概念は掴めたので、次は実際に触るフェーズに入る。

  • LLVMをWindowsにインストールする

  • 小さいプロジェクトをClangでビルドしてみる

  • MSVCとの差を、警告や診断メッセージで体感する

手を動かしながら、「思ってたのと違う」ポイントを拾っていく。たぶん、また誤解が見つかるはず。そのたびに記事にしていこうと思う。


触り始める前に、地図を描く時間を取った。地図を持たずに歩き始めると、どこで詰まっているのか自分でも分からなくなる。今回はそれを避けたかった。

コンパイラの話は深い。深入りすれば、ドラゴンブックやタイガーブックの世界に入っていく。だが、今の目的はプラットフォーム移行であって、コンパイラ研究ではない。必要な分だけ、道具として理解する。そういう距離感で付き合っていこうと思う。

いいなと思ったら応援しよう!