幾つか C ライブラリの wrapper を作ったので、コツをメモしておきます。
メタ
リファレンス
TODOs..
- Windows, MSVC
- CI
no_std
- Application bundle (と dylib)
- ユーザのコンピュータにインストールされたライブラリを発見する (SDL など)
bindgen
Builder
の設定
#[derive(Default, PartialEq)]
を追加
Builder を設定すれば、 Default
や PartialEq
を実装してもらうことができます。
Rustified enum を使う
C の enum は型付けが弱く、 32 bit 整数値として扱われます。
これは C コードと対応する Rust FFI の例です:
typedef enum sg_action {
_SG_ACTION_DEFAULT,
SG_ACTION_CLEAR,
SG_ACTION_LOAD,
SG_ACTION_DONTCARE,
_SG_ACTION_NUM,
_SG_ACTION_FORCE_U32 = 0x7FFFFFFF
} sg_action;
pub const sg_action__SG_ACTION_DEFAULT: sg_action = 0;
pub const sg_action_SG_ACTION_CLEAR: sg_action = 1;
pub const sg_action_SG_ACTION_LOAD: sg_action = 2;
pub const sg_action_SG_ACTION_DONTCARE: sg_action = 3;
pub const sg_action__SG_ACTION_NUM: sg_action = 4;
pub const sg_action__SG_ACTION_FORCE_U32: sg_action = 2147483647;
pub type sg_action = ::std::os::raw::c_uint;
一方、 build.rs
で bindgen::Builder
を設定すると、 rustified enum が出力されます:
builder.default_enum_style(bindgen::EnumVariation::Rust {
non_exhaustive: false,
});
# [repr(u32)]
# [derive(Debug, Copy, Clone, Hash, PartialEq, Eq)]
pub enum sg_action {
_SG_ACTION_DEFAULT = 0,
SG_ACTION_CLEAR = 1,
SG_ACTION_LOAD = 2,
SG_ACTION_DONTCARE = 3,
_SG_ACTION_NUM = 4,
_SG_ACTION_FORCE_U32 = 2147483647,
}
ところで、 C の
enum
はプラットフォームによって異なる ABI を持っていいます。上の例で、sg_action
型は::std::os::raw::c_uint
でしたが、 Windows MSVC では::std::os::raw::c_int
型になります。
出力ファイルに追記
Builder::raw_text
で bindgen
の出力にテキストを追加できます。せっかくなので、モジュールの docstring と #![allow(warnings)]
を追加しておきます。
出力を src/
ディレクトリに出す
bindgen
の user gude では、 OUT_DIR
(ユーザから隠れたディレクトリ) に FFI ファイルを出力し、それを include!
することが勧められています。
しかし、僕は src
ディレクトリに FFI を出力し、コミットするのが良いと思います。メリットは、
-
include!
しなくなったため、 FFI の定義へ飛べるようになります (Emacs の場合) 。 - C ライブラリがアップデートされたとき、 FFI の変化を確認できます。
注意点としては、 crates.io では
src
ディレクトリが read-only になっているため、ファイルの書き込みに失敗します。そのため、ファイルの書き込みに失敗してもok)
とします。
まとめ
bindgen
の wrapper を用意しました:
/// Generates Rust FFI from a wrapping `.h` file
fn gen_bindings(
// *.h
wrapper: impl AsRef<Path>,
// *.rs
dst: impl AsRef<Path>,
clang_args: impl IntoIterator<Item = impl AsRef<str>>,
docstring: &str,
mut setup_builder: impl FnMut(bindgen::Builder) -> bindgen::Builder,
) {
let gen = bindgen::Builder::default()
.header(format!("{}", wrapper.as_ref().display()))
.clang_args(clang_args)
.parse_callbacks(Box::new(bindgen::CargoCallbacks));
let gen = setup_builder(gen);
// 出力ファイルに docstring を追加
let gen = gen
.raw_line(docstring)
.raw_line("")
.raw_line(r"#![allow(warnings)]");
// FFI を生成
let gen = gen.generate().unwrap_or_else(|err| {
panic!(
"Unable to generate bindings for `{}`. Original error {:?}",
dst.as_ref().display(),
err
)
});
// FFI を出力する。 crates.io では `src` ディレクトリへの書き込み権が無いため、
// 失敗を許す
gen.write_to_file(dst).ok();
}
呼び出し側のコード例としては、
/// Compile `Refresh` and generate Rust FFI to it
fn main() {
println!("cargo:rerun-if-changed=Refresh");
println!("cargo:rerun-if-changed=wrappers");
// compile (and link)
// ~~
// generate Rust FFI to `Refresh`
let root = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
let args = &[format!("-I{}", root.join("Refresh/include").display())];
self::gen_bindings(
root.join("wrappers/refresh_ffi.h"),
root.join("src/ffi.rs"),
args,
"//! Rust FFI to `Refresh.h`",
|b| b.derive_default(true).derive_partialeq(true).derive_eq(true)
.default_enum_style(bindgen::EnumVariation::Rust {
non_exhaustive: false,
})
);
self::gen_bindings(
root.join("wrappers/refresh_ffi_image.h"),
root.join("src/img.rs"),
args,
"//! Rust FFI to `Refresh_Image.h`",
|b| b.derive_default(true).derive_partialeq(true).derive_eq(true)
.default_enum_style(bindgen::EnumVariation::Rust {
non_exhaustive: false,
})
);
}
コンパイル
リンクするためには Build Scripts - The Cargo Book を参考にします。
cc
- 必要に応じて、コンパイルに必要なライブラリにリンクします:
println!("cargo:rustc-link-lib=framework=Metal");
println!("cargo:rustc-link-lib=framework=MetalKit");
- コンパイルした他の C ライブラリは、
cc
が自動的にリンクしてくれます。
cmake
CMake には詳しくないのですが、 no_build_target
を指定するとコンパイルできたことがあります:
/// Compile and link to the `Refresh` C library
fn compile() {
let root = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap());
let path = root.join("Refresh");
let _out_dir = Config::new(path)
.no_build_target(true)
.cflag("-w") // suppress errors
.build();
println!(
"cargo:rustc-link-search=native={}",
out_dir.join("build").display()
);
println!("cargo:rustc-link-lib=dylib=Refresh");
}
- コンパイルしたライブラリへのリンクを
println!
で指示します。 - リンクできているか確かめるため、 FFI の関数を呼ぶテストを走らせます。
- ビルドする度に
cmake
が走るなら、分岐を入れます: もしも$OUT_DIR/build/libRefresh.dylib
が存在したら、コンパイルをスキップします (リンクはします) 。
FFI の wrapper を作る
Wrapper が必要な理由
bindgen
の出力 (sys クレート) は、そのままでは使いにくいです:
- 関数
-
unsafe
とマークされている - メソッドではない
-
- 型付けが弱い
-
bool
がu8
になっている (C89 の場合) - bitflags が
u32
になっている
-
-
enum
の名前が冗長 - Rust の型と C の型が合わない
- C にはスライスが無いため、代わりにポインタと長さが使われている
- C の文字列は null-terminated だが Rust の文字列は fat pointer 経由で扱われる
- C ではコールバックに
void*
などのポインタを使用している。 Rust において対応する関数はextern "C" fn
となる
なので wrapper を用意します。ただし、作業量はそれなりのものとなります。ユーザから隠れるクレートならば、 wrapper 無しで使っても良いと思います。
ラッピングのコツ
作業量が多いので、 Vimmer ならマクロを使うと良いと思います。
*mut T
, *mut void
-
&T
から*mut T
を作る
C の const T* x
は Rust の x: *const T
に翻訳されます。
C の T* x
は Rust の x: *mut T
に翻訳されます。
C の関数宣言に const
が無い場合、 Rust 側コードでは &T
から *mut T
を作られなばりません。可能です:
// x: &T
let x_ptr: *mut T = x as *const _ as *mut _
ただし C の関数が引数を書き換える場合は、必ず
&mut T
から*mut T
を作ってください (でなければ UB です) 。
-
*mut c_void
から&mut T
を作る
let typed_user_ptr: &mut T = unsafe {&mut *(user_ptr as *const _ as *mut T) }:
- メモリ位置を固定する
Callback 関数を使う場合は、ユーザデータのメモリ位置を固定する必要が出てきます。その場合は Box
(ヒープ) に入れます。
enum
Rustified enum (前述) は、まだ variants の名前が冗長なので、ラッピングするのも手です。
bitflags
bitflags::bitflags! で実装します。
struct
- 構造体のフィールドの型付けの弱さと名前ケースは諦めます。
- 構造体にメソッドを追加するには、 trait を実装するか、別の構造体でラッピングして
Deref
を実装します。
構造体は re-export することになります。 pub type X = ffi::Item
よりも pub use ffi::Item as X
を使うのがおすすめです。
- 一発で FFI 側の定義へ飛べます。
-
cargo doc
が綺麗になります (FFI のアイテムが wrapper 側に存在するアイテムのように見える) 。
所有権と参照ルール (を無視)
C の関数が C 構造体のポインタを使うとき、 wrapper は &mut self
ではなく &self
を引数にできます。すると Rust の借用ルールを疎かにしてしまいますが、考えることは減ります。
僕は積極的に &self
を引数にしています :)
build.rs
間の情報伝達
比較的マニアックです。ここで読むのを止めても十分だと思います。
Feature flag の伝播
Features - The Cargo Book に載っている通り、クレートの feature flag と依存クレートの feature flag を連動させることができます:
[features]
glcore33 = ["rokol_ffi/glcore33"]
metal = ["rokol_ffi/metal"]
d3d11 = ["rokol_ffi/d3d11"]
[dependencies]
rokol_ffi = { path = "rokol_ffi", version = "0.1.2" }
環境変数ではなく feature flag を使うメリットは、 feature 毎にキャッシュが取られる点です。
メタデータ (例: 選択された backend) を FFI 使用者に通知する
Build Scripts - The Cargo Book #The links manifest key に載っています。
たとえば FFI の build.rs
が graphics API を指定するとします:
```rust:rokol_ffi/build.rs`
/// Helper for selecting Sokol renderer
[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Renderer {
D3D11,
Metal,
GlCore33,
}
impl Renderer {
pub fn select(is_msvc: bool) -> Self {
// set renderer defined by feature
if cfg!(feature = "glcore33") {
Self::GlCore33
} else if cfg!(feature = "metal") {
Self::Metal
} else if cfg!(feature = "d3d11") {
Self::D3D11
} else {
// select default renderer
if cfg!(target_os = "windows") && is_msvc {
Self::D3D11
} else if cfg!(target_os = "macos") {
Self::Metal
} else {
Self::GlCore33
}
}
}
pub fn emit_cargo_metadata(&self) {
match self {
Self::D3D11 => println!("cargo:gfx=\"d3d11\""),
Self::Metal => println!("cargo:gfx=\"metal\""),
Self::GlCore33 => println!("cargo:gfx=\"glcore33\""),
}
}
}
`emit_cargo_metadata` の結果を受け取れるのは、『`rokol_ffi` を `Cargo.toml` に追加しているクレートの `build.rs`』のみです。環境変数 `DEP_<LIB>_<VAR>` として渡されます:
```toml:rokol/Cargom.toml
[dependencies]
rokol_ffi = { path = "rokol_ffi", version = "0.1.2" }
fn main() {
let gfx = env::var("DEP_SOKOL_GFX").expect("`rokol_ffi` failed to select graphics backend?");
// For `DEP_<LIB>_<VAR>`, see:
// https://doc.rust-lang.org/cargo/reference/build-scripts.html#the-links-manifest-key
// and emit it so that it's available in this crate!:
println!("cargo:rustc-cfg=rokol_gfx={}", gfx);
}
これにより、 rokol
クレート内で #[cfg(rokol_gfx = "glcore33")]
のような分岐ができます。
分かりづらい!!!!