61
46

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Python3からRust関数を呼び出す

Last updated at Posted at 2017-03-03

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側のコードはこんな感じにします。

lib.rs
#[no_mangle]
pub extern fn rust_fn() {
    println!("This is a rust function.");
}

#[no_mangle]は関数名のマングリングを行わないようにしてくれます。
また、Rustの外部に関数を渡せるようにextern修飾子も忘れないでください。

次にCargo.tomlには以下を追記します。

Cargo.toml
[lib]
name = "embed"
crate-type = ["dylib"]

ライブラリの名前は適当にしてください。ここではembedにしています。
共有ライブラリを作りたいのでcrate-typedylibを指定します。

そして、こいつをビルドします。

$ cargo build --release

これで、target/release/libembed.soが生成されますので、これを使うPythonスクリプトを用意しましょう。

main.py
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を呼び出せました。

引数ありの例

次は引数がある場合を実装してみます。

lib.rs
#[no_mangle]
pub extern fn rust_fn(x: i32) {
    println!("called with {}.", x);
}
main.py
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!

これもうまく動いてます。

今度は浮動小数点数を引数に取ってみましょう。

lib.rs
#[no_mangle]
pub extern fn rust_fn(x: f32) {
    println!("called with {}", x);
}
main.py
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_fnargtypes属性で確認できます。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を使います。

main.py
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のf32c_floatにしましたが、この対応はアーキテクチャ依存かもしれません(ABI周りの知識がないのでよく分からない)。ですのでRust側でも明示的にc_floatに対応していると指定したいですね。

今度はlib.rsを変更します。Rustでc_float等のC互換の型を使うにはlibcを使います。

lib.rs
extern crate libc;
use libc::c_float;

#[no_mangle]
pub extern fn rust_fn(x: c_float) {
    println!("called with {}.", x);
}

Cargo.tomlに以下を追記してください。

Cargo.toml
[dependencies]
libc = "0.2.21"

これでちゃんとc_floatを明示的に引数の型として指定できました。
これもコンパイルして実行すると同様な結果が出てきます。

返り値もある場合

前述したようにcdllでロードした関数はデフォルト返り値がintだと認識されています。ですので引数同様こちらで正しい型をPythonに教えてあげなくてはいけません。
とりあえずRust側の関数を定義しましょう。

lib.rs
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側から呼びましょう。

main.py
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で実装していきましょう!
それでは。

参考文献

61
46
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
61
46

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?