はじめに
本記事は Rust にスクリプト言語を組み込む方法について調査したメモです。
本記事では弊社での実務に使用することを念頭に、
- 簡単に使えること
- Rust ←→ スクリプト言語間のデータのやり取りが簡単に行えること
- パフォーマンスが悪くないこと
- 安全であること
- スクリプト言語のライブラリが利用できること
の観点で調査を行います。
なお、弊社では Rust からスクリプト言語を実行するケースしかないため、スクリプト言語のモジュールを Rust で実装する方法については調査を行っていません。
調査対象のクレート
今回は pyo3 について調べます。
pyo3 は Rust に Python を組み込むためのライブラリです。
Repository
PyO3/pyo3: Bindings to Python interpreter
調査時のバージョン
pyo3 = "0.16.5"
前提条件
本記事では他の調査との環境の差異を無くすために、検証はすべて以下のコマンドで起動した docker のコンテナ内で行います。
docker run --rm -it \
-v $PWD:/home \
-w /home \
rust:1.62.0 \
/bin/bash
調査内容
導入方法
利用には Python の共有ライブラリが必要なので、 python-dev
をインストールします。
$ apt install python3-dev
その上で pyo3 をインストールするのですが、その際には
- auto-initialize
- serde
の2つの feature フラグを付けておくと良いです。
cargo add --features auto-initialize --features serde pyo3
features の説明は以下のドキュメントをご参照ください。
Rust からスクリプト言語を実行する
簡単なスクリプトの実行
まずは簡単なスクリプトの例です。
Rust と Python のやり取りは Python::with_gil
のブロック内で実行するようになっており、 let fun = PyModule::from_code(
で Python のメソッドを記述したうえで fun.call1((n,))
のように書くことで Rust から引数を渡したうえで Python のメソッドを実行することができます。
fn fibonacci_py(n: i64) -> PyResult<i64> {
Python::with_gil(|py| {
let fun = PyModule::from_code(
py,
r#"
def fibonacci(n):
a, b = 1, 0
for _ in range(n):
a, b = b, a + b
return b
"#,
"",
"",
)?
.getattr("fibonacci")?;
fun.call1((n,))?.extract()
})
}
serde_json::Value 型のデータを渡す
これは少し調べた感じでは出来そうにありませんでした。
serde_json::Value
型から PyObject
型に変換する処理を書ければ良さそうなのですが、これは相当頑張る必要がありそうです。
現実的にはJSON型を文字列で渡して Python 側で dict に変換して処理する、という感じでしょうか。
use pyo3::prelude::*;
use serde_json::json;
fn use_json_in_py(json_str: &str) -> PyResult<bool> {
Python::with_gil(|py| {
let fun = PyModule::from_code(
py,
r#"
import json
def is_adult(data):
data = json.loads(data)
return data['age'] >= 20
"#,
"",
"",
)?
.getattr("is_adult")?;
fun.call1((json_str,))?.extract()
})
}
fn main() {
let john = json!({
"name": "John Doe",
"age": 43,
"phones": [
"+44 1234567",
"+44 2345678"
]
});
dbg!(use_json_in_py(&serde_json::to_string(&john).unwrap()));
}
スクリプト言語のライブラリを利用する
普通に pip を使ってインストールしたライブラリが利用可能です。
この記事の主題からは外れるので簡単に紹介すると、以下の手順で pip をインストールして、
$ wget https://bootstrap.pypa.io/get-pip.py
$ python3 get-pip.py
その後は pip を使用してライブラリをインストールします。
$ pip install numpy
これで、 pyo3 からも numpy が使用できるようになります。
use pyo3::prelude::*;
fn use_library() -> PyResult<()> {
Python::with_gil(|py| {
py.run("import numpy", None, None)?;
Ok(())
})
}
fn main() {
dbg!(use_library());
}
莫大な Python の資産を簡単に利用できるのはとても良いですね!
パフォーマンス
先ほどのフィボナッチ数列を求めるプログラムの実行速度を計測します。
計測に利用したコード
use pyo3::prelude::*;
use std::time::Instant;
fn fibonacci_rs(n: i64) -> i64 {
let (mut a, mut b) = (1, 0);
for _ in 0..n {
(a, b) = (b, a + b);
}
return b;
}
fn fibonacci_py(n: i64) -> PyResult<i64> {
Python::with_gil(|py| {
let fun = PyModule::from_code(
py,
r#"
def fibonacci(n):
a, b = 1, 0
for _ in range(n):
a, b = b, a + b
return b
"#,
"",
"",
)?
.getattr("fibonacci")?;
fun.call1((n,))?.extract()
})
}
fn main() {
let start = Instant::now();
for n in 1..=64 {
let _ = fibonacci_rs(n);
}
println!(
"Rust の実行時間\t:{:?}",
Instant::now().duration_since(start)
);
let start = Instant::now();
for n in 1..=64 {
let _ = fibonacci_py(n).unwrap();
}
println!(
"Python の実行時間\t:{:?}",
Instant::now().duration_since(start)
);
}
$ cargo run --release
Finished release [optimized] target(s) in 0.01s
Running `target/release/pyo3-example`
Rust の実行時間 :50ns
Python の実行時間 :12.945586ms
より効率の良い書き方はありそうですが、気にするほど遅くは無さそうです。
安全性
Python::with_gil
は PyResult
型を返すようになっており、例えば Python のコード内で例外が発生しても panic することなく適切に処理をすることが可能です。
エラーメッセージも特別分かりづらいことは無く、十分実用レベルの安全性かなと感じました。
まとめ
pyo3 はパフォーマンス・安全性は問題がなく、Python の資産を気軽に利用できる点でも有力なクレートだと思いました。
Python であればメンバーの学習コストも比較的小さく済む可能性があり、以前調査した rlua と比べても悪くないです。
一方で、serde_json::Value
型を直接渡せそうにないのは現在想定しているユースケース的にはちょっと辛いです。
記事で紹介した方法も悪くはないのですが、より良い解決策があれば一気に最有力な候補になりそうかなと考えています。