どうも、ナナオです。
最近PythonからRustを呼び出す実装をすることがありまして、pyo3にお世話になることがありました。
pyo3のビルドにはmaturinというCLIを使うのですが、これをRustのワークスペース機能と併用できるのかどうか気になったので、検証してみたいと思います。
準備
とりあえず適当にpyo3を使用したライブラリを作ります。
ryeを使っていれば以下のコマンドでmaturinをビルダーに指定したプロジェクトを作成できます。
rye init maturin-workspace-playground --build-system maturin
maturinをインストールしていなかったので、以下のコマンドでインストールしておきます。
rye install maturin
作ったプロジェクトに移動して、ワークスペースのメンバーになるプロジェクトを作成しておきます。
cd maturin-workspace-playground
mkdir rust && cd rust
cargo init --lib app
cargo init --lib python-api
作成したプロジェクトをワークスペースのメンバーになるように設定をしていきます。
ルートディレクトリのCargo.tomlを以下のように編集します。
[workspace.package]
name = "maturin-workspace-playground"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[workspace]
members = ["rust/python-api", "rust/app"]
rust/app
のCargo.tomlは以下のように編集します。
[package]
name = "app"
version.workspace = true
edition.workspace = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
rust/python-api
のCargo.tomlは以下のように編集します。
[package]
name = "python-api"
version.workspace = true
edition.workspace = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib]
name = "maturin_workspace_playground"
crate-type = ["cdylib"]
[dependencies]
pyo3 = "0.19.0"
あとはルートディレクトリのpyproject.toml
の設定も変えておきましょう。
[tool.maturin]
python-source = "python"
module-name = "maturin_workspace_playground._lowlevel"
features = ["pyo3/extension-module"]
manifest-path = "rust/python-api/Cargo.toml" # この行を追加
これでほぼ準備はできましたが、このままだとmaturin develop
が実行できないので、プロジェクトの開発依存関係にpipを追加しておきましょう。
rye add --dev pip && rye sync
これで準備はできました。
実装
rust/app
にrust/python-api
から呼び出される関数を書いて、rust/python-api
で実際にpyo3の処理を実装していきましょう。
まずはrust/app
から実装しましょう。ですが、既に用意されている関数があるので別に元のコードから特に変更する必要はありませんでした。
pub fn add(left: usize, right: usize) -> usize {
left + right
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_works() {
let result = add(2, 2);
assert_eq!(result, 4);
}
}
次にこの関数を呼び出せるようにrust/python-app
の依存関係にrust/app
を追加していきましょう。
[dependencies]
pyo3 = "0.19.0"
app = { path = "../app" } # この行を追加
rust/python-app/lib.rs
は以下のように実装します。
use pyo3::prelude::*;
use app::add;
/// Prints a message.
#[pyfunction]
fn hello() -> PyResult<String> {
Ok(format!("1 + 1 = {}", add(1, 1)))
}
/// A Python module implemented in Rust.
#[pymodule]
fn _lowlevel(_py: Python, m: &PyModule) -> PyResult<()> {
m.add_function(wrap_pyfunction!(hello, m)?)?;
Ok(())
}
appに定義したadd関数を呼び出すだけの簡単なプロジェクトです。
ではこれをpythonで呼び出せるかテストしてみましょう。
まずはmaturinでビルドします。
maturin develop
python
ディレクトリ配下にtests
ディレクトリを作成し、pytest
を追加します。
mkdir python/tests
rye add --dev pytest && rye sync
pyproject.toml
にpytest用のセクションを追加します。
addopts
に"-s"を指定しているのは、テスト結果に標準出力を表示させるためです。
[tool.pytest.ini_options]
addopts = "-s"
testpaths = [
"python/tests",
]
VSCodeでテストを実行する場合は、.vscode/settings.json
に以下の設定を追加します。
{
"python.testing.unittestEnabled": false,
"python.testing.pytestEnabled": true
}
実行結果は以下の通りです。
============================= test session starts =============================
platform win32 -- Python 3.11.6, pytest-8.3.2, pluggy-1.5.0
rootdir: c:\Users\Nanao\Project\study\maturin-workspace-playground
configfile: pyproject.toml
collected 1 item
python\tests\test_main.py 1 + 1 = 2
.
============================== 1 passed in 0.01s ==============================
Finished running tests!
ちゃんと表示されています。
まとめ
ワークスペース機能を使っても問題なく動かすことができました!
今回使用したリポジトリは以下にプッシュしてあるので、pyo3でワークスペース運用を検討している人は参考にしてみてください。