
2025/12/16 22:33
Rust GCC back end: Why and how
RSS: https://news.ycombinator.com/rss
要約▶
Japanese Translation:
Rust のコンパイラ(
)は、ソースコードを実行可能な機械語に変換する一連のパスで構成されており、フロントエンドとバックエンドが明確に分離されています。フロントエンドは Rust の構文を抽象構文木(AST)へ解析し、その後 HIR(High‑Level Intermediate Representation)と MIR(Mid‑Level IR)という高レベル表現に変換します。これらの段階で型チェック、借用分析、およびその他の言語固有の検査が行われ、制御はバックエンドへ渡されます。rustcバックエンドは MIR を消費し、LLVM または GCC を介してターゲット特定の機械語を生成します。新しい Rust のバックエンドを書き込むには、
からrustc_codegen_ssa、CodegenBackend、ExtraBackendMethodsといったトレイトを実装する必要があります。バックエンドのエントリポイントは、以下に示すエクスポートされた関数です。WriteBackendMethods#[no_mangle] pub fn _rustc_codegen_backend() -> Box<dyn CodegenBackend>これはバックエンド実装のインスタンスを返します。記事では、定数文字列(
)を作成し、関数パラメータにアノテーション(例:const_strは「パラメータ 1 が null であってはならない」)を付与するサンプルコードを示しています。nonnull(1)また、GCC バックエンド(
)が必要となる理由も説明されています。LLVM は Dreamcast のような古いプロセッサをサポートしていないため、libgccjit バインディング(rustc_codegen_gccとgccjit-sys)を使用して AOT コードを生成します。これは Rust 用に再実装されたパーシングと検査を行う GCC の別個の C++ フロントエンドであるgccjitとは対照的です。gccrs最適化は両方のバックエンドで適用されます。LLVM は到達不可能なコードを除去でき、GCC バックエンドも Rust の保証に基づく類似の最適化を行います。記事ではさらに多くの最適化(例:Matt Godbolt が「Advent of Compiler Optimizations」シリーズで議論したもの)が存在することにも触れています。
今後は、LLVM の機能セットに合わせてより多くの GCC 固有の最適化を追加し、新しいターゲットへの Rust の展開を拡大することが提案されています。これはレガシーやニッチなプロセッサで Rust を必要とする開発者に恩恵をもたらし、共有された最適化手法によって広範なコンパイラコミュニティを豊かにします。
本文
Rust でのコンパイラとバックエンドの仕組み
はじめに
Rust コンパイラ(
rustc)は、ソースコードを読み込み、最終的にはターゲットプロセッサ用のバイナリコードを生成します。デフォルトでは LLVM をバックエンドとして使用していますが、Cranelift や GCC など他のバックエンドも存在します。本稿では、特に GCC バックエンド がどのように機能するかを解説します。
コンパイルパス(Passes)
コンパイラは「パス」と呼ばれる複数段階で処理を行います。
各パスは独自の AST(抽象構文木)を生成し、次のパスへ渡します。Rust の簡略化例を示すと以下のようになります。
| ステップ | 内容 |
|---|---|
| AST | 構文が正しいかチェック |
| HIR | 型が有効か確認 |
| MIR | ライフタイム検査・借用チェッカー実行 |
| codegen | バイナリコード生成(LLVM/GCC/Cranelift 等) |
備考
パスの詳細を深く知りたい場合は、より長い解説を書きます。
フロントエンドとバックエンド
-
フロントエンド
- コード解析(パース・型チェック・借用チェッカーなど)を担当
- AST → HIR → MIR の変換まで行う
-
バックエンド
- フロントエンドが生成した情報(AST、HIR、MIR)を取り込み、実際の機械語やアセンブリコードへ変換する
- LLVM や GCC への API 呼び出しを通じて最適化・コード生成を行う
なぜ GCC バックエンドが必要なのか
LLVM は比較的新しく(2003 年)作られたため、古いプロセッサはサポートされていません。
例:Dreamcast のような旧世代プラットフォーム向けに Rust プログラムをビルドしたい場合、GCC バックエンドが唯一の選択肢となります。
Dreamcast 用 Rust ビルドガイド などは別途紹介しています。
gccrs
と GCC バックエンド
gccrs| 名称 | 内容 |
|---|---|
| gccrs | C++ で書かれた GCC のフロントエンド。Rust の フロントエンドを再実装する必要がある |
GCC バックエンド() | Rust コンパイラのコード生成部分に特化したバックエンド。AST → GCC API への橋渡しのみ行う |
GCC は内部 API を公開していないため、libgccjit を利用します。
は AOT(Ahead‑of‑Time)コンパイルをサポートするライブラリです。libgccjit
libgccjit の構成
– 必要な C インターフェースを再宣言gccjit-sys
–gccjit
上にラッパー API を提供gccjit-sys
Rust で書いたコンパイラから GCC を利用する場合、これらのバインディングを使います。
Rust バックエンドの実装
Rust コンパイラは
rustc_codegen_ssa クレートで抽象化されたインターフェースを提供しています。バックエンドは以下のトレイトを実装します。
CodegenBackend ExtraBackendMethods WriteBackendMethods
また、必ず次のエントリポイント関数を定義する必要があります。
#[no_mangle] pub fn _rustc_codegen_backend() -> Box<dyn CodegenBackend> { /* ここにバックエンド実装を返す */ }
GCC バックエンドでの例:定数文字列生成
ConstCodegenMethods トレイト内の const_str を実装します。以下はコメント付きサンプルです。
impl<'gcc, 'tcx> ConstCodegenMethods for CodegenCx<'gcc, 'tcx> { /// Returns the pointer to the string and its length. fn const_str(&self, s: &str) -> (RValue<'gcc>, RValue<'gcc>) { // 1. キャッシュ取得 let mut const_str_cache = self.const_str_cache.borrow_mut(); // 2. 既に登録済みか確認、未登録なら作成 let str_global = const_str_cache.get(s).copied().unwrap_or_else(|| { // GCC API を使って文字列リテラルを生成 let string = self.context.new_string_literal(s); // シンボル名を決定し、グローバル変数として宣言 let sym = self.generate_local_symbol_name("str"); let global = self.declare_private_global(&sym, self.val_ty(string)); // キャッシュに保存して返す const_str_cache.insert(s.to_owned(), global); global }); // 3. 長さを取得 let len = s.len(); // 4. ポインタ型へキャスト(C の `*const char` に相当) let cs = self.const_ptrcast( str_global.get_address(None), self.type_ptr_to(self.layout_of(self.tcx.types.str_).gcc_type(self)), ); // 5. (ポインタ, 長さ) を返す (cs, self.const_usize(len as _)) } }
重要ポイント
- キャッシュ:同じ文字列を複数回生成しないようにする
- 型変換:Rust の
→ C の&str
に適切にキャスト*const char - 長さ:C では文字列の終端は必ず 0 であるため、実際の長さを渡す
Rust バックエンドが行う追加情報・最適化
GCC(や LLVM)に渡す前に、Rust の知識を利用してコード生成時に属性を付与します。
例:参照は NULL になり得ないので
nonnull 属性を付けます。
// Rust コード fn t(a: &i32) -> i32 { *a }
C 版(属性無し):
int t(int *a) { if (!a) return -1; return *a; }
LLVM/GCC に
nonnull(1) 属性を付与すると、以下のようにコンパイルされます。
_attribute_((nonnull(1))) int t(int *a) { return *a; // NULL チェックが不要 }
なぜチェックを残す?
Rust の型情報を活かして最適化を行い、不要な分岐を削除します。
まとめ
- Rust コンパイラはフロントエンドとバックエンドに分離されている。
- GCC バックエンド(
)は、Rust の AST を GCC に渡す橋渡し役である。rustc_codegen_gcc
を介して AOT コンパイルが可能。libgccjit- バックエンド実装ではキャッシュや型変換、属性付与などの最適化を行う。
このように、Rust のバックエンドは単なるコード生成だけでなく、コンパイラ独自の情報を活かして効率的なバイナリを作り出します。
この記事は私の猫にインスピレーションをもらって書きました。