Rustは C FFI が強いので、はっきりしたstructとCの関数を呼び出すなんていうケースでは、それっぽいstructとexternでの関数の定義を書けば呼び出せる。コールバックもできる。
→
他言語関数インターフェイス
しかし、以下のようなケースではちょっとやりにくい。
- 必要な関数の呼び出しにCマクロが必要
- opaqueなstructの下のほうのメンバにアクセスが必要
要はC言語を解釈してもらえると楽だよねという話なので、「Cでラッパー書いてautotoolsだMakefileだなんだかんだ、ようやくつなげる」となるかと思いきや、このようなCラッパーが必要な状況についてもRustには補助がある。今回はそのやりかたについて紹介する。
自分が必要だったところがC世界からのコールバック呼び出しだったので、それについても触れる。基本的には、RustのアプリケーションがCライブラリを使うという想定。コールバックなので、行ったり来たりなのだけどね。
やりかたの概要
やりかたは概ね以下の通り。
- ラッパー関数をCで用意する
- CargoのビルドスクリプトでCソースのコンパイル方法を指示する
- ラッパー関数をexternで定義する
- ラッパー関数を呼び出すstruct & traitを定義する
- 実際に呼び出す
実際のソースは
https://github.com/sunnyone/rust-bridgesampleに置いてあるので、ソースがあればいいという方はこちらでどうぞ。
やりかた
ラッパー関数の準備
Rustから呼び出したい関数を、普通の関数呼び出しにできる形で定義する。本来は何かのコードのラッパーの想定なのだが、そっち側の知識が必要となると話がややこしくなるので、ここでは単なるCソースを例として使う。
上記がsrc/bridge.cに置かれているとする。
Cargo.toml + build.rsにCソースのコンパイル方法の指示する
ここがこの記事のキモ。cargoに、置いたCソースのコンパイル方法を指示する必要がある。
まず、Cargo.tomlにビルドスクリプトが「build.rs」にあるよ、ということを指示する。またコンパイラの実行の面倒を見てくれる「gcc」crateをbuild dependenciesに入れる(gccと言っているが、MSVCも対応しているらしい。試してないが)。
[package]
(略)
build = "build.rs"
[build-dependencies]
gcc = "0.3"
[dependencies]
libc = "0.2"
dependenciesのlibcは、ビルドスクリプト的には必要ないが、C FFIではほぼ使うのでここで入れておく。
上記で定義した「build.rs」に、コンパイラの呼び出しを定義する。
extern crate gcc;
fn main() {
gcc::compile_library("libbridgesample.a", &["src/bridge.c"]);
}
このあたりの記述、詳しくはCargoのドキュメント「
Build Script Support」を参照。
本来ライブラリのリンクの指示も必要だが、ここでは一旦省く。詳しくは上記ドキュメントを参照(下でもすこし触れる)。
ラッパー関数をexternで定義する(いわゆる-sys部分)
ここからはもうRust onlyでのFFIと一緒。externでfnとstructを定義する。opaqueなstructをenumを定義するのがイディオムだそうだ。
Cargoの流儀的には、FFIのためのRustらしくない定義はなんちゃら-sysというcrateに定義するほうがそれらしい。この部分だけの差し替えがしやすいから、とのこと。
ラッパー用のstructを用意する
ffi層を叩く、Rust 的構造にあわせた呼び出し部を用意する。今回は、コールバックをクロージャでもらい、それを実際のCコールバックから呼ぶ形にした。
「外側」と「内側」の2つのstructを作る
Rust世界のコード用のデータを入れておく「内側」と、その「内側」とC関数からもらったポインタを格納しておく「外側」を用意する。
必ずしもこうでなくてよいが、こうしておくことで「内側」をBox(ヒープ)で持つことが出来、ポインタに変換してC関数のポインタに渡すことができる。
git2-rsの
src/transport.rsでやっていたので参考にした。
Drop traitを実装しポインタが自動で捨てられるようにする
後始末大事。
コールバックとしてC側に渡す関数をexternで定義する
今回はライフタイムをRustが持っているので、キャストするだけ。ここでは、Rust側がライフタイムを握っているのでポインタを渡しているが、もしC側がライフタイムを握るようであれば、
Box::into_rawと
Box::from_rawをうまく使うといいと思う。ライフタイムの制御から外せる。
mem::forget というのもあるかな。
ラッパーを実装
あとはffi関数を呼び出すようMyStructにimplする。
呼び出しコードを書く
実際呼ぶコードはこんなかんじになる。 結果はこんな感じ。
mystruct_new
Hello, World: 11
Hello Result: 12345
mystruct_free
これでだいたい使えるはず。
おまけ:pkg-configでライブラリを参照させる
本来ラップする対象があるはずなので、リンク対象やincludeパスを指定する必要がある。基本的には、println!()で、cargoが必要な情報を渡してあげればよいが、build-dependenciesでpkg-config crateを使うと、pkg-configの結果を使える。例えばこんな感じ。
with-glib branchで使ってみてある。実際のところは、panicはやっつけなのでちゃんと分岐させたり、pkg-configに関わらず環境変数で設定できるようにしたりと、工夫がされていることが多いので、詳しくはそこらのライブラリのコードを参照。例えば、glib-sys crateは
こんな感じ。