Python から Rust を呼ぶライブラリを比べる
tl;dr
Rust で実装されたライブラリに対して Python 側から関数呼び出しをしたいと考えたとき、Rust に対して Python binding を提供してくれるライブラリとして maturin、setuptools-rust、milksnake があり、それぞれのツールの間で微妙に特徴が違うので、その試行錯誤の過程を紹介する。
基本的な流れ
- ユーザー自身の環境で、dylib を生成するオプションで rust をコンパイルし、 共有ライブラリを作る。
- python 側から、 FFI を介して共有ライブラリに対して関数呼び出しを行う。
- 上記を、pip install を叩いたときにワンストップでインストールできるような状態にしておく。
上記のライブラリでは、Python の構造体やクラスなどを、Rust から透過的に扱うことのできるようなインターフェースを提供している。Python のある一部分のコードを高速化するために Rust を呼び出すというケースを想定していると思われるが、今回はより一般的に、Rust で実装したライブラリを pub extern "C"
したコードを、 Python 側から呼び出すということを想定している。上記のコードを1つ1つ実装してもそれほど手間ではないかもしれないが、先人が踏んだ落とし穴をできるだけすり抜けたいと思うと、ライブラリを利用するのが最もシンプルなアイディアだろう。
結論から述べておくと、maturin と setuptools-rs は、python3 の環境があることが前提となっている。
maturin
いわゆるzero-conf 系のツールであり、必要最小限の定義だけすると、あとはよしなに調節してビルドしてくれるというツールである。python 3.5+対応。上記のスクリプトが、 wheel を自動で生成してくれるので、これを使ってみることにする。
$ pip3 install maturin $ maturin build
これをmacos で実行すると、 target/wheels/<script 名>-0.1.0-py2.py3-none-macosx_10_8_x86_64.whl
が作られる。ただし、 wheel のビルドは環境依存なので、単純にそのwheel をlinux 環境に持って行っても動かない。そこで、一つの方法としては、様々な環境で動作する wheel をそれぞれ作るという方針が考えられる。
$ docker pull konstin2/maturin:master $ docker run --rm -v $(pwd):/io konstin2/maturin build
上記のコマンドを叩けば、一応あらゆる環境で動作可能なバイナリが作れるらしい。
そうでない場合は、ユーザーの手元でビルドしてもらうようにする必要がある。つまりwheel を pre-built しておくのではなく、相手の環境でビルドできるような状態でソースコードを頒布したいと考えた時、 pyproject.toml
に記述するという方法がある。そうすると、 pip install .
コマンドで、python library としてインストールすることができるようになる。なお、伝統的にpythonにおいてライブラリの設定は setup.py
が利用されており、pipのバージョンがあまりにも古い場合は pyproject.toml
が動作しないが、ユーザがpip コマンドは最新のものを入れているだろうという想定で、これでいくことにする。
[build-system] requires = ["maturin"] build-backend = "maturin" [tool.maturin] bindings = "cffi" manylinux = "off"
公式サイトによれば、bindings は、 cffi のほか、pyo3, rust-cpythonなどを利用できるとなっている。 manylinux を off にしないと、以下のようなエラーメッセージがでて終了することがある。
💥 maturin failed Caused by: Failed to ensure manylinux compliance Caused by: Your library is not manylinux compliant because it links the following forbidden libraries: ["libz.so.1"]
maturin は、外部ライブラリにリンクしている状況を検知して、 multilinux ビルドできないことを検知しているようだ。各ユーザーがpip install で入れる場合には、multilinux 対応する必要が無いので、その機能はoffにすると、とりあえずビルドできるようになった。これで、ユーザーのlinux/mac/windows 環境でもビルドしてそれぞれでインストールしてもらえるようになった。
ところで、もう少し凝ったことをする必要がある場合は、 setup.py
に相当するものを自分で書けた方が良い*1。そこで、次の2つのツールも試す。
setuptools-rust
これはSetuptools pluginである。 setuptools-rust/html-py-ever at master · PyO3/setuptools-rust · GitHub の例にあるように、setup.py, MANIFEST.in, pyproject.toml, build-wheels.sh を記述する必要がある。build-wheels.sh の中に、cp27 という文字列があるので、一見すると python2 の環境でも動きそうだが、実際にはpython2 しかない環境でビルドしようとする場合、以下のエラーが出た。
Running `rustc --crate-name build_script_build --edition=2018 ~/.cargo/registry/src/github.com-1ecc6299db9ec823/pyo3-0.8.5/build.rs --error-format=json --json=diagnostic-rendered-ansi --crate-type bin --emit=dep-info,link -C opt-level=3 --cfg 'feature="default"' --cfg 'feature="extension-module"' --cfg 'feature="python3"' -C metadata=c13070d14f8e5f9c -C extra-filename=-c13070d14f8e5f9c --out-dir /tmp/pip-req-build-sf23o2hn/target/release/build/pyo3-c13070d14f8e5f9c -L dependency=/tmp/pip-req-build-sf23o2hn/target/release/deps --extern lazy_static=/tmp/pip-req-build-sf23o2hn/target/release/deps/liblazy_static-1cc38642f95bd1c1.rlib --extern regex=/tmp/pip-req-build-sf23o2hn/target/release/deps/libregex-e4869bcab7e72b4a.rlib --extern serde=/tmp/pip-req-build-sf23o2hn/target/release/deps/libserde-198eb2bbcb756b12.rlib --extern serde_json=/tmp/pip-req-build-sf23o2hn/target/release/deps/libserde_json-7bf7d5b71c87fe3c.rlib --extern version_check=/tmp/pip-req-build-sf23o2hn/target/release/deps/libversion_check-65476cb62a0489dd.rlib --cap-lints allow` Running `/tmp/pip-req-build-sf23o2hn/target/release/build/pyo3-c13070d14f8e5f9c/build-script-build` error: failed to run custom build command for `pyo3 v0.8.5` Caused by: process didn't exit successfully: `/tmp/pip-req-build-sf23o2hn/target/release/build/pyo3-c13070d14f8e5f9c/build-script-build` (exit code: 101) --- stderr thread 'main' panicked at 'Python 3.x interpreter not found', ~/.cargo/registry/src/github.com-1ecc6299db9ec823/pyo3-0.8.5/build.rs:28:9 note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
python3 interpreter がないといってエラーが出ている。setuptools-rust は pyo3 というライブラリに依存していて、 pyo3 はpython3 のinterpreter を要求しているようにみえる。上記で見てきたように、pyo3 organization 以下のライブラリはどれもpython3 しか対応していない。いかに python2 がoutdated であるとはいえ、少しの改変でpython2 も対応しているほうが望ましいという立場から、silksnake も試してみることにする。
もし、各環境に応じたwheel を作れば良いという立場であれば、 docker image を使ってビルドすれば、特に問題は無い。
milksnake
python2, python3 のどちらの環境でもビルドを通るようにしたい場合は、このライブラリを使うのが一番簡単である。といってもやっていることはそれほど複雑ではなく、 setup.py
にフックして cargo build --release
を実行するというのがポイントである。こちらも、setuptools-rust と同様に記述しなければならないスクリプトは多いが、特に落とし穴は少ない。
python からは、以下のようにコードを呼び出すことができる。
from example import lib print(lib) print(lib.hello_rust()) print(lib.test())
ビルド時に cbindgen
というライブラリがC のヘッダファイルを出力し、そのヘッダファイルをもとに、cffi を介してビルドされた共有ライブラリにアクセスして、関数呼び出しを行うという仕組みになっていると考えられる。milksnake は最近活発に更新されていないというのが心配だが、その点を除いては問題無く動作した。
他の言語へのバインディング
ここはメモ程度に、検索した結果を貼っておく。
neon: JavaScript でつなげる場合。ただしこのライブラリでは、 JxResult という JavaScript に返す型を定義しなければならないので、Rust にも手を加える必要があってつらいようにみえる。
https://github.com/neon-bindings/neon
https://github.com/neon-bindings/examples
fn make_an_array(mut cx: FunctionContext) -> JsResult<JsArray> { // Create some values: let n = cx.number(9000); let s = cx.string("hello"); let b = cx.boolean(true); // Create a new array: let array: Handle<JsArray> = cx.empty_array(); // Push the values into the array: array.set(&mut cx, 0, n)?; array.set(&mut cx, 1, s)?; array.set(&mut cx, 2, b)?; // Return the array: Ok(array) } register_module!(mut cx, { cx.export_function("makeAnArray", make_an_array) })
*1:例えば、cargo が入っていない環境ではrustupで自動的にrust をインストールしてほしいという場合など