Rust + PyO3にて、自作のPythonモジュールをRustで実行する

RustからPythonのモジュールを使うときは、 PyO3 が便利です。
PyO3/pyo3: Rust bindings for the Python interpreter

 
ただ、公式ドキュメントでは、Pythonの標準モジュールをimportして実行する方法は記載されていたものの、自作のPythonモジュールについては記載が見当たりませんでした。
Calling Python from Rust - PyO3 user guide

 
そこで、調べたことをメモしておきます。

 
目次

 

環境

 

自作のPythonモジュール

以下の内容を hello.py として用意します。この中の say() 関数をRustで使いたいとします。

def say(message):
    return f'hello, {message}'

 

ディレクトリ構造
$ tree
.
├── Cargo.lock
├── Cargo.toml
├── hello.py
├── src
│   └── main.rs
└── target

 

ダメな方法

標準モジュールのように

let gil = Python::acquire_gil();
let py = gil.python();
let hello = py.import("hello")?;

とimportした場合、コンパイルは通るものの、実行すると以下のエラーになります。

Error: PyErr { type: Py(0x10529f7a8, PhantomData) }

 

対応:sys.pathにエントリを追加する

以下のissueにある通り、自作モジュールがsys.pathに追加されていないため、PyO3を使って追加します。
How can I import Python code into Rust? · Issue #148 · PyO3/pyo3

今回は、カレントディレクトリにPythonファイルを置いたため、カレントディレクトリを sys.path に追加します。

let syspath: &PyList = py.import("sys")
    .unwrap()
    .get("path")
    .unwrap()
    .try_into()
    .unwrap();

let path = env::current_dir()?;

// path.display()をそのまま渡すとエラーになるので、文字列にして渡す
// error[E0277]: the trait bound `std::path::Display<'_>: pyo3::conversion::ToPyObject` is not satisfied
// syspath.insert(0, path.display()).unwrap();
syspath.insert(0, format!("{}", path.display())).unwrap();

 
これで import できるようになりました。

 
全体像はこんな感じです。

use pyo3::prelude::*;
use pyo3::types::PyList;
use std::env;


fn main() -> PyResult<()> {
    let gil = Python::acquire_gil();
    let py = gil.python();

    let syspath: &PyList = py.import("sys")
        .unwrap()
        .get("path")
        .unwrap()
        .try_into()
        .unwrap();

    let path = env::current_dir()?;

    // error[E0277]: the trait bound `std::path::Display<'_>: pyo3::conversion::ToPyObject` is not satisfied
    // syspath.insert(0, path.display()).unwrap();
    syspath.insert(0, format!("{}", path.display())).unwrap();
    
    let hello = py.import("hello")?;
    let response: String = hello.call1("say", ("Shinano Gold", ))?.extract()?;
    println!("{}", response);

    Ok(())
}

 
cargo run すると、say()関数の結果が表示されました。

$ cargo run
   Compiling using_python_from_rust_by_pyo3 v0.1.0
    Finished dev [unoptimized + debuginfo] target(s) in 2.83s
     Running `target/debug/using_python_from_rust_by_pyo3`
hello, Shinano Gold

 
なお、READMEには

Python::with_gil(|py| { /**/ }

のような書き方がされていますが、これは次のバージョン (0.12) よりサポートされるようです。
no function or associated item named with_gil found for struct pyo3::python::Python<'_> · Issue #1079 · PyO3/pyo3

そのため、今のバージョンで書こうとすると、 with_gil のところでコンパイルエラーになります。

 

その他

RustでPythonを扱う方法としては、PyO3の他に rust-cpython もあります。
dgrunwald/rust-cpython: Rust <-> Python bindings

ただ、GitHubのstar数や、rust-numpyがバージョン0.3以降rust-pythonからPyO3に移ったことを考えて、今回はPyO3を使いました。
PyO3/rust-numpy: PyO3-based Rust binding of NumPy C-API

PyO3については、メンテナの方の記事が詳しいです。
PyO3: これまで/これから - Qiita