FORCIA CUBEフォルシアの情報を多面的に発信するブログ

RustでFFIを使う・FFIでRustを使う

2021.12.01

アドベントカレンダー2021 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_libmodest_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-typecdylib にします。

# 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/main.c
#include<stdio.h>

// 関数定義のみ
int gcd(int a, int b);

void main(){
    printf("%d\n", gcd(12, 8));
}

例えば ../legendary_c_lib/legend.sogcd の実装があるとすれば次のコマンドでコンパイル・実行が可能です。

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言語で実装された関数を実行する例を示します。

# main_py/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から使うことができます。

# main_py/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

ずばりサンプルそのままなので説明は割愛しますが、 pymodulepyfunction を詰め込んでいく形が分かりやすいと感じました。

maturin を使わない場合は target/release/string_sum.sotarget/release/libstring_sum.so にリネームして sys.pathtarget/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が得意なエンジニア。お寿司が大好き。

フォルシアではフォルシアに興味をお持ちいただけた方に、社員との面談のご案内をしています。
採用応募の方、まずはカジュアルにお話をしてみたいという方は、お気軽に下記よりご連絡ください。


採用お問い合わせフォーム 募集要項

※ 弊社社員に対する営業行為などはお断りしております。ご希望に沿えない場合がございますので予めご了承ください。