見出し画像

Rustコンパイラの全体感を掴むための「Rust入門」

はじめに

こんにちは、ワンキャリアでエンジニアリングマネージャーをしている江副です。

最近ちょくちょくRustにコントリビュートしているのですが、Rustの素晴らしさに気付きつつある今日この頃です。
というのは、言語仕様そのものも素敵なのですが、それを取り巻くRustコミュニティに活気があり、ドキュメントが充実しており…開発者/ユーザーの裾野を広げる心意気を感じます。

一方で、比較的チャレンジングな言語仕様ゆえ初心者には取っ付きづらく、ましてやコントリビュートとなると尻込みしてしまう方も居るかもしれません。

そんな方の一助になれば。
ということで、今回はRustにコントリビュートする上で何となく知っておくと良いRustコンパイラの概観について、簡単にお話しできればと思います。

Rustにコントリビュートするには

RustコミュニティはPRを出すこと以外にも様々なコントリビューションを歓迎しています(例えばドキュメンテーションやIssueのトリアージなど)。
もしPRを出す際には、一般的なOSSへのコントリビュートするときと同様に、以下の流れとなります。

  1.  オリジナルのリポジトリをフォーク

  2. 作業用ブランチを切る

  3. 作業が終わったら、PRを出す

実際にどうやってコントリビュートしていけば良いかについては、Rustコミュニティメンバーの方が書かれたこちらの記事が詳しいのでご参照ください。

余談ですが、ワンキャリアでは組織的にOSSコントリビュートを行っています。ご興味ある方はぜひこちらの記事もご一読ください。

Rustコンパイラの概観

前提として、RustはLLVMに立脚した言語です。
LLVMとは何かについて、ここでは詳しくは触れませんが、
プログラミング言語を作成する基盤だと理解するのがわかりやすいと思います。

LLVMを利用すると、様々なアーキテクチャに対応したマシンコードの生成をLLVMに対して委託できるので楽だというメリットがあります。

そんなLLVMを使ったRustのコンパイラは、以下のような流れでRustのソースコードをコンパイルします。

パイプラインとしては、
マシンコード生成を目指して、ソースコードを解析しながら中間表現(IR)への変換を繰り返していっているのだと読み取れるかと思います。

ご覧の通り、RustのコンパイラとしてはLLVM IRまで作ってしまえば、あとはLLVMが最適化の上、対象のアーキテクチャに応じたマシンコードを生成してくれます。

ちなみにマシンコード生成の過程は、一般的に以下のような流れです。

  1. 字句解析

  2. 構文解析

  3. AST(抽象構文木)生成

  4. 中間表現生成

  5. 最適化

  6. アセンブリ生成

  7. リンク

  8. マシンコード生成

(至極当たり前のことではあるのですが)Rustのコンパイラも概ねこの流れに則っていることが分かると思います。
なお、コンパイラにおいては1~4の処理がフロントエンド、5以降がバックエンドと呼ばれることが多いです。

ここからはRustのコンパイラの各ステップの処理を簡単に見ていきましょう。

Rustソースコード -> HIR

Rustのソースコードがコンパイラに渡されると、
まずはrustc_lexerによって、トークンストリームに変換されます。
これがいわゆる字句解析ですね。

ただ、このままでは続く構文解析の準備ができていないので、rustc_parse中のレキサによってさらに変換されたのち、パーサによって構文解析がなされます。

構文解析の結果、ASTが出来上がり、これをHIR(High-Level Intermediate Representaion)に変換してこのパートは終了です。

なお、ASTからHIRに変換される過程で、その後の型チェックなどに不要な構造の多くは削除されます(例えば括弧など)。
ちなみに実際のHIRを見たい時は、コンパイラに対して以下のフラグを渡してあげましょう。

-Z unpretty=hir-tree

HIR -> MIR

MIR(Mid-Level Intermediate Representation)に変換する過程で、型推論・型チェック・トレイト解決(Trait Solving)などを行います。
なお、トレイト解決について軽く説明しておくと、これはトレイトとその実装(impl)をペアリングすることです。(ちなみに現行のトレイト解決をリプレイスするPoCが進行中のようです。)
実はHIR -> MIRの変換の過程で、THIR(Typed High-level IR)を挟むのですが、ここでは説明を省きます。気になる方はこちらへ。

MIR -> LLVM IR

この辺からコンパイラとしてのバックエンドと呼ばれる領域に入っていきます。Rustのコンパイラにおいては、MIRの段階で借用チェックや最適化が行われます。

ここで実施される最適化としては、例えばジェネリクスで表現されたコードのMonomorphization(単形化)があります。
Monomorphizationとは聞き慣れない方もいらっしゃるかもしれませんが、これはジェネリクスを使用している箇所に対して具体的な型を埋めていくことを指します。これは実際のところ、同じコードをコピペして回ることに近いのでバイナリサイズを大きくしうるのですが、結果具体的な型を得られる以上、より最適化しやすいというメリットがあります。

なお、MIRの構造はCFGとなっており、このデータ構造は最適化や静的解析などによく使われるデータ構造として知られています。
実際のMIRを見たい場合は、HIRの時と同様、コンパイラに対して以下のフラグを渡してあげましょう。

-Z unpretty=mir

このMIRをLLVM IRに変換し、LLVMコンパイラに投げ込むことでRustのコンパイラとしては一息つくことになります。(その後はLLVMコンパイラがアセンブリやマシンコードの生成をやってくれます。)
LLVM IRはコンパイラに対して、以下のようなフラグを渡すと見られます。

--emit=llvm-ir

実際に見ていただくとわかりますが、型が付いているアセンブリのような印象を受けると思います。

ワンキャリアにおけるRust

いかがでしたでしょうか。Rustにコントリビュートする上での前提知識として、少しでも参考になれば幸いです。

なお、ワンキャリアでは2022年10月より一部プロダクトにおいて、試験的にRustの導入を始めました。現時点ではPoCの側面が強いですが、個人的にはやはりメモリセーフであり、GC(ガベージコレクション)がない言語仕様が、本格採用に向けた強い理由になりうると考えています。
今後のサービス開発に向けて本格採用を検討しているところですので、ご興味ある方がいればぜひご連絡ください!

ざっくばらんにお話ししましょう!


この記事が気に入ったらサポートをしてみませんか?