RustOwlのテクノロジー

2025-12-02 00:28

tags: #rust

こんにちは。

本記事は私のインターン先であるLabBaseというところのアドベントカレンダー2日目の記事です。 LabBaseは開発にRustを使っている奇特な企業なのですが、まぁ私が提供できるRustの話題といえばこれかな、ということで。

RustOwlが話題になってから、かれこれ一年近くが経とうとしています。 まだ賞味期限内かな?そうであって欲しいですが。

RustOwlとは、Rust言語を対象にした、エディタ上での所有権とライフタイムの可視化に対する模索を行ったプロジェクトであり、またその成果物のツールの名称です。 そもそもこのプロジェクトは私の研究の一環であり、元々はRustVizAquascopeの影響を受けたものでした。 これらの先行研究は私がRustの研究をしようと考えたきっかけであり、そういう意味では本当に私のキャリアに大きな影響を与えた研究たちです。

さて、研究として取り組んでいるということは当然論文を出すという話になるわけですが、今年一年で出したRustOwlに関する論文はすべてRejectでした。 私の力不足ですね。 そもそもが3週間ででっちあげられる(?)ということで始めたものなので、先生も自分もあんまり期待していないところはありましたが……

それはいいとして、論文というものはエンジニアリングについて発表するものでもなかったりします。 もちろんその論文が掲載されるところにもよるのですが、基本的には論文に書かれる部分はその解析アルゴリズムや妥当性の評価、各種調査結果などとなってきますし、私もそういうことがメインで論文を書いています。 そのため、RustOwlのすべてを書くということは当然できないわけです。 RustOwlにはさまざまな側面が生まれましたから、論文でそれがすべて語られることはありません。

前置きが長くなりましたが、今回はRustOwlの論文にならない部分、すなわち「RustOwlのテクノロジー」の部分を書いていきます。

本記事を読み、ぜひ開発に参加してみたい!と思っていただける方が一人でも増えたら嬉しく思います。

Rustコンパイラに挑む

RustOwlは所有権やライフタイムの情報を表示しますが、これらの情報はどこで取得できるのでしょうか? そもそもどこにそんな情報があるのでしょうか?

答えはMid-Level IR(MIR)という中間表現です。

中間表現とは、プログラミング言語のコンパイル過程で、ソースとなる言語とコンパイル対象となる言語の間に位置するなんらかの表現、です。 曖昧な書き方で申し訳ないですが、多くの人に伝わりやすいところでは、LLVM IRなどはご存知の方も多いのではないでしょうか。 LLVM IRはLLVMの多様なコンパイル対象、そのバックエンドと、ソースとなる言語を処理するフロントエンドとの架け橋になる表現・言語です。 この共通言語を経由してコンパイルすることで、共通の最適化が利用できるといったさまざまな恩恵を受けることが可能です。 現代のコンパイラでは、こうした中間の表現・言語を用いて、対象の言語にコンパイルすることが多いです。

さて、ではMIRというのは何かというと、RustとLLVM IRの間に位置する中間表現です。

詳しいことを言うと、RustとMIRの間にもいくつかの中間表現があるのですが、今回はMIRに的を絞って話します。

長い前置きになりましたが、なぜMIRが重要なのかというと、MIR中には所有権の操作が明確に現れており、かつ所有権とライフタイムの検査はMIRに対して行われるからです。

所有権とライフタイムの検査は(今後は借用検査と呼びますが)、Non-Lexical Lifetime(NLL)実装以降、このMIRに対して行われています。 MIRはControl Flow Graph(CFG)の形になっており、MIRを解析することで、プログラムが実行される経路がわかります。 借用検査はこのMIR上でどのような経路でプログラムが実行され、どこからどこまでの範囲で借用が用いられているのか、を解析することにより、無駄なスコープの導入などを考えずに、借用を作り破棄するというサイクルが簡単に行えるようになりました。

つまり、RustOwlで所有権とライフタイムを扱うためには、MIRを見ることが必須となってきます。

では、RustのMIRへのコンパイラを一から作るのか?答えは当然NOです。

独自のコンパイラ作成は大変ですし、Rustコンパイラとの互換を保証するのが難しいです。 そのため、できるだけ既存のRustコンパイラの仕組みに乗っかりたいのです。

rustc_private にお任せあれ!

ここで登場するのが、以前から私が何度か言及している rustc_private というRustコンパイラの機能、あるいはAPIになります。

広く使われているRust公式のRustコンパイラである rustc は、コンパイルを担う中心的な存在である rustc_driver と、それをラップするコマンドである rustc に分けられます。 rustc_driver は動的リンクライブラリであり、 rustc はこれを動的リンクして、プログラムのコンパイルをそちらに任せます。

rustc_private を用いることで、この rustc_driver のAPIを利用する独自のラッパーを作成することができるようになります。 すなわち、 rustc 内部の関数を利用する、独自のRustコンパイラが書けるわけです。

実は、みなさんお馴染みのClippyなどもこの仕組みを用いて作られています。 Rustコンパイラの複雑な解析を再実装することなく、再利用して独自のプログラムを構築できる仕組みです。

RustOwlが知りたいのは、MIRとその解析結果でした。 そのため、RustOwlはこの rustc_private を用いて、借用検査に独自の解析処理を差し込むことにより、独自の解析を簡単に行うことを実現しています。

mir_borrowck とPolonius

rustc はクエリと呼ばれるシステムを採用しており、コンパイラのコンテキスト中にある関数への参照を差し替えることで、コンパイル中のその関数の呼び出しを自分が呼び出したい関数に差し替えることができます。 rustc で借用検査を行なっている関数は mir_borrowck という関数なので、これを独自の関数に差し替えることで借用検査の情報を取得しています。

そしてこの mir_borrowck の中で使う、借用検査を行なってくれるAPIこそがPoloniusになります。

PoloniusのAPIを利用することで、借用検査を行い、その結果得られたライフタイムに関する制約を得ることができます。 この辺は詳しくは論文で説明できるようにしたい……

MIRとデバッグ情報

ではこれらの制約がわかったところで、どのようにしてコードの位置とこれらの情報を紐づけているのでしょうか。

実は、MIRはその中の様々な要素が、デバッグ情報としてソースコード上の対応する位置を保持しています。 実際のRustコンパイラが借用検査の結果を位置で表示できるのも、この情報のおかげなのですね。

Cargoで広がる世界

さて、Rustコンパイラを用いることで、Rustフルセットの解析に対応するところまでわかりました。 しかし現実には、依存関係のあるRustプログラムをCargoを用いてビルドすることがほとんどです。 これをどう扱っているのでしょうか?

なんとこれもありがたいことに、CargoコマンドはRustのコンパイル実行時に実行するRustコンパイラのパスを環境変数で設定可能にしてくれています。 すなわち、Cargoに対して、RustOwlのコンパイラ部分(これは rustowlc として実装されています)を「Rustコンパイラですよ」と教えることで、Cargoのコンパイル過程でRustOwlを用いることが可能になります。

具体的には RUSTC 環境変数や、 RUSTC_WORKSPACE_WRAPPER といった環境変数を操作することで、Cargoの実行する rustc コマンドを rustowlc に差し替えています。

管理職、LSPサーバー。

ここまではRustコンパイラのお話でした。 ここからはRustコンパイラから得た情報をLSPサーバで使う話をします。

解析結果を取得する

Cargoを使って rustowlc を実行するということを前の節で説明しましたが、そこで得た情報はどのようにしてLSPサーバに渡っているのでしょうか。

これは至ってシンプルで、 rustowlc はその解析結果をすべて標準出力にJSON形式で出力します。 LSPサーバはこのJSONを読み取り、各所有権とライフタイムの位置情報をメモリに保持します。

カスタムメソッド

LSPに乗っかると何かと便利ですから、実装はLSPになっています。 一方で、所有権やライフタイムを可視化するメソッドなんて、LSPで定義されているわけがありません。

しかし、LSPではカスタムメソッドを定義して利用することができます。

エディタから可視化をリクエストするときに開いているソースコードとカーソルの位置情報とともにこのカスタムメソッドを呼び出します。 LSPサーバはメモリに保持した所有権とライフタイムの位置情報から、カーソル位置に対して適切なものを選択し、エディタ側に返します。

実際はここでも、下線の重複処理といった様々な処理を行っていますが、詳しくはソースコードを見てもらいたいと思います。 まぁ、自分でも読むのが辛いくらいの複雑さではありますが……

エディタで可視化

最後に、エディタ側、具体的にはVS Code extensionやNeovim Pluginなどで、LSPサーバから受け取った情報を元に、ソースコード上に下線を引く処理をしています。 この処理はどうしてもエディタ側に依存してしまうため共通化が難しく、できるだけLSPサーバで情報を処理してから、エディタ側にフィードバックする仕組みにしています。

おわりに

ここまでお読みいただきありがとうございました。

RustOwlの技術的な側面は、語り始めたらキリがありません。 もしご興味を持っていただけたら、ぜひソースコードを読んで、なんでも質問してくれると嬉しいです。

RustOwlは私を含めメンテナの多くは忙しく、ここ最近は開発も停滞気味です。 ここまで読んでくれたあなたには、ぜひ開発やメンテナンスに参加してもらいたい……!