はじめに
maturin / PyO3 を使ってPythonの拡張モジュールをRustで実装する、ことをよくしていたが、コマンドラインツールとしてもビルドする必要が出てきたので、その方法をまとめる。
要件:以下の2つを同じプロジェクトでできるようにする。
- Rustのコマンドラインツールの実行バイナリ(
cargo build
でビルド) -
pip install
できるPythonモジュール(maturin build
でビルド)
外部リンク:
- 自作テンプレート(English version here too.):
https://github.com/k94-ishi/maturin-mixed-template - 参考ページ:
How to Mix Rust and Python in Your Project
Maturin User Guide: project_layout#mixed-rustpython-project
Python側の環境準備
-
maturin
をインストール
pip install maturin
プロジェクトの作成
単にPythonモジュールを作成する場合はmaturin new --bindings pyo3 my_project
で作成するが、今回はRustのコマンドラインツールの実行バイナリもビルドできるようにしたいので、--mixed
オプションを指定する。
-
maturin new --help
で--mixed
オプションを確認
--mixed
Use mixed Rust/Python project layout
- Rust/Python混合プロジェクトを新規作成
maturin new --bindings pyo3 --mixed my_project
Cargo.toml
とpyproject.toml
の修正
Cargo.toml
の修正
-
[features]
の追加
cargo build
でPython関連のソースをビルドしようとしてエラーと成ることを防ぐ。
[features]
default = [] # デフォルトではPython関連の機能を無効化
python = ["dep:pyo3"] # `python` フィーチャーが有効な時のみ `pyo3` を使う
-
[lib]
のcrate-type
に"rlib"
を追加
src/lib.rs
をPython用とコマンドライン用それぞれの用途でビルドできるようにする。
[lib]
name = "my_project"
# ["cdylib"]
crate-type = ["cdylib", "rlib"]
# "cdylib"はPython用、"rlib"はコマンドライン用
-
[dependencies]
のpyo3
にオプション追加
ビルドソースを選んでビルドできるようにする。
[dependencies]
# pyo3 = "0.23.3"
pyo3 = { version = "0.23.3", features = ["extension-module"], optional = true }
pyproject.toml
の修正
-
[tool.maturin]
のfeatures
に"python"
を追加
maturin build
でのビルド対象の選択にpython
フィーチャーを使えるようにする。
# features = ["pyo3/extension-module"]
features = ["pyo3/extension-module, python"]
ソースの修正
src/lib.rs
の修正
- コマンドラインでも使いたいソースと、Python用のみに使いたいソースを分ける。
-
cargo build
でビルドしたくないPython用のソースには#[cfg(feature = "python")]
を付ける
#[cfg(feature = "python")]
use pyo3::prelude::*;
pub fn sum_as_string_rs(a: usize, b: usize) -> String {
(a + b).to_string()
}
#[cfg(feature = "python")]
#[pyfunction]
fn sum_as_string(a: usize, b: usize) -> PyResult<String> {
Ok(sum_as_string_rs(a, b))
}
#[cfg(feature = "python")]
#[pymodule]
fn my_project(m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_function(wrap_pyfunction!(sum_as_string, m)?)?;
Ok(())
}
src/main.rs
を追加
- プロジェクト作成時点では
src/main.rs
はないので追加で作成
touch src/main.rs
tree -L 3 # ディレクトリ構成の確認
ディレクトリ構成は下記のようになるはず。
.
├── Cargo.lock
├── Cargo.toml
├── pyproject.toml
├── python
│ ├── my_project
| | └── __init__.py
│ └── tests
│ └── test_all.py
└── src
├── lib.rs
└── main.rs
src/main.rs
を下記のように編集する。
use my_project::sum_as_string_rs;
fn main() {
let text = sum_as_string_rs(1, 1);
println!("1 + 1 = {}", text)
}
ビルド+実行+pip install
cargo run
maturin build
pip install wheel
pip install ./target/wheels/my_project-*.whl
Pythonコードの修正
import
関連のコードが動かない状況なので、修正する。
python/my_project/__init__.py
の修正
# from .my_project import *
from my_project import my_project
python/tests/test_all.py
の修正
# import my_project
from my_project import my_project
この修正でpytest
が動くはず。
pip install pytest
my_project $ pytest
=============================== test session starts ================================
platform darwin -- Python 3.12.3, pytest-8.3.5, pluggy-1.5.0
rootdir: /<directory>/my_project
configfile: pyproject.toml
collected 1 item
python/tests/test_all.py . [100%]
================================ 1 passed in 0.04s ================================
さいごに
-
features
をうまく使おう - 詳しくはMaturin User Guideを読むべし