はじめに
最近では、新しいライブラリがRustで書かれるようになってきました。これに伴い、RubyやCrystalなどの好みの言語からRustの関数を呼び出したくなることが増えます。
Python言語に対しては、バインディングが最初から提供されることも多いのですが、それ以外の言語からRustの関数を呼び出したい場合、自身で作成する必要があります。
この記事は、Rustに全く触れたことがない人間が、数時間調査した内容をまとめたものです。
しかし、QiitaでMagnusをググっても記事がほとんどヒットしないことからわかるようにそれほど日本語の情報が多くない分野なので、調べ物の取っ掛かりとしては価値があると思います。
目標
Rustで作成したライブラリの構造体と関数をみて、RubyやCrystalからFFIで呼び出せるようになること
Rustで共有ライブラリを作成する方法
Rustは共有ライブラリを書き出すことができます。
Tomlファイルに [lib] crate-type = ["dylib"] を設定します。
[lib]
name = "myrustlib"
crate-type = ["dylib"]
呼び出したい関数に #[no_mangle] という属性を追加し、関数でも pub extern "C" を指定します。
#[no_mangle]
pub extern "C" fn string() -> *const u8 {
"Hello World\0".as_bytes().as_ptr()
}
構造体は、#[repl(c)]という属性を追加します。
#[repr(C)]
struct Foo {
n: u32
}
C言語と互換性のある関数・構造体しか書き出せないことに注意してください。つまり、C言語と互換性のある関数や構造体を自分で追加してC言語のAPIを自分で作成する必要があります。
これでは不便なので、後述のMagnusのように自動で、Rustの(struct + trait)をRubyのクラスに変換する仕組みがあります。
RubyにおけるRustのエコシステム
これまで多数のライブラリが作成されてきましたが、現状では rb-sys と、rb-sys 上に構築された Magnus が有力です。Bundlerのgemテンプレートでも Magnus が使用されているため、今後も継続的なメンテナンスが期待できるでしょう。
bundle gem --ext=rust hoge
- rb-sys は低レイヤーを担当し、RubyのC APIを呼び出したり、Gem内でRustのコードをコンパイルするサポートをしています。今回のメインテーマではありませんが、RustからRubyのC APIを呼び出すことで、RubyのRust拡張を作ることができます。
- Magnusは rb-sys よりも上位のレイヤーを担当し、RustとRubyの型変換を担当します。特に、Rustのクラスに相当する部分(struct + trait)をRubyのクラスに自動的に変換する機能があります。以下の記事では、Magnusの使い方を簡単に紹介されています。
Magnusを利用して作成された実用的で大規模なRubyのGemの一例として、polars-rubyがあります。これはRustで実装されたデータフレームのPolarsをRubyから利用できるようにするもので、現状ではRubyから利用できる最速のデータフレームです。このようなしっかりとした利用事例があるのもポイントです。
CrystalにおけるRustのエコシステム
- Rustの関数をCrystalから呼び出したい需要は非常に強いですが、ディレクトリ構造、コンパイル方法、型変換のどの部分においてもデフォルトのものは存在しません。このため、自分でディレクトリ構造を考え、Makefileを作成し、型変換のコードを追加する必要があります。
- 他の言語からCrystalを呼ぶことは推奨されていません。その理由は以下のとおりです。
- Crystalは静的な言語ですが、ダックタイピングが可能で、その結果、ライブラリを含む全てのコードを常にコンパイルし、必要な型を割り出します。しかし、コンパイル毎に型と型ID(数値)の対応が変わるため、共有ライブラリとしてコードを再利用する場合、動作の保証がないと公式に明言されています。そのため、RubyやRustなど、他の言語からCrystalを呼び出す試みはあまり行われていません。
- 今のところ実行ファイルの中で「単一のCrystalの共有ライブラリとしかリンクしてない」という条件下においては、Crystalで書かれた共有ライブラリを使っても問題なく実行できると言われています。これは、型の不整合が起きる可能性がないからです。
感想・ポエムなど
Rustで書かれるライブラリは今後も増えると予想されます。したがって、Rubyユーザーとしては、Rustの関数を安全に呼び出す方法を確保したいと思います。
一方で、RustのライブラリはいつでもC言語との互換性があるわけではありません。
- 構造体は、Option型などを使用している場合、Cの構造体との変換が必要です
- 関数も、引数や戻り値にプリミティブな型以外を使用している時は、Cの関数でラップする必要があります
そのため、Rustのライブラリ関数を共有ライブラリに書き出していくには、それらをラップするAPIを整備する必要がありますが、これは手間がかかります。たとえ、C言語のAPIは自分で作成せずにMagnusなどの自動変換に頼る場合でも、Magnusのルールに則ったラッパーを自作する必要があるでしょう。
PythonやRubyなどの主要なオブジェクト指向言語では、Rustの(struct + trait)に属性を追加することで、それをPythonやRubyのクラスに自動変換するような方法が採用されています。
しかし、この方法は言語間での互換性がなく、RustのPythonバインディングがPyO3で提供されていても、それがRubyから呼び出せるとは限りません。これは、RubyやCrystalから見た場合、囲い込みのような状況に見えます。
しかし、RustでOSSのライブラリを公開してくださっている作者にC APIを提供するように求めることは現実的ではないし、道義的にも「どうかなぁ」と思われます。(もちろん本人がよろこんで提供している場合は別です)
そこで、Rustの(struct + trait)から、将来的にはCのAPIより少々高度な、さまざまな言語で共通に利用Attributeのようなものが求められるかも知れないなと思ったりしました。
この記事は以上です。