rustc_codegen_clr は Rust コードを C と .NET 向けに生成するプロジェクトで、テスト通過率の改善とともに Rust Week 発表 を控えている
- Rust core テストの通過率は2か月前の 92% から 95.9% まで上昇し、残る約65件のテストも原因が互いに似ているように見える状態だ
- 最近の修正は 128ビット整数イントリンシック、checked arithmetic、subslicing に集中しており、.NET 側でも Rust core テストの 96.3% を実行できるようになった
- より多くの C コンパイラを対象にするため、C99 または ANSI C に近い出力と標準 POSIX API の使用を目標としており、一部の ANSI C コンパイラでは非常に単純な Rust プログラムの実行に成功している
- 大規模な C コード生成でファイルサイズを減らす最適化と内部 IR のリファクタリングも進行しており、古いまたは特殊なプラットフォームへ Rust の実行範囲を広げる基盤になっている
Rust Week 発表とプロジェクトの進捗状況
rustc_codegen_clr プロジェクトの発表がオランダの Utrecht で開催される Rust Week に予定されている
- 発表は初心者にも入りやすくしつつ、Rust-to-C コンパイルという高度なテーマも扱うバランスを目指している
- この数か月のあいだに、テスト修正、C コンパイラ互換性、性能改善、内部リファクタリングが並行して進められた
Rust core テスト通過率の上昇
- Rust core テストの通過率は2か月前の 92% から 95.9% に増加した
- まだ約 65件のテスト が残っているが、原因が互いに似ているように見えるため、残る修正の性質は比較的絞り込まれている
- .NET 側も同じ修正の影響を受け、Rust core テストの 96.3% を実行できるようになった
128ビット整数と checked arithmetic の修正
- 改善のかなりの部分は 128ビット イントリンシック、checked arithmetic、subslicing の修正から来ている
- C の
popcount イントリンシックには __builtin_popcount、__builtin_popcountl、__builtin_popcountll の3種類がある
__builtin_popcountll は名前に反して __int128_t ではなく unsigned long long に対して動作する
- x86_64 Linux と GCC では
unsigned long と unsigned long long はどちらも64ビットである
- 既存実装では bit counting イントリンシックで 128ビット整数 を暗黙に64ビットへ切り詰めて計算してしまい、誤った結果を出していた
- 128ビット
popcount は下位64ビットと上位64ビットの set bit 数をそれぞれ数えて足し合わせる方法でエミュレーションしている
- 128ビット乗算の overflow check については効率的な方法が見つからず、単純な方法で処理している
b が 0 ではなく (a * b) / b == a なら overflow はないと判定する
- 画期的な方法ではないが、いくつかのテストを追加で通すには十分だった
Subslicing バグと fallback イントリンシック
- subslicing のバグは
sizeof の欠落により、slice のデータポインタを 要素単位 ではなくバイト単位で offset していた問題だった
- このバグは slice の先頭ではなく末尾から subslicing したときだけ壊れており、主に pattern matching で使われる形だったため長く見つからなかった
- byte slice と string slice では byte と UTF-8 code unit のサイズが1バイトなので個別テストは通っていたが、Rust compiler のテストスイート全体で露呈した
- 一部のイントリンシックは直接実装せず、Rust compiler の fallback 実装 を利用できる
carrying_mul_add は入力の2倍の大きさを持つ整数で乗算を行う必要がある
- 64ビットまでは処理できるが、128ビット入力には256ビット整数が必要になる
- LLVM は256ビット整数をサポートしているが、C と .NET はサポートしていない
- Rust compiler の fallback 実装は128ビット整数で256ビットの乗算と加算を行うため、64ビット整数だけで128ビット演算をエミュレートする際の参考になる
より多くの C コンパイラのサポート
- プロジェクトはより多くの 特殊な C コンパイラ で Rust コードが動作する可能性を高めようとしている
- Rust コードが Game Boy で実行されたり、move 命令だけでコンパイルされたり、Temple OS の Holly C で動作したりする事例があった
- 入手できない proprietary C compiler を直接サポートすることはできないが、obscure C compiler の対応範囲を広げれば、そのような環境で Rust コードが動く可能性は高まる
- 多くのプラットフォームは文書不足やアクセス性の低さのためにサポートされていないことがある
- Git の一部を Rust で書く議論に関連して、NonStop のような proprietary プラットフォームは Rust、LLVM、GCC をサポートしておらず、Git サポートの低下や終了につながる恐れがある
- Rust を C にコンパイルすれば、理論上は C がある場所ならどこでも Rust を実行できる
- すべてのプラットフォームで問題を回避できるかは、まだ確実ではない
- 対象プラットフォーム向けコンパイラを合法的に入手するにはサーバー購入が必要で、予算を大きく超えていた
- そのようなプラットフォームで近いうちに Rust が動く可能性は低そうだ
現在の C 出力戦略
- 現在の計画は、できる限り 標準準拠の C99 または ANSI C に近いコードを生成することだ
- thread-local 初期化のために一部の threading support が必要なため、標準 POSIX API のみを使う方向としている
- 特定のイントリンシック向け fallback 実装を独自に持っており、その一覧を段階的に増やしている
- 一部の ANSI C コンパイラでは非常に単純な Rust プログラムの実行に成功している
- 現時点では対応が難しいプラットフォームでも、必要になったとき比較的容易に追加できる状態を目指している
小さな性能改善とファイルサイズ削減
- 整数 literal の出力では、2^32 未満の整数は hexadecimal より decimal のほうが同じか短いことを反映した
255 は 0xFF より1バイト短い
65536 も 0xFFFF より短い
0x prefix があるため、2^32 までは hexadecimal 表記のほうが小さくならない
- Rust compiler 全体を C に変換する極端なケースでは 最大 1GB の C source file を生成したことがあり、わずかな割合のファイルサイズ削減でも影響がある
- debug info 挿入に使う
#line directive もより賢くなった
- source file name は変わるときだけ含める
- debug info 使用時のファイルサイズを大きく減らせる
内部リファクタリングと IR 整理
rustc_codegen_clr 内部の一部機能を別 crate に分離し、incremental build の速度を高めた
- よりメモリ効率の高い interned IR への移行も進行中だ
- 既存 IR には C にうまく対応しない奇妙な rvalue/lvalue があり、dynamically sized type のような Rust 機能で問題が大きくなっていた
- 例として
MyStr のような custom dynamically sized type では、&self.s は単純に見えても実際には fat pointer metadata を扱う必要がある
- C99 の compound literal を使えば
struct FatPtr_str を1行で作れる
- ANSI C では一時変数に
data と meta をそれぞれ入れて返す必要がある
- Rust と MIR の1行が複数行の C に展開されるため、old IR では temporary local と sub-statement を持つ内部スコープ方式で処理していた
- 新方式は setup 段階がより複雑だが、IR 全体をより単純にし、最後に残った厄介なケースを解決できれば old IR の該当機能を削除できる
次の作業
- プロジェクト作業期間は約 1.5年 になり、バグが減るほど残ったバグを見つけるのにより多くの時間が必要になっている
- 「Rust panics under the hood」第2弾を執筆中で、Rust の panic 過程を段階的に解きほぐす予定だ
- panic message 生成の説明だけですでに10分分あり、記事を2本に分けることも検討している
- Rust 向けの小さく正確な メモリプロファイラ も開発中だ
- コード規模は約 2K LOC
- 日程は厳しいが、数週間以内に関連する記事を書きたいとしている
1件のコメント
Hacker News の意見
もともと rustc_codegen_gcc プロジェクトがこれを約束していたが、結局果たせなかった
[0] https://github.com/rust-lang/compiler-team/issues/458
GCC/Clang 側は「80%」にすぎない
| .NET Core tests | 1764 | 48 | 20 | 96.29% |
| C Core tests | 1712 | 71 | 8 | 95.59% |
このハックは Rust コンパイラのバックエンドだ。バックエンドはプラットフォーム固有の命令を入力として受け取るため、生成された単純ではない C コードは移植可能ではないはずだ
ユーザーは事前生成されたプラットフォーム別ソースを受け取るか、Rust コンパイラとこのバックエンドをインストールして自分で生成する必要がある
extern "c"関数ではすでにできないことで、これが新たにもたらす利点は何?「私の .NET IR 表現は C にうまくマッピングでき、そのおかげで2〜3千行のコードで Rust を C にコンパイルするサポートを追加できた。コードベースのほぼ全体を再利用しており、C と .NET 専用のコードはコンパイルの最後の段階にだけ存在する」
最初は .NET バックエンドとして始まったが、そのアプローチが C コード生成も容易にサポートできると分かって追加したものだ。rustc が渡してくるものを独自の**中間表現(IR)**に変換して処理する
境界チェックのような動的保証も C ランタイムで問題なく実装できる
unsafeブロック一つに入れる必要があるだろうC は Rust でコンパイルできるようにするために明示的に知る必要がある情報をすべて渡してはくれない
https://www.youtube.com/watch?v=1VgptLwP588
unsafeブロックを多用し、たいてい結果のコードが Rust らしくないことだたとえば、グローバル変数ベースの状態機械を、名前付き enum のようなものを使ったより Rust らしい状態機械に変えるのは非常に難しいだろう
十分に強力な AI の助けがあれば可能かもしれないが、AI はまだ意図したことを実際にやり遂げるにはかなり不足していて、まだ準備ができているとは言い難い。C と Rust のコードベース全体をコンテキストウィンドウに入れられるだけのメモリも必要になるはずで、コードが一定規模を超えると、すぐに非常に高価なハードウェアが必要になるだろう。そうでなければ、多くのコード支援 LLM のように、互いに互換性のないコードを独立に生成してしまう
それでも C プロジェクトを Rust で拡張したり、少しずつ書き直したりしたいなら、https://c2rust.com/ はすぐに使える
Rust はパッケージ管理、簡素化された統合ビルド/テストツール、はるかに少ないレガシーな負担、人々が実際に好む高水準機能と構文を備えた現代的な言語だ
C もクリーンだが、複雑なコードベースは C の速度を保ちながら堅牢な抽象化を作れる現代的な言語の恩恵を受ける。もちろん借用チェッカーとメモリ安全性も欠かせない
十分に標準的な C コードを生成する Rust→C トランスレータがあれば、そのギャップを埋められる