
2025/12/07 10:41
Eurydice: a Rust to C compiler (yes)
RSS: https://news.ycombinator.com/rss
要約▶
Japanese Translation:
Summary
Eurydice は、単一の Rust コードベースを 読みやすく、標準に準拠した C または C++ に変換する Rust‑to‑C コンパイラです。
Charon インターフェース経由で rustc の MIR に接続するため、Rust コンパイラを再実装する必要がなく、少ない構文で忠実な意味論的翻訳が可能です。
Core transformations
- Whole‑program monomorphisation:ジェネリクスを具体型に展開します。
- Pattern‑match compilation:Rust の
式を C でタグ付きユニオンへ変換します。match - Iterator recognition:慣用的な Rust のイテレータチェーンをネイティブ C のループへ変換します。
- Array initialisation handling:ゼロ初期化、初期化リスト、およびループベースの初期化を区別し、Rust のセマンティクスに合わせます。
Readability choices
- Rust 構造体 → C 構造体(flexible array members を使用)。
- 単一バリアントの列挙型はタグを省略して簡潔化。
- 制御フローは Charon 経由で再構築し、raw goto ではなくより明確なコードにします。
コンパイラ自体は OCaml の約 8 k 行です:MIR‑to‑KaRaMeL AST 変換が約 3 k 行、カスタムナノパス/最適化が約 5 k 行、内部構文パターン用の小さな ppx が付随。生成コードは C11/C++20 対応または C++17 対応で、マクロによりコンパウンド初期化子と列挙型のメンバーポインタの違いを隠します。
Limitations
- Rust と C の型レイアウト不一致の可能性。
- DST 処理から生じる厳格なエイリアシング違反。
- 単一プラットフォームの MIR ビュー(cfg の調整が必要)。
- ターゲット固有のモノマル化インスタンスの煩雑な設定。
デプロイには手書きのグループコード―マクロと static inline 関数―を用いて重複する多態コードを削減します。例えば、配列に対して
Eq トレイトを実装する際にマクロを使用します。
Future work
- Microsoft/Google の暗号ライブラリとの統合。
- 仮想テーブルによる動的トレイトのサポート。
- Charon を通じた完全モノマル化。
- 2026 年までに Rust 標準ライブラリ全体を抽出し、開発者が単一の権威ある Rust ソースを維持しつつ、レガシーシステム向けに C を段階的に生成できるようにする。
プロジェクトは約 8 k 行の OCaml で、GitHub ユーザー @ssyram と @lin23299 が貢献し、ギリシャ神話をテーマとした命名規則(Eurydice, Aeneas, Charon)に従っています。Rust と既存の C エコシステム間でよりスムーズな相互運用性を提供し、両言語が必要な企業の重複作業を削減するとともに、読みやすいクロス言語コンパイラ出力の再利用可能モデルを提示します。
本文
ここ数年で私が最も驚いたことの一つは、単に「C を Rust にコンパイルする」だけではなく、「Rust を C にコンパイルする」という点にも人々が関心を持っているという事実でした。なぜなら、明らかにメモリ安全性などの理由で C から Rust への移行は重要ですが、逆方向も同様に重要だと多くの方が感じているからです!なんだって?
数年前に簡単に触れたことがありますが、このプロジェクトに対する関心の高さには驚かされました。そこで今日は「Rust を C にコンパイルする」ことについてもう少し詳しく掘り下げてみましょう。
Rust の採用障壁
Rust は導入面で大きな進展を遂げており、特に新しいコードを書く際には非常に価値のある選択肢です。私が以前勤めていた会社も、新しく働いている会社も、ほぼすべてのプロジェクトが純粋な Rust で書かれているか、Rust のコンポーネントを持っています。Windows カーネルドライバでも今では Rust で書くことが可能です―驚きですね。
しかし、もしあなたのプロジェクトが「多種多様なターゲットアーキテクチャ・OS・ディストリビューション・ツールチェーン上でビルドされるオープンソースライブラリ」だったとすると、そのうちの一つは Rust をサポートしていない可能性が高いです。暗号ライブラリを例に挙げれば、変わった組み込みターゲット向けに作られた不思議なコンパイラを使っている人々がいて、彼らは「自分で暗号機能を実装しないように」という指示を受けており、あなたのライブラリをビルドしたいと考えています。あるいはメモリエラーに悩まされるフォーマットライブラリを Rust に移植したいケースもあります。さらには社内分析ツールが C コードしか動かないという状況も想像できます。どんなシナリオでも、やがて「Rust への移行を妨げるレガシーなユースケース」が存在し、それが解消されるまで(2035 年に LTS バージョンがすべて退役する頃)あなたは Rust を採用できません。
つまり、Rust‑to‑C コンパイラを使う意思がある場合のみ、この障壁を乗り越えることができます。
なぜ必要なのか?
Rust が C にコンパイル可能な後方互換性のシナリオは、以下のような目的に役立ちます。
- 段階的移行 – コードベースを Rust にポートし、データ型・パターンマッチング・多態性・メモリ安全性などの優れた機能を活用して再構築・クリーンアップします。C バージョンは並行して存在するため、ユーザー基盤を排除せずに済みます。
- 唯一正統なバージョン – Rust コードが「正統」であり、C コードは自動的(CI で)に生成される形です。両者の同期を CI ジョブで確認できます。
- 問題点の可視化 – Rust をデフォルトにし、C バックアップを
フラグで隠すことで、「まだ Rust に移行できないユーザー」を最終的に列挙することが可能になります。--write-us-an-email
このようなメリットに惹かれたら、ぜひ Eurydice をご覧ください。
Eurydice とは
Eurydice は「Rust → C コンパイラ」であり、読みやすい C コードを生成することを目指しています。
可読性は主観的ですが、Rust が 全プログラム単位のモノポリゼーション(whole‑program monomorphization)に依存しているため、C コードは Rust コードよりも冗長になるでしょう。実際に
libcrux を C にコンパイルした結果を見てください。
テストスイートの出力はバージョン管理下にあり、さらに多くのテストが存在します。以下の例では Rust のオリジナルと比較してみます。
Eurydice の設計
Eurydice は MIR(Mid-level Intermediate Representation) で直接プラグインし、Charon を利用して
rustc の内部構造を再実装する手間を省きます。Charon に関する論文では、そのアーキテクチャについて詳しく説明しています。
MIR レベルでのプラグインメリット
- 構文糖(syntactic sugar)を解釈しなくてよい – したがって、Rust の意味論に忠実な翻訳が可能です。
- コンパイル対象となる構造体が少ない – C に変換する際の複雑さが軽減されます。
それでも Rust を C に翻訳することは容易ではありません。主に以下を行う必要があります。
- 型と const‑generic 引数に対して 全プログラムモノポリゼーション
- パターンマッチをタグ付きユニオンへ変換
- ループが可能なイテレータはネイティブ C for‑loop に置き換え
- 配列の繰り返し表現を合理的にコンパイル(可能ならゼロ初期化、そうでない場合は initializer list、コード量が多いと for‑loop)
- 可視性・
・static
など C 固有のルールを処理inline
さらに生成されるコードは「読みやすさ」を保つ必要があります。たとえば Rust の構造体(DST を含む)は 柔軟配列メンバ を用いて C 構造体に変換します。また、単一ケースの enum にはタグを付けず、可能な限り汎用的なタグ付きユニオンパターンは避けます。Charon は MIR の制御フローグラフ(CFG)を直接 C にコンパイルせず、代わりに制御フローを再構築します。
低レベルの詳細
- Rust の配列は値型であるため、C では 構造体内に埋め込む(例:
)。以前はポインタを使い[u32; 8] → struct { uint32_t data[8]; }
に頼っていましたが、タイプ汎用性がなく多くのケースで問題が発生していたためです。memcpy - C の lvalue 概念では Rust よりも変数宣言が必要になるケースがあります。たとえば
を直接コンパイルすることはできず、配列に名前を付ける必要があります。&[0u32; 1] - C の評価順序の曖昧さから、中間計算結果を変数に保存して明示的な順序制御が必要です。
- 全プログラムモノポリゼーションにより、同一型・関数が異なるジェネリック引数で複数回生成されます。現在はビルトインフェーズで処理していますが、将来的には Charon のサポートを利用する予定です。
- Peephole 最適化は不可欠です(例:
を認識して in‑place 初期化コードを生成、array::from_fn
トレイトのインスタンスに対してEq
を使うなど)。memcmp
最後に設計上、Eurydice は Rust よりも多くの振る舞いを定義できることがあります。たとえば Rust では整数オーバーフローでパニックしますが、Eurydice コンパイル済みコードはそうしません。入力コードが検証済みでパニックが発生しない前提にしていますが、これは簡単に変更可能です。
実際にはトレイトを使用すると C コードの量が増大します。そのため、構成ファイルでモノポリゼーションインスタンスの配置を制御し、大規模プロジェクトでは手動介入が必要になる場合があります。
Eurydice の実装
-
MIR → KaRaMeL AST
Charon から取得した MIR AST を KaRaMeL の内部 AST(約 3000 行の OCaml コード)に変換します。トレイトメソッドとそのモノポリゼーションが主な作業です。 -
Nano‑passes
約 30 個のナノパスで KaRaMeL AST を簡略化し、C コンパイル対象となる形に整えます。既存の KaRaMeL 用に書かれたものも利用しつつ、新規に約 5000 行の OCaml コードを追加しました。 -
変数代入削除
MIR ではすべての変数が未初期化状態から始まり、最初の値で代入されます。最初のフェーズでこれらを「宣言 + 初期化」に再構築し、コード品質を向上させています。これは MIR を使う欠点ですが、可読性を高めるために選択しました。 -
カスタム OCaml 文法拡張
多数の peephole 最適化を管理するため、KaRaMeL 内部言語で具体的なパターンを書けるようにします。これはコンパイル時に ppx で解析・変換され、OCaml AST ノードとして生成されます。
Eurydice 生成コードのデプロイ
Eurydice が生成したコードは 手書きのグルー(マクロや static inline 関数) を必要とします。多くの場合、複数の特化版を生成する代わりに「型を引数に取る単一マクロ」を作成すると便利です。例として
Eq トレイトの配列実装では、Eurydice_array_eq(a1, a2, len, t) というマクロを出力し、内部で !(memcmp(a1, a2, len*sizeof(t))) を展開します。
生成コードは以下のいずれかです。
- C11 および C++20 対応
- C++17 対応だが純粋な C には非対応
Rust の enum は任意の式位置で使用できます。Eurydice はコンパウンドイニシャライザ(例:
Foo { .tag = bar, .value = { .case_Foo = { .bar = baz }}})や、C++20 の designated initializers を利用します。マクロで構文差異を隠蔽しています。C++17(designated initializers が無い場合)はメンバポインタを用いて同様の効果を実現できます。
Eurydice の限界
- オブジェクトレイアウト: C と Rust で完全に同一になる保証はありません。MIR からレイアウト情報を解析し、コンパイラ固有のアラインメントディレクティブを出力することも可能ですが、現在は実装していません。
- Strict aliasing の違反: ユーザー定義 DST を作成するときにポインタ型をキャストします(例:構造体内の配列 → 柔軟配列メンバ)。現状では
でビルドしてください。-fno-strict-aliasing - プラットフォーム固有コード: Eurydice は MIR を cfg 調整後に見るため、マルチプラットフォームのコードでは「一つのバージョン(AVX2, ARM64 等)しか見えません」。対策が必要です。
- 設定の複雑さ: モノポリゼーションが広範囲にわたるため、設定言語で「
を参照する型は AVX2 向けファイルに分離して__m256i
でビルド」などを表現しなければならず、手間がかかります。-mavx2
今後の展望
現在は Microsoft と Google の暗号ライブラリへの Eurydice 生成コード統合作業中です。コミュニティも GitHub ユーザー @ssyram と @lin23299 の貢献で拡大しています。次なる目標は dyn トレイト(vtable) のサポートと、Charon が提供するモノポリゼーションを利用して MIR を「Rust コンパイラが生成するもの」と同一に保つことです。
さらに野心的なゴールとして、2026 年までに Rust 標準ライブラリ全体を Eurydice で抽出 しようと考えています。実現は容易ではありませんが、達成可能だと信じています。
PS:名前の由来
「Eurydice」という名前についてよく質問されます。このプロジェクトは Aeneas と Charon と同じインフラを多く共有しているため、ギリシャ神話にちなんだテーマで名付けました。ユリディケ(Eurydice)の物語が、私にとって「C コード生成の地獄から救われ、生者の世界へ戻る」というイメージに響きました――残念ながら、実際にはそのような奇跡は起きませんでした。