どうも、ナナオです。

最近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/apprust/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でワークスペース運用を検討している人は参考にしてみてください。

GitHub - satodaiki/maturin-workspace-playground