RustでFFIを使う・FFIでRustを使う
RustでFFIを使う・FFIでRustを使う
これは、FORCIA Advent Calendar 2021の1日目の記事です。
エンジニアの松本(@matsu7874)です。
FORCIA CUBEにはRustやサマーインターンの記事を書くことが多いです。
さて、Rustを導入する際、直ちにシステム全体をRustで書き直すのではなく、既存資産を有効活用しながら開発を進められます。
この記事ではFFI(foreign function interface)を使って既に書かれたプログラムを活用しながら、一部をRustに置き換えていく方法について解説します。
特に次の2つのパターンに分けて解説します。
- A: C言語やPythonで書かれた一部のモジュール(典型的には速度や安全性が重要な部分)をRustに置き換えたい。
- B: 主な実装はRustに変更するが、部分的に別の言語で書かれたモジュールを活用したい。
ディレクトリ構成
作業ディレクトリ直下に下記のディレクトリ・ファイルが置かれているとして以降お読みください。
説明のため、legendary_c_lib
と modest_rs_lib_for_c
には、正の整数a,bを受け取り、それらの最大公約数を返すgcd関数を実装しています。
ソースコードはこちらからダウンロードできます。
- legendary_c_lib/ 手が入れられないC言語のライブラリ
- legend.c
- main_c/ C言語で実装された既存のアプリケーションコード
- main.c
- main_py/ Pythonで実装された既存のアプリケーションコード
- call_c.py
- call_rs.py
- main_rs/ Rustで新たに実装される legendary_c_lib を呼びだすコード
- src/main.rs
- build.rs
- Cargo.toml
- modest_rs_lib_for_c/ Rustで新たに実装される main_c, main_py から呼びだされるコード
- src/lib.rs
- Cargo.toml
パターンA: C言語やPythonからRustを使う
C言語から使ってもらえるようにRustを書く
まずはC言語で実装されているアプリケーションから、新しくRustで実装するモジュールを使ってもらえるようにしましょう。
端的にいうとRustで実装したコードを共有ライブラリにして、C言語側のビルド時にリンクします。
cargo new --lib modest_rs_lib_for_c
から lib.rs
に下記のコードを書きます。
// modest_rs_lib_for_c/src/lib.rs
#[no_mangle]
pub extern "C" fn gcd(a: u64, b: u64) -> u64 {
let mut x = a;
let mut y = b;
if x < y {
let t = x;
x = y;
y = t;
}
while y > 0 {
let t = x % y;
x = y;
y = t;
}
x
}
#[test]
fn test_gcd() {
assert_eq!(gcd(12, 4), 4);
assert_eq!(gcd(12, 3), 3);
assert_eq!(gcd(12, 7), 1);
assert_eq!(gcd(2, 70), 2);
}
おおよそ普通のRustのコードです。テストコードも普通に書けます。
Rustの世界で完結する場合は関数定義は fn gcd(a: u64, b: u64) -> u64
となりますが、Cから呼びだしたい場合は #[no_mangle]
と pub extern "C"
をつけます。
こちら(Cと少しのRust - The Embedded Rust Book)に書いてある通りなのですが、コンパイルしたオブジェクトコードをC言語で書かれたプログラムからでも利用できるようにするための宣言です。
続いて Cargo.toml
を編集し、 crate-type
を cdylib
にします。
# modest_rs_lib_for_c/Cargo.toml
[lib]
crate-type = ["cdylib"]
cargo build --release
でコンパイルすると target/release/modest_rs_lib_for_c
ではなく target/release/libmodest_rs_lib_for_c.so
が作られます。
このファイルをリンクすることでC言語側からRust実装の関数を呼びだすことができます。
C言語側の実装を見ていきましょう。 main_c/main.c
はどこかにある gcd
という関数を呼びだすだけのコードです。
//main.c
#include<stdio.h>
// 関数定義のみ
int gcd(int a, int b);
void main(){
printf("%d\n", gcd(12, 8));
}
例えば ../legendary_c_lib/legend.so
に gcd
の実装があるとすれば次のコマンドでコンパイル・実行が可能です。
gcc main.c ../legendary_c_lib/legend.so -o main_c.o
実行すれば正しく 4
が表示されます。
./main_c.o
リンクするオブジェクトをRust実装に切り替えましょう。下記のコマンドでコンパイル・実行ができます。
gcc main.c ../modest_rs_lib_for_c/target/release/libmodest_rs_lib_for_c.so -o main_rs.o
./main_rs.o
C言語実装のgcd関数を使ったときと同じように 4
と出力されることを確認できると思います。
これでC言語からRustで書かれた関数を呼びだすことができました。
PythonからRustを呼ぶ(ctypesを使ってPython側で面倒を見る)
PythonからはCで実装された関数を呼びだすことができ、上記の方法で作った共有ライブラリは同じ方法で、Pythonからも使うことができます。
ctypes --- Pythonのための外部関数ライブラリ — Python 3.10.0b2 ドキュメント
PythonからC言語で実装された関数を実行する例を示します。
# p/call_c.py
import ctypes
legend = ctypes.CDLL('../legendary_c_lib/legend.so')
legend.gcd.argtypes = [ctypes.c_int, ctypes.c_int]
legend.gcd.restype = ctypes.c_int
assert legend.gcd(120, 16) == 8
同じようにRustでCから使えるように作った共有ライブラリをPythonから使うことができます。
# p/call_rs.py
rust = ctypes.CDLL('../modest_rs_lib_for_c/target/release/libmodest_rs_lib_for_c.so')
rust.gcd.argtypes = [ctypes.c_int, ctypes.c_int]
rust.gcd.restype = ctypes.c_int
assert rust.gcd(120, 16) == 8
PythonからRustを呼ぶ(PyO3を使ってRust側で面倒を見る)
上記の方法ではPython側で関数の引数や戻り値の型を明示するなど面倒なことがありました。
Python側にあまり手を入れたくない場合Rust側でもう少しカバーすることができます。
PyO3/pyo3: Rust bindings for the Python interpreter
ずばりサンプルそのままなので説明は割愛しますが、 pymodule
に pyfunction
を詰め込んでいく形が分かりやすいと感じました。
maturin
を使わない場合は target/release/string_sum.so
を target/release/libstring_sum.so
にリネームして sys.path
に target/release
を追加すると import string_sum
ができるようになります。
パターンB: Rustから既存資産を使う
RustからC言語で実装された関数を呼びだす
続いてRustからC言語で実装された関数を呼びだす方法を説明します。端的に言うと、C言語側でアーカイブライブラリを作成し、rustcでコンパイル時にリンクします。
※こちら(RustからCを呼ぶ - Embedded Rust Techniques)で解説されているように rust-lang/rust-bindgen を使ってC言語の実装からRust側のインターフェイスを自動作成する方法もありますが、原理を理解するために自分の手で実装します。
※『実践Rustプログラミング入門』やRustと少しのC - The Embedded Rust Bookなどで cc - crates.io: Rust Package Registryを使って、Rust側のbuild時にC言語側のコンパイルをする方法が紹介されていますが、やはり何が行われているかを理解するために、最低限必要なリンク処理を明示的に実装します。
C言語側でアーカイブファイルを用意します。
gcc -c legend.c -o legend.o
ar crs liblegend.a legend.o
続いて cargo new main_rs
でクレートを作成し、 main_rs/src/main.rs
を編集します。
// main_rs/src/main.rs
extern "C" {
fn gcd(a: i32, b: i32) -> i32;
}
fn safe_like_gcd(a: i32, b: i32) -> i32 {
let mut res = 0;
unsafe {
res = gcd(a, b);
}
res
}
fn main() {
let mut res = 0;
unsafe {
res = gcd(12, 8);
}
assert_eq!(res, 4);
println!("{:?}", res);
res = safe_like_gcd(12, 8);
assert_eq!(res, 4);
println!("{:?}", res);
}
extern "C"
のブロック内で定義されるC言語実装の関数はRustコンパイラの検証を受けていないので unsafe
ブロックの中でしか使えません。呼びだしごとにunsafeが登場しては不便ですから、 safe_like_gcd
のような内部に unsafe
を押し込めたラッパー関数を書くことがあります。
ビルド時にリンクをする部分を見ていきましょう。 Cargo.toml
でビルドスクリプトを設定します。
[package]
build = "build.rs"
main_rs/build.rs
では liblegend.a
をリンクするように下記の記述を追加します。
// main_rs/build.rs
fn main(){
println!("cargo:rustc-link-search=native=/path/to/legendary_c_lib");
println!("cargo:rustc-link-lib=static=legend");
}
ビルドスクリプトにおいて cargo:
で始まる行はCargoを制御するための行であり、今回はリンクしたいオブジェクトの親ディレクトリのパスとリンクしたいライブラリの名前を指定しています。
rustcを直接実行する場合のコマンドで表現すると下記と同じ意味合いです。
rustc src/main.rs -L ../legendary_c_lib -llegend
./main
ではCargoでコンパイル・実行してみましょう。
cargo run --release
4
4
gcd
, safe_like_gcd
それぞれが4を返しており期待通りに動作していることが確認できました。
より詳しく知りたい人は下記の資料をご確認ください。
おわりに
本記事ではC言語からRust、PythonからRust、RustからC言語で実装された関数を呼びだす方法について解説し、より詳しい資料へのリンクを提供しました。
これからRustで開発を始める皆様の助けになり、Rustコミュニティが盛り上がっていくことを願います。
今月末に弊社が運営しているRustのLT会 Shinjuku.rs #19 @オンライン がございますので、なにかやってみたという方はぜひご参加いただければと思います。
また、明日以降もFORCIA Advent Calendar 2021の記事が公開されますので、ぜひご覧ください。
この記事を書いた人
松本健太郎
RustとPythonが得意なエンジニア。お寿司が大好き。
Discussion