maturinで始めるRustのPythonバインディング
はじめに
PydanticがRustで爆速になるぜといって、実際にV2がリリースされました。
本当に17倍速くなったかはあまり気にしていませんが、多分速くなっているんでしょう。私はAPIがきれいになったので実務的にはそちらの方が嬉しいと思っています。
さて、Rust実装はpydantic/pydantic-coreに置かれており、pydanticはこれを使っているようです。
プロジェクトはmaturinで管理されています。
maturinに興味が出てきたので触ってみると意外と簡単に使えたので、そのメモです。
課題設定としてはRustで実装された便利クレートがあるとして、それをPythonから使いたいというものです。今回はcedar-policyを題材にします。
サンプルコードは以下に置いてあります。
プロジェクトの作成
Install dependency
Python, Rust, maturin, pdmをインストールします。
maturin init
maturinプロジェクトを生成します。
プロンプトでは pyo3
を選択します。
以下の様なファイルが生成されます。
個人的にポイントなのはGitHub Actionsの設定が生成されていることです。
タグを付けると自動で複数環境のwheelを作成し、PyPIにリリースされるようになっています。
src/lib.rsには以下の様なコードが生成されています。
これももう動くようになっており、加算した結果を文字列で返す関数が定義されています。
私のようにGitHubのレポジトリにプレフィックスを付けたい派の人はフォルダ名とプロジェクト名が一致しないため、ここで直しておきます。
プロジェクト名は test_maturin_cedar
ということにしました。
pdm init
maturinはRust-Python間のみが守備範囲のため、Pythonプロジェクトの管理は別途必要です。
pdmが良さそうなのでこれを使います。
プロンプトは以下で答えました。pyproject.tomlが既にある状態で更新してくれるのはすごいですね。
.gitignore
にいくつかエントリを追加します。
maturinの.gitignoreは不必要な行が多かったので一旦削除しました。
また、srcフォルダにプロジェクトフォルダが追加されているので削除します。(このフォルダはRustのツリーなので)
pyproject.toml
を少し修正します。
- build-backendはmaturinを使うので、pdmに変更されているのを戻します。
- versionがdynamicと設定されているので削除します。
動作確認
pdm install
でインストールできます。
動かせるかPythonのREPLでさくっと確認します。
ちゃんと動くのはすごいですね。これでRust界とPython界を繋ぐことができました。
自動テストの整備
pdmを使っているので、簡単にpytestをインストールできます。
tests/test_maturin_cedar.py
を作成します。
pdm run pytest
でテストが通ることを確認します。
完璧ですね。maturinで作ったモジュールのテストはPython界から行なうと良いのかなと思います。Rustからもできるのですが、maturinのテストの作法が難しく、結局Python側から使えることを確認する必要があるので。
pythonレイヤーの追加
maturinで作ったモジュールをPythonから使うことができるようになりましたが、PythonにRustのAPIをそのまま見せると整備が結構大変です。Pythonのモジュールとして使いやすくするために、Rustの手前にPythonレイヤーを追加します。
ドキュメントはProject Layout/Mixed Rust/Python projectを参考にします。
良く読むと冒頭の例より python
というフォルダを切る方がおすすめのようなので、これを採用します。
pyproject.tomlで python
フォルダを使うよと教えます。
あとはさくっと用意します。
__init__.py
は以下の内容を書きます。
lib.py
は以下の内容を書きます。
テストを追加します。
pdm run pytest
でテストが通ることを確認します。
動きますね。
typing
maturinで作ったモジュールには型情報がないので、教えてあげる必要があります。
Rustの型情報から自動生成できるようになるらしいのですが、今のところは手動で定義する必要があります。
py.typed
は空ファイルです。
PEP561に準拠して型情報があることを示します。
test_maturin_cedar.pyi
は以下の内容を書きます。
これで型情報を教えることができ、LSPなどで補完が効くようになります。
リリース
ここまでで一旦PyPIにリリースしてみます。
PyPIのトークンの設定をします。usernameは固定値 __token__
です。passwordにはPyPIのトークンを設定します。
リリースします。1
PyPIにリリースできたら、今回のレポジトリに権限を絞ったトークンを作成できるので作成します。
GitHubの当該レポジトリの [Settings] -> [Secrets and variabes] -> [Actions] -> [New repository secret] で登録します。キーの名前は PYPI_API_TOKEN
です。
先程の手動リリースで v0.1.0
が消費されたので、 v0.1.1
とします。
GitHub Actionsでリリースするためトリガーを叩きます。
しばらく待つとリリースが完了します。素晴らしいですね。
PyPIを見に行くとこの様に各環境のwheelが作成されていることが確認できます。
利用者はこのwheelをダウンロードして利用するため、利用側にはRustのインストールは不要です。便利。
cedarバインディングの作成
Rustプロジェクトに cedar-policy
を追加します2。
後は src/lib.rs
でPythonとのインターフェースを追加します。
バインディングで pyo3
を選択したので、実際には pyo3
のドキュメントを参考にしながら実装することになります。
src/lib.rs
は以下のようになります。いろいろお手軽実装になっていますが、一旦動作はします。
Python側の pyi
はこの様になります。
テストコードです。
さて動くでしょうか。
動きました! これにて目標達成です。
まとめ
maturin
を使うとRustをPythonに簡単にバインディングすることができます。
maturin
コミュニティの尽力によりリリースも簡単ですし、ドキュメントも豊富です。世界が広がると思うので、ぜひ試してみてください。
最後にcedarのサンプルコードを比較しようと思います。
一方、今回作成したcedarバインディングで書いたPythonではこんな感じです。
これを見ると、やはりRustのフロントエンド言語としてのPythonという領域は思ったより可能性があると感じます。みなさんはどう思いますか?