6
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Rust FFI bindings のコツをメモ

Last updated at Posted at 2021-01-15

幾つか C ライブラリの wrapper を作ったので、コツをメモしておきます。

メタ

リファレンス

TODOs..

  • Windows, MSVC
  • CI
  • no_std
  • Application bundle (と dylib)
  • ユーザのコンピュータにインストールされたライブラリを発見する (SDL など)

bindgen

Builder の設定

#[derive(Default, PartialEq)] を追加

Builder を設定すれば、 DefaultPartialEq を実装してもらうことができます。

Rustified enum を使う

C の enum は型付けが弱く、 32 bit 整数値として扱われます。

これは C コードと対応する Rust FFI の例です:

example.c
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;
example.rs
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.rsbindgen::Builder を設定すると、 rustified enum が出力されます:

build.rs
builder.default_enum_style(bindgen::EnumVariation::Rust {
    non_exhaustive: false,
});
example.rs
# [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_textbindgen の出力にテキストを追加できます。せっかくなので、モジュールの docstring と #![allow(warnings)] を追加しておきます。

出力を src/ ディレクトリに出す

bindgenuser gude では、 OUT_DIR (ユーザから隠れたディレクトリ) に FFI ファイルを出力し、それを include! することが勧められています。

しかし、僕は src ディレクトリに FFI を出力し、コミットするのが良いと思います。メリットは、

  • include! しなくなったため、 FFI の定義へ飛べるようになります (Emacs の場合) 。
  • C ライブラリがアップデートされたとき、 FFI の変化を確認できます。

注意点としては、 crates.io では src ディレクトリが read-only になっているため、ファイルの書き込みに失敗します。そのため、ファイルの書き込みに失敗しても ok) とします。

まとめ

bindgen の wrapper を用意しました:

build.rs
/// 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();
}

呼び出し側のコード例としては、

build.rs
/// 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

  • 必要に応じて、コンパイルに必要なライブラリにリンクします:
build.rs
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 とマークされている
    • メソッドではない
  • 型付けが弱い
    • boolu8 になっている (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 を連動させることができます:

rokol/Cargo.toml]
[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" }
rokol/build.rs
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")] のような分岐ができます。

分かりづらい!!!!

6
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?