普段はPython使いなのですが、可視化部分等はPythonでそれ以外はRustで高速できないかと考え調べてみました.
今回はPyo3を使ってRustとPythonをつないでみました.
Pythonのモジュールとして使われるように,Rustのコードを書いてます
ドキュメントはあるものの、情報量もまだまだ足りないので,わからない所含めまとていきます.
ちなみにRustとPyo3の記事だと
を参考にしました.
注意 rustのバージョン
現在,pyo3を使うにはnighty
版が必須です.
これはissueにも記載がありますが,いくつか必要な機能がstable
では提供されていないためです.
Cargo.toml
公式のサンプルを例にPyo3を使うのに必要な設定について説明します.
[package]
name = "string-sum"
version = "0.1.0"
edition = "2018"
[lib]
name = "string_sum"
crate-type = ["cdylib"]
[dependencies.pyo3]
version = "0.7.0"
features = ["extension-module"]
使うセクションは
- [package]
- [lib]
- [dependencices]
の三種類です.
[package]
はcrate用のセクションで,cargoを使ったことがあれば実際にわかると思います.
変数は以下になります.
- name: rustで参照するpackage名
- version: アプリケーションのバージョン
[lib]
はライブラリの出力のセクションです.
- name:build時に作れるファイルの名前です.
- crate-type: 出力のタイプを指定するものです.
Pythonで使うなら、基本Python用の動的ライブラリになるので、["cdylib"]を指定すればよいです.
詳細はここを御覧ください.
[dependencies]
は依存ライブラリのついて記載する所です.
- dependencies.pyo3でpyo3に関する条件を記載する箇所になります.
- versionは指定するバージョン
- featuresは条件つきコンパイルを指定する所で今回は外部モジュールとしてコンパイルすることを指定しています.
なので、実際にpyo3を使って開発したい場合はCargo.toml
からnameだけ変更すればよいのかと思います.
実際のソース
実際にPythonから呼び出す方法を公式のサンプルから説明します.
まずPythonのモジュールとしてみせたいものを
#[pymodule]
をつけた関数として定義します.
引数の型は{ythonのプロセスを表すPython
型と, Pythonのモジュールを表すPyModule型になります.
Rustのattributeは基本決まっていますが,
proc_macro_attribute
を使って新たに作ることができます.(nighty版だけの機能のようですが)
attributeはPythonでいうデコレータになります.おかげで特に何も考えずに使えます.
実際にpythonから呼び出したいオブジェクトを定義してきます.
関数を呼び出す場合
Pythonから呼び出す関数の場合は #[pyfunction]
を設定すればよいです.
公式そのままですが、以下のようにすれば動きます.
use pyo3::prelude::*;
use pyo3::wrap_pyfunction;
#[pyfunction]
/// Formats the sum of two numbers as string
fn sum_as_string(a: usize, b: usize) -> PyResult<String> {
Ok((a + b).to_string())
}
/// This module is a python module implemented in Rust.
#[pymodule]
fn string_sum(py: Python, m: &PyModule) -> PyResult<()> {
m.add_wrapped(wrap_pyfunction!(sum_as_string))?;
Ok(())
}
注意点ですが, 返り値はPyResult
にする必要があります.
また,OK()
は返り値をPyResultにしてくれます.
返り値がない場合は,Ok(())
とし、型としてはPyResult<()>
とすればよいです.
配列の返却方法
さっきの例だとPython側ではStringが返ります.
今度はPython側でlistを返してみます.
#[pyfunction]
fn make_list<'p>(py: Python<'p>) -> PyResult<&'p PyList> {
let v1 = vec![vec![vec![99; 3]; 2]; 10];
let list = PyList::new(py, v1);
Ok(list)
}
でできます.
注意
-
listの場合は
PyList
型にする必要があります. -
#[pyfunction]
が着いている場合,引数:py は普通は指定する必要がありません.
ただlistの場合はnewで引数として渡す必要があるため,指定しています. -
ライフタイム
'p
はpyfunctionとして使用する限りは不要のようです.
ただ,#[pymethods]
で実行する場合、ライフタイムを指定しないとエラーになります.
ソースをおえていないので実態はわからないのですが,pyfunctionはpymoduleにライフタイムが紐づく?ので,ライフタイムを指定せずともエラーにならないのかなと思いました.
クラスの場合
Rust側ではclassを返却するには二つの処理が必要です.
-
#[pyclass]
をつけた構造体の定義 -
m.add_class::<クラス名>
をpymodule内に定義する.
これができれば,動きます.
-
newの設定方法
#[new]
をつけて以下のようにnew
メソッドで実行すると,new相当になります.
実際に例を書くと以下みたいな形になります.
#[pyclass]
pub struct Hoge {
pub x: usize,
}
#[pymethods]
impl Hoge {
#[new]
fn new(obj: &PyRawObject, x: usize){
obj.init({
Hoge {
x: x,
}
});
}
他にもgetter,setterが定義できたりします.
Numpy
ndarrayとやり取りできるはずですが、少し頑張ってもエラーになったので、今回はスキップします.
githubはあるものの、そこのexampleも
#![deny(rust_2018_idioms)]
になっていて,これを外すと動きませんでした.
テスト
どうやらPyo3がある状況だとMacでCargo test
をするとエラーになるようです.
以下のみの実行がエラーになります.(他のPyo3が使わないプロジェクトでエラーにならないことは確認しました)
#[cfg(test)]
#[test]
fn it_works() {
assert!(false);
}
Finished dev [unoptimized + debuginfo] target(s) in 3.19s
Running target/debug/deps/python_rust_example-3e2a90b01ebe111d
dyld: Symbol not found: _PyExc_OverflowError
Referenced from: python_rust_example/target/debug/deps/python_rust_example-3e2a90b01ebe111d
Expected in: flat namespace
in python_rust_example/target/debug/deps/python_rust_example-3e2a90b01ebe111d
error: process didn't exit successfully: `python_rust_example/target/debug/deps/python_rust_example-3e2a90b01ebe111d` (signal: 6, SIGABRT: process abort signal)
この辺は解決するべきですが,その前に夏休みが終わってしまったので,今回はこの辺でお別れです(涙)
まとめ
今回は,最低限Pyo3の使い方を解説しました.
Pyo3はまだまだ枯れきっていないのか,調べてみるとかなりトラブルが発生しました.
そうしたトラブル自体が勉強になることもあるので、自分でも使い込みつつどこかでコミットできたらなと思います.