【Python】ctypes で Rust 関数を呼び出す【FFI】

  • 2021年12月25日
  • 2022年8月1日
  • Python, Rust
  • 181View
  • 0件

はじめに

Python のパッケージでは、高速化のために一部 C言語を利用しているものがあったりします。(numpy とか)
でも今は C よりも Rust が人気なようなので、Rust の関数の呼び出し方を紹介します。

ctypes を利用した、最も原始的なやり方です。
最近の技術を活用すると wasi とかも手段にあります。
いずれ紹介します。

サンプルコード

hello 関数呼び出し

最も単純な関数、hello を呼び出します。

なお、WSL の Ubuntu で動作確認をしています。
Linux なので、Rust のコンパイル結果は libhello.so というファイル名になっています。
Windows だと .dll が生成されるかと思います。

#[no_mangle]
pub extern "C" fn hello() {
    println!("Hello from Rust!")
}
❯ rustc --crate-type cdylib hello.rs
#!/usr/bin/env python3
import ctypes
import os


lib = ctypes.cdll.LoadLibrary(os.path.join(
    os.path.dirname(__file__), 'libhello.so'))

lib.hello()
❯ ./main.py 
Hello from Rust!

hello.rs

hello.rs は Rust のソースコードです。
hello 関数のみ定義しています。

no_mangle によって、コンパイル時のマングリングを抑止しています。
これを抑止しないと、関数名が無茶苦茶な文字列に翻訳されてしまいます。
なお、no_mangle を付与しないと次のようなエラーが発生します。
AttributeError: ./libhello.so: undefined symbol: hello

また、extern “C” の部分は、バイナリ生成時の ABI に影響します。
ちなみに今回の例では extern “C” を除外しても問題なく動作しました。

rustc でコンパイルする際には、–crate-type cdylib を指定します。
これを指定することで .so ファイル(Windows の場合は .dll)が生成されます。

main.py

Python からは ctypes を使って Rust の hello 関数を呼び出しました。
C言語の関数を呼び出すやり方と全く同じです。

まず LoadLibrary で libhello.so を読み込みます。
このとき、.so のファイルパスには注意です。

そのあとで hello() を呼び出したら、Rust 実装の関数がそのまま呼び出されます。

引数と戻り値

次に、引数と戻り値を持つ関数の場合を紹介します。

#[no_mangle]
pub extern "C" fn add(x: f64, y:f64) -> f64 {
    x + y
}
#!/usr/bin/env python3
import ctypes
import os


lib = ctypes.cdll.LoadLibrary(os.path.join(
    os.path.dirname(__file__), 'libadd.so'))

lib.add.argtypes = [ctypes.c_double, ctypes.c_double]
lib.add.restype = ctypes.c_double
print(lib.add(2.2, 3.3))
❯ ./main.py 
5.5

add.rs

Rust の関数として、浮動小数点数を2つ足し算する add を定義しました。
hello を作ったときのルールさえ守れば、他には注意することはそんなにありません。

main.py

Python 側で Rust 関数を呼ぶ前に、argtypes と restype を指定しています。
これを指定しないと、型変換エラーが発生したり、強引に int 型として取り扱われたりしてしまいます。

argtypes の指定を省略するとこうなります。

Traceback (most recent call last):
  File "./main.py", line 12, in <module>
    print(lib.add(2.2, 3.3))
ctypes.ArgumentError: argument 1: <class 'TypeError'>: Don't know how to convert parameter 1

restype の指定を省略すると、正常終了はしてしまうのですが、期待とは異なる値が取得されてしまいます。

❯ ./main.py 
2

なお、Python コード上で指定するのは、あくまで C言語上での型を指定することになります。
Rust の f64 は、C言語の double と同等ですので、メモリサイズを気にしながら頭の中で翻訳してあげる必要があります。

おしまい!