RustのABIと共有ライブラリ
最近Rust言語の人気が上がってきてると思います。自分も最近Rustを触り始めて、その魅力に取り憑かれてる身のうちの1人です。
そんな中である時、「rustには安定したABIがありません。」というissueを見つけました。(きっかけははっきり覚えてませんが。)
はじめは、「ABIが定まってないって、そもそもなにが問題なんやあ...」って感じだったのですが、ちょっと気になって調べてみると、このissueはRust言語の今後の立ち位置をも左右させうるようなかなりスケールの大きい話だったので、今回はそのお話を自分の理解でつらつらと書いていこうと思います。(認識違いやご意見感想などがあればぜひコメントいただけると励みになります!)
- そもそもABIとは
- RustにABIがないというのはどういうことか
- ABIと共有ライブラリ
- 日々私たちはいかに共有ライブラリに依存してるか
- ABIと共有ライブラリ 2
- どうしてRustのABI安定が図られない?
- まとめと今後の動向
- 参考文献
なおこの記事は CyberAgent 22 新卒 Advent Calendar 5日目の記事として投稿してます。
そもそもABIとは
ABIは「Application Binary Interface」の略で、アプリケーションコードと生成されるマシン語の間のインターフェースを指します。
似た言葉に、API(Application Programming Interface)があります。これはプログラムのソースコードと、ライブラリ間のインターフェースを指す用語として使用されます。
例えばIntをStringに変換するようなライブラリ関数
pub fn int_to_string(n: i32) -> String {...}
みたいなのがあった場合、
- この関数はi32を引数にとる
- 返り値としてStringを返す
と言ったのがAPIの例と言えます。
ABIも同じような感じで、ソースコードがどのようなマシン語になるかを規定したものです。
よく出てくる例として、関数の呼び出し規約(引数はどのレジスタに渡されるか、stackのレイアウトはどのようになるか)や構造体のデータ構造をどのようなマシン語で実現するか、と言った話はABIと関連しています。*1
なので、ABIが安定しているとは、「関数呼び出しや構造体のデータ構造をどのような規則でマシン語にするかといった仕様がきちんと定義されている」と言い換えることができそうです。
ABIが安定している言語の例として、C言語が挙げられます。*2 これは、C言語のコンパイラが(x86_64において)Sytem V ABIと呼ばれる仕様に基づいてコード生成を行うため、安定していると言われているわけです。
RustにABIがないというのはどういうことか
では、RustのABIが定まってないとはどういうことでしょうか? 自分なりに下記のようだと解釈しています。(まあ、そのままですが.)
関数の呼び出し規約や構造体のデータの表現方法などに代表される、マシン語の生成に関する仕様が明確に定まっていない.
例えば、Rustのunsafe code guidelinesには、コンパイル時の状況によって構造体のレイアウトが変化しうるという記述があり、これはABIが定まっていない挙動の1つ例として挙げることができます。*3 これはわざと定めてないという訳ではなく、コンパイラの「構造体のフィールドをいい感じに並び替えて構造体サイズを最小にしようとする」という最適化の挙動から来てるものです。ただ、結果的にABIが定まっていないというデメリットも含んでしまってるわけです。
しかし、よくよく考えてみても、これのどこが問題なのでしょうか? 実際、Rustは現在様々なシステム上で問題なく実行できていますし、自分もABIが定まっていないことによって、日々の趣味開発で不便さを感じたことがありません。
どうしてABIが定まってることが大事なのでしょうか?
ABIと共有ライブラリ
stableなABIが無いと困る場面として、システムの共有ライブラリを作成しにくい、という点が挙げられます。
ここで少し共有ライブラリの話をしてみましょう。
システムにおける共有ライブラリというのは、OSに標準でついてくる「頻繁に行う処理」を集めたライブラリです。*4 例えば「文字を出力する・読み込む」とか「新規プロセスを作成する」とかはシステム上において頻繁に行う操作なので、それらは共有ライブラリとして提供されています。
共有ライブラリにはいくつかメリットがあります。
まずは、実行ファイルの大きさを節約できるという点です。共有ライブラリの関数はリンク時ではなく実行時に解決されます。例えばC言語のおなじみprintf()
は共有ライブラリが提供する関数ですが、printf()
自体のマシン語はコンパイルした実行ファイルに含まれません。代わりに、実行時において共有ライブラリのprintf()
のアドレスが実行ファイルのprintf()
のcall先に埋め込まれます。このように、実行ファイル自体にはprintf()
のマシン語は入らないので、普通の関数をコンパイルするのに比べてバイナリサイズを小さく保てるのです。
さらに、別のメリットとして共有ライブラリの関数は複数のプロセスでも共有できるという点もあります。例えば、マシン内でprintf()
をコールするプロセスが複数あった場合を考えてみます。この場合、各プロセスに対して共有ライブラリprintf()
がそれぞれ個別にメモリ上に展開されるわけではなく、展開されてる共有ライブラリのprintf()
をそれぞれのプロセスが参照するような形をとります。
このように、共有ライブラリは「バイナリサイズを小さくする」、「実行時にメモリを節約する」といったメリットがあり、これは「システム内で頻繁に行う処理」に対して特に有効に働きます。
日々私たちはいかに共有ライブラリに依存してるか
ここで、共有ライブラリの重要性に関して述べたある記事を紹介します。
これは、「もし共有ライブラリなしでLinuxディストリビューションを構成したらどうなってしまうのか?」ということに関して、仮説・検証も交えつつ非常に面白く解説している記事です。(短いし内容もわかりやすいのでぜひ直接読んでみてください)
この記事の要点は下みたいな感じです。
* 静的リンクしか提供していない言語DostでLinuxディストリビューションを構築する という前提をおいた結果、 * /usr/bin 配下のバイナリサイズが概ね4GB程度になる * 静的リンクにより、動的リンクに比べバイナリサイズが20倍大きくなった * 動的リンクなしで、静的リンクだけでOSを構成するのは という検証結果が得られた。
共有ライブラリがない世界、凄まじいですね。。 実行ファイルが依存している処理を全てその実行ファイル自身が保持しているわけですが、それでここまでのサイズになるのは驚きです。 これではPCの容量がいくらあっても足りない気がしますし、ましてや組み込みシステムなど、メモリ資源が潤沢でない環境などにおいては全く機能しなさそうです。
つまりOSのようなシステムプラットフォームを構築するにあたっては、共有ライブラリの概念は必須のものと言えるでしょう。
ABIと共有ライブラリ 2
あるシステムを構成するにあたり、共有ライブラリが重要な役割をはたしていることが分かりました。 共有ライブラリがないと、私たちのシステムは、バイナリサイズがやけに大きい、しかも内部的に重複をたくさん含んだ無駄だらけの実行ファイルたちで埋め尽くされてしまいます。 なので共有ライブラリを導入したいわけです。
では、どういった言語が共有ライブラリを構築するのに向いてるでしょうか?
様々な観点があると思いますが、ABIの安定性はまさにその特徴の1つになるでしょう。
仮にABIが安定していない言語で共有ライブラリを構成した時をイメージしてみます。 ABI仕様がコロコロ変更されるため、その度に共有ライブラリと、その共有ライブラリたちに依存しているバイナリ達(これは実質ほぼ全てのアプリが当てはまるでしょう)を再コンパルする必要があります。これはとんでもなく面倒です。そんな言語で、各種OSベンダ達はシステムを構築しようとは思わないでしょう。
どうしてRustのABI安定が図られない?
Rustに話を戻します。 安定したABIを提供し、システム共有ライブラリを提供できるようになることは、Rustがシステムプログラミング言語となるための大きなステップであることは間違いなさそうです。
では一体、どうしてRustのABI安定が図られないのでしょうか。 筆者が調べた中だと、概ね下記の懸念があるようです。
1つ目はRustのパフォーマンスとのトレードオフになっています。ABIを定義することにより、ある種の最適化が行えなくなるということです。例えば先に述べた構造体のフィールドレイアウトの話も、フィールド順を固定化するようなABIを定めてしまうと、構造体のpaddingを加味して構造体サイズを最小にするみたいな最適化ができなくなってしまいます。
2つ目はRust言語の複雑な言語機能に起因することです。C言語は長年共有ライブラリの父とも言える地位を築き続けてきていますが、それはシンプルな言語使用によってABIが決めやすいという部分も大きく寄与していたのかもしれません。Rustはどうでしょうか。ジェネリクスなどのコンパイル時にコード生成を行う機能はそもそも共有ライブラリと相性が悪いことに加え、所有権、非同期処理、マルチスレッドなど、Cにはない複雑、多様な言語機能を持っており、これらのABI仕様を決定するというのは相当タフなタスクであることは想像できます。*7
このようにABIは単に決めればいいだけという話ではなく、決定に際して様々なデメリットや必要となる作業が生じ、それらを加味した上で最善な判断を下さないといけないわけです。大変そうですね。
まとめと今後の動向
Rustは、C, C++のような「システムプログラミング言語」としての成長を期待されており、近年人気が上がってきている言語です。 コンパイル時の静的検証・最適化に徹底しており、GCは無く、実行時のパフォーマンスは他の言語と比較してもかなり高い水準です。さらになんと言っても、 Cのダングリングポインタのようなつらーいメモリバグは、Rustの型システムの前では姿を消します。 これらの特徴はシステム開発者にとっては非常に喜ばしい機能でしょう。
しかし、繰り返すように、Rustには安定したABIがありません。安定したABIがないと、共有ライブラリの例で挙げたように、Rustをインターフェースにしてある機能を提供するといったことが難しくなります。
そのため現状、Rustの良さを十分に把握し、「Rustを使いたい!」と思ってるシステムベンダーも、ABIが安定してないことに不安を覚えてRust導入に踏み切れないという面があるようです。低レイヤ言語としての地位を期待されてるRustとしては、システムの共有ライブラリを提供できないというのはデメリットが大きいのです。
ただ、良いニュースもあります。 例えば1年前の春ごろにA Stable Modular ABI for Rustというタイトルで、Rust Internal Formalから下記のような投稿があったようです。
これはRustのABIの安定化に向けた議論で、特にモジュラーABIという手法をRustに導入できないか、といった議論がされています。 モジュラーABIというのは、コンパイラの1つのモジュールとしてABIモジュールというものを切り出せるようにし、ABIをユーザーが自由に選択可能となるようにする、といったものだそうです。これが実現できれば、例えばABI互換を持たせたいライブラリにはstable_abiモードでコンパイル、特に気にしない場合は通常通りのコンパイルを行う、といった選択ができるようになると説明されています。
さらにRust ABI wikiなるページもできており、Rustチームの中でもABI安定化に向けた動きは少しずつ進んでいるのかもしれません。
すでにRustは所有権モデルなどの斬新な言語機能で幅広いプログラマーから注目されています。
が、個人的には今後Rust言語がどのように「システムプログラミング言語」としての地位を獲得していくのかを特に注目していきたいです。
さらに、この過程をABIという観点で見ていくと、より面白いかもしれない、とissueを読みながら思ったりしました。
参考文献
A Stable Modular ABI for Rust
https://internals.rust-lang.org/t/a-stable-modular-abi-for-rust/12347
How Swift Achieved Dynamic Linking Where Rust Couldn't
https://gankra.github.io/blah/swift-abi/
Rust ABI wiki
https://slightknack.github.io/rust-abi-wiki/intro/intro.html
Rust does not have a stable ABI
https://people.gnome.org/~federico/blog/rust-stable-abi.html
Layout of structs and tuples
https://rust-lang.github.io/unsafe-code-guidelines/layout/structs-and-tuples.html
Is static linking the solution to all of our problems?
https://nibblestew.blogspot.com/2017/03/is-static-linking-solution-to-all-of.html
When is the ABI stable?
https://internals.rust-lang.org/t/when-is-the-abi-stable/10420/2
ABIバグは「悪夢」
https://www.snsystems.com/ja/technology/tech-blog/2015/06/11/abi-bugs-are-a-nightmare/
*1:例えば関数の呼び出し規約に関してはx86_64アーキテクチャだとSystem V Application Binary Interfaceという仕様に準拠してます。この仕様における関数の内部的な実行の流れを知りたい場合は、例えばここの投稿を参考にしてみてください。(説明がとても丁寧で図もあるので、関数の処理のされ方のイメージを掴みやすいと思います。
*2:Swift言語もSwift5からABI互換を謳っています。
*3:ただし構造体に関しては、#[repr()]といった属性をつけることでこの挙動をオプションにすることができます
*4:Linuxでは.soの拡張子で提供されてるアレです。http://archive.linux.or.jp/JF/JFdocs/Program-Library-HOWTO/shared-libraries.html