TL;DR

  • FFI で C の構造体や関数を使うには、対応する Rust の宣言が要る。両者が一致しているかは、ツールチェーンでは確かめられない。
  • そこで bindgen で C のコードから Rust の宣言を生成し、CI で再生成して一致を確かめよう。
  • C++ とより滑らかに連携したいなら、cxx を検討しよう。

内容

bindgen が必要な理由

FFI 境界を越えて C の構造体や関数を使うには、それに対応する Rust の宣言が必要である。Rust と C のコードは一致していなければならないが、ツールチェーンはそれを確かめない(Rust から C の関数を呼び出す)。そのため、bindgen を使って C のコードから Rust の宣言を生成しよう。生成を自動化すれば、CI で再生成して既存の生成コードと一致するかを確かめられる(項目32)。

たとえば、次の C のヘッダがあるとする。

/* File lib.h */
#include <stdint.h>
 
typedef struct {
    uint8_t byte;
    uint32_t integer;
} FfiStruct;
 
int add(int x, int y);
uint32_t add32(uint32_t x, uint32_t y);

これに bindgen をかける。--no-layout-tests はメモリ配置が一致するかのテストを生成しない指定、--allowlist-function と --allowlist-type は Rust に取り込む関数名(正規表現)と型名の指定である。

bindgen --no-layout-tests \
  --allowlist-function="add.*" \
  --allowlist-type=FfiStruct \
  -o src/generated.rs \
  lib.h

次のコードが生成される。

/* automatically generated by rust-bindgen 0.71.1 */
 
#[repr(C)]
#[derive(Debug, Copy, Clone)]
pub struct FfiStruct {
    pub byte: u8,
    pub integer: u32,
}
unsafe extern "C" {
    pub fn add(x: ::std::os::raw::c_int, y: ::std::os::raw::c_int) -> ::std::os::raw::c_int;
}
unsafe extern "C" {
    pub fn add32(x: u32, y: u32) -> u32;
}

Rust 側では include!("generated.rs"); で取り込む。

段階的に公開する

--allowlist-function や --allowlist-type を使うと、C のライブラリを Rust に段階的に公開できる。xyzzy というライブラリなら、クレートを 2 つに分ける。xyzzy-sys クレートは bindgen が生成したコードだけを持ち、使うには必ず unsafe が要る。xyzzy クレートは unsafe なコードをカプセル化し、安全で Rust らしい使い方を提供する(項目16)。

C++ の場合

bindgen は C++ のアイテムも一部扱えるが、制約が多い。C++ と Rust をよりスムーズに連携させたいなら、cxx クレートを検討しよう。cxx にも制限はあるが、Rust と C++ の双方のコードを自動生成する方式を取っている。

キーワード