Rust FFI bindings のコツをメモ

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

# メタ

## リファレンス

* [Build Scripts - The Cargo Book](https://doc.rust-lang.org/cargo/reference/build-scripts.html)
* [bindgen](https://github.com/rust-lang/rust-bindgen)
* [cc](https://docs.rs/cc/latest/cc/)
* [cmake](https://docs.rs/cmake/latest/cmake/)

## TODOs..

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

# `bindgen`

## `Builder` の設定

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

[Builder](https://docs.rs/bindgen/latest/bindgen/struct.Builder.html) を設定すれば、 `Default` や `PartialEq` を実装してもらうことができます。

### Rustified enum を使う

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

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

```c: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;
```

```rust: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.rs` で `bindgen::Builder` を設定すると、 rustified enum が出力されます:

```rust:build.rs
builder.default_enum_style(bindgen::EnumVariation::Rust {
    non_exhaustive: false,
});
```

```rust: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_text` で `bindgen` の出力にテキストを追加できます。せっかくなので、モジュールの docstring と `#![allow(warnings)]` を追加しておきます。

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

`bindgen` の [user gude](https://rust-lang.github.io/rust-bindgen/) では、 `OUT_DIR` (ユーザから隠れたディレクトリ) に FFI ファイルを出力し、それを `include!` することが勧められています。

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

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

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

## まとめ

`bindgen` の wrapper を用意しました:

```rust: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();
}
```

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

```rust: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](https://doc.rust-lang.org/cargo/reference/build-scripts.html) を参考にします。

## `cc`

* 必要に応じて、コンパイルに必要なライブラリにリンクします:

```rust:build.rs
println!("cargo:rustc-link-lib=framework=Metal");
println!("cargo:rustc-link-lib=framework=MetalKit");
```
 
* コンパイルした他の C ライブラリは、 `cc` が自動的にリンクしてくれます。

## `cmake`

CMake には詳しくないのですが、 `no_build_target` を指定するとコンパイルできたことがあります:

```rust
/// 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` を作られなばりません。可能です:

```rust
// x: &T
let x_ptr: *mut T = x as *const _ as *mut _
```

> ただし C の関数が引数を書き換える場合は、必ず `&mut T` から `*mut T` を作ってください (でなければ UB です) 。

* `*mut c_void` から `&mut T` を作る

````rust
let typed_user_ptr: &mut T = unsafe {&mut *(user_ptr as *const _ as *mut T) }:
````

* メモリ位置を固定する

Callback 関数を使う場合は、ユーザデータのメモリ位置を固定する必要が出てきます。その場合は `Box` (ヒープ) に入れます。

### `enum`

Rustified enum (前述) は、まだ variants の名前が冗長なので、ラッピングするのも手です。

### bitflags

[bitflags](https://docs.rs/bitflags/latest/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](https://doc.rust-lang.org/cargo/reference/features.html) に載っている通り、クレートの feature flag と依存クレートの feature flag を連動させることができます:

```toml: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](https://doc.rust-lang.org/cargo/reference/build-scripts.html#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" }
```

```rust: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")]` のような分岐ができます。

分かりづらい！！！！