2020/12/02 追記
Rustでpythonパッケージを作るためのクレートであるPyO3がv0.11.0からRustのstable版でコンパイルできるようになっています。
最近私がPythonを触る機会が少ないのでPyO3を試せていないのですが、この記事で書かれている方法よりはPyO3を使ったほうが幸せになれると思うのでここに追記しておきます。
TL;DR
PythonからCやC++などの低水準のプログラミング言語の関数を呼びたい場合は、CythonやBoost.Pythonを使うのが標準だと思いますが、Rustにはこれが定石というものがありません。
一応cpythonというRustライブラリがあるらしいですが、今回は使いません(使い方が良く分からなかったという理由だったり)。
ここではRustの公式ドキュメントを参考に、PythonからRustの関数を呼び出す方法を模索しようと思います。
当然ながらRustプロジェクトのビルドにはcargo
を使用します。
引数、返り値のない例
まず、Pythonから呼ばれるRustの関数に引数と返り値のない簡単な例を実装します。ついでにここを参考にしてます。
まずはcargo new
で新しいプロジェクトを作ってください。
Rust側のコードはこんな感じにします。
#[no_mangle]
pub extern fn rust_fn() {
println!("This is a rust function.");
}
#[no_mangle]
は関数名のマングリングを行わないようにしてくれます。
また、Rustの外部に関数を渡せるようにextern
修飾子も忘れないでください。
次にCargo.toml
には以下を追記します。
[lib]
name = "embed"
crate-type = ["dylib"]
ライブラリの名前は適当にしてください。ここではembed
にしています。
共有ライブラリを作りたいのでcrate-type
はdylib
を指定します。
そして、こいつをビルドします。
$ cargo build --release
これで、target/release/libembed.so
が生成されますので、これを使うPythonスクリプトを用意しましょう。
from ctypes import cdll
lib = cdll.LoadLibrary("target/release/libembed.so")
lib.rust_fn()
print("done!")
ctypes
はPythonからC互換のライブラリを扱うためのパッケージで、cdll
関数を使ってライブラリを読み込みます。
これを実行すると以下のように出力されます。
$ python3 main.py
This is a rust function.
done!
無事rust_fn
を呼び出せました。
引数ありの例
次は引数がある場合を実装してみます。
#[no_mangle]
pub extern fn rust_fn(x: i32) {
println!("called with {}.", x);
}
from ctypes import cdll
lib = cdll.LoadLibrary("target/release/libembed.so")
lib.rust_fn(100)
print("done!")
実行結果はこうなります。
$ cargo build --release
$ python3 main.py
called with 100.
done!
これもうまく動いてます。
今度は浮動小数点数を引数に取ってみましょう。
#[no_mangle]
pub extern fn rust_fn(x: f32) {
println!("called with {}", x);
}
from ctypes import cdll
lib = cdll.LoadLibrary("target/release/libembed.so")
lib.rust_fn(2.718)
print("done!")
実行
$ cargo build --release
$ python3 main.py
Traceback (most recent call last):
File "main.py", line 5, in <module>
lib.rust_fn(2.718)
ctypes.ArgumentError: argument 1: <class 'TypeError'>: Don't know how to convert parameter 1
はいダメでしたー。
float
型である2.718
を変換する方法がわからないと怒られてます。
Python内でのrust_fn
の引数の型を見てみましょう。
rust_fn
のargtypes
属性で確認できます。Pythonインタプリタを起動して見てみましょう。
Python 3.6.0 (default, Jan 16 2017, 12:12:55)
[GCC 6.3.1 20170109] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> from ctypes import cdll
>>> lib = cdll.LoadLibrary("target/release/libembed.so")
>>> print(lib.rust_fn.argtypes)
None
None
になってますね。
cdll
でロードされた関数群は引数なしで返り値はint
として処理されるらしいです。
こういう時は、自分で引数と返り値の型を指定しなくてはいけません。
一つ制約として、argtypes
に代入するのはctypes
の型のリストのみとなっているので注意が必要です。
ctypes
の型はC互換の型を意味しています。ここではRustのf32
に対応する型が欲しいので仮にc_float
を使います。
from ctypes import cdll, c_float
lib = cdll.LoadLibrary("target/release/libembed.so")
lib.rust_fn.argtypes = (c_float,)
lib.rust_fn(2.718)
print("done!")
$ python3 main.py
called with 2.718.
done!
できましたね。
対応する型の対応表はこちらに書いてありますので参考にしてください。
ここのc_float
を間違ってc_double
とかにすると実行はできますがおかしな結果になってしまうので注意です。
ここで、Rustのf32
がc_float
にしましたが、この対応はアーキテクチャ依存かもしれません(ABI周りの知識がないのでよく分からない)。ですのでRust側でも明示的にc_float
に対応していると指定したいですね。
今度はlib.rs
を変更します。Rustでc_float
等のC互換の型を使うにはlibc
を使います。
extern crate libc;
use libc::c_float;
#[no_mangle]
pub extern fn rust_fn(x: c_float) {
println!("called with {}.", x);
}
Cargo.toml
に以下を追記してください。
[dependencies]
libc = "0.2.21"
これでちゃんとc_float
を明示的に引数の型として指定できました。
これもコンパイルして実行すると同様な結果が出てきます。
返り値もある場合
前述したようにcdll
でロードした関数はデフォルト返り値がint
だと認識されています。ですので引数同様こちらで正しい型をPythonに教えてあげなくてはいけません。
とりあえずRust側の関数を定義しましょう。
extern crate libc;
use libc::c_float;
#[no_mangle]
pub extern fn twice(x: c_float) -> c_float {
x * 2.0
}
引数の値を2倍にして返すtwice
という関数を定義しました。ここでも浮動小数点数はc_float
としてます。
この関数をPython側から呼びましょう。
from ctypes import cdll, c_float
lib = cdll.LoadLibrary("target/release/libembed.so")
lib.twice.argtypes = (c_float,)
lib.twice.restype = c_float
x = 2.718
ans = lib.twice(x)
print("twice({}) = {}".format(x, ans))
print("done!")
ロードされた関数の返り値はrestype
で参照及び更新できます。
これを実行すると以下のようになります。
$ python3 main.py
twice(2.718) = 5.435999870300293
done!
結果が2.718
の2倍の5.436
にぴったりではないのは単精度計算ゆえの誤差ですので、うまく動いています。
これでPythonからRustの関数を引数、返り値ありで呼ぶことができました。
さいごに
引数や返り値に配列や構造体を使いたい場合はもう少しややこしいのでここでは扱いません。
ですが、関数周りの扱いはここでやった事とほぼ同じです。
CやC++でPythonの中身の実装するのが苦行になってきたという方はどしどしRustで実装していきましょう!
それでは。