Rustのプロジェクトの中でCを使う

この記事ではRustのビルドシステムであるCargoを使って、Cのコードをビルドし、それをRustのコードから呼び出す方法について紹介します。
※記事作成時のRustのバージョンはrustc 1.33.0です。

この記事で取り扱うRustの知識とクレートは次の通りです。


また、この記事に載っているソースコードは全て次のリポジトリに置いてあります。
ryochack/rust-ffi-c-tutorial

事前準備

cargo-editというCargo.tomlへのクレートの追加や削除を楽にしてくれるコマンドをこの記事では使用しています。
必須ではありませんが、とても便利なコマンドなので是非インストールしてみてください。

$ cargo install cargo-edit

目次

次の順番で説明を進めます。

  1. Cのコードを書く
  2. ccクレートを使ってCのコードをCargoからビルドする
  3. bindgenクレートを使ってRustバインディングを生成する
  4. bindgenで生成されたコードをよりRustらしいコードでラップする

1. Cのコードを書く

Cのコードは何でもいいのですが、今回はフィボナッチ数列を返すモジュールを用意します。

fibonacci.h

#include <stdint.h>

typedef void* Fibonacci_t;

/* Create fibonacci instance */
Fibonacci_t* fibo_new();
/* Get latest fibonacci value */
uint32_t fibo_next(Fibonacci_t* handle);

fibonacci.c

#include "fibonacci.h"

struct Fibonacci {
    int32_t  n;
    uint32_t prev;
    uint32_t cur;
};

Fibonacci_t* fibo_new() {
    struct Fibonacci *self = calloc(1, sizeof(struct Fibonacci));
    return (void*)self;
}

uint32_t fibo_next(Fibonacci_t* handle) {
    if (!handle) {
        return 0;
    }

    struct Fibonacci *self = (struct Fibonacci*)handle;
    self->n++;
    switch (self->n) {
        case 1:
            return 0;
        case 2:
            self->cur = 1;
            return 1;
        default:
            ;
            uint32_t next = self->prev + self->cur;
            self->prev = self->cur;
            self->cur  = next;
            return next;
    }
}

動作確認のために、fibonacciモジュールを呼び出すmain.cを実装します。

main.c

#include <stdio.h>
#include <stdlib.h>
#include "fibonacci.h"

int main() {
    Fibonacci_t *p = fibo_new();
    for (int i=0; i<20; i++) {
        printf("%d, ", fibo_next(p));
    }
    free(p);
    return 0;
}

実行してみます。

$ cc fibonacci.c main.c
$ ./a.out
0 1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 1597 2584 4181

良さそうですね。 ここまでは単純にCの話でした。

2. ccクレートを使ってCのコードをCargoからビルドする

ここからRustのプロジェクトを作って、その中でCをビルドする仕組みを入れていきます。
まずは、Cargoでクレートパッケージを作ります。 パッケージ名は fibonacci-sys とします。

$ cargo new --lib fibonacci-sys

先ほど書いたCのコードを fibonacci-sys/src/c の下に置きます。(Rustのパッケージの中にCのコードを置く場合の置き場所については、特に決まりはないようです。)

$ mkdir fibonacci-sys/src/c
$ cp fibonacci.[ch] fibonacci-sys/src/c

CargoからCのコードをビルドするために、ccクレートをCargo.tomlの build-dependencies に追加します。 build-dependencies 指定を使うことで、ビルド時にのみ必要なクレートを記述することができます。
Specifying Dependencies - The Cargo Book

$ cd fibonacci-sys
$ cargo add cc --build

ここでCargo.tomlは次のようになるはずです。 build-dependencies にccクレートが追加されているのがわかります。

[package]
name = "fibonacci-sys"
version = "0.1.0"
edition = "2018"

[dependencies]

[build-dependencies]
cc = "1.0.35"

build.rsというビルドスクリプトで、ビルド時にCのコードの src/c/fibonacci.c もコンパイルするようにしてみます。
(build.rs については、Build Scripts - The Cargo Book で詳しい説明がされています。)

build.rs

fn main() {
    // Cのコードをコンパイルします。
    //    ccクレートはcargoにコンパイルのリンクオプションを自動的に追加します。
    //    これの意味することは、本来Cのコードをビルドした後にRustのコードにリンクするために必要な
    //    以下の記述をbuild.rsから省略できるということです。
    //     println!("cargo:rustc-link-search=native={}", env::var("OUT_DIR").unwrap());
    //     println!("cargo:rustc-link-lib=fibonacci");
    cc::Build::new()
        .warnings(true)
        .flag("-Wall")
        .flag("-Wextra")
        .file("src/c/fibonacci.c")
        .include("src/c")
        .compile("libfibonacci.a");
}

今のディレクトリ構造は次のようになっています。

$ tree
fibonacci-sys
├── build.rs
├── Cargo.toml
└── src
    ├── c
    │   ├── fibonacci.c
    │   └── fibonacci.h
    └── lib.rs

それではビルドしてみます。

$ cargo build
    Updating crates.io index
   Compiling cc v1.0.35
   Compiling fibonacci-sys v0.1.0 (/home/ryo/code/sandbox/rust/ffi-fibonacci/fibonacci-sys)
    Finished dev [unoptimized + debuginfo] target(s) in 5.17s

ビルドが無事通ったようです。fibonacci.cからライブラリが生成されているはずなので探してみます。

$ find . -name "libfibonacci.a"
./target/debug/build/fibonacci-sys-147411519e93d7f0/out/libfibonacci.a

見つかりました。これでCargoからCのコードがビルドできていることが確認できました。

3. bindgenクレートを使ってRustバインディングを生成する

CargoからCのコードをビルドできることは確認できましたが、RustからCのコードを呼び出すためにはFFIを使ってバインディングを実装する必要があります。
自分で実装する方法もありますが、ここではCのヘッダファイルからRustのバインディングを自動生成してくれるbindgenクレートを使います。

bindgenクレートを Cargo.toml に追加します。

$ cargo add bindgen --build

bindgenクレート追加後の Cargo.toml は次のようになります。

[package]
name = "fibonacci-sys"
version = "0.1.0"
edition = "2018"

[dependencies]

[build-dependencies]
cc = "1.0.35"
bindgen = "0.49.0"

ビルド時にbindgenを実行するように builld.rs を修正します。

use std::env;
use std::path::PathBuf;

fn main() {
    // Cのコードをコンパイルします。
    //    ccクレートはcargoにコンパイルのリンクオプションを自動的に追加します。
    //    これの意味することは、本来Cのコードをビルドした後にRustのコードにリンクするために必要な
    //    以下の記述をbuild.rsから省略できるということです。
    //     println!("cargo:rustc-link-search=native={}", env::var("OUT_DIR").unwrap());
    //     println!("cargo:rustc-link-lib=fibonacci");
    cc::Build::new()
        .warnings(true)
        .flag("-Wall")
        .flag("-Wextra")
        .file("src/c/fibonacci.c")
        .include("src/c")
        .compile("libfibonacci.a");

    // bindgenにfibonacci.hの場所を伝えます。
    let bindings = bindgen::Builder::default()
        .header("src/c/fibonacci.h")
        .generate()
        .expect("Unable to generate bindings!");

    let out_path = PathBuf::from(env::var("OUT_DIR").unwrap());
    // bindgenによってbindings.rsという名前でRustのバインディングコードが生成されます。
    bindings
        .write_to_file(out_path.join("bindings.rs"))
        .expect("Couldn't write bindings!");
}

ビルドしてbindgenが bindings.rs を生成してくれるかを確認してみます。

$ cargo build

$ find . -name "bindings.rs"
./target/debug/build/fibonacci-sys-5c43ba6fb0edf06f/out/bindings.rs

$ cat ./target/debug/build/fibonacci-sys-5c43ba6fb0edf06f/out/bindings.rs
...
pub type Fibonacci_t = *mut ::std::os::raw::c_void;
extern "C" {
    pub fn fibo_new() -> *mut Fibonacci_t;
}
extern "C" {
    pub fn fibo_next(handle: *mut Fibonacci_t) -> u32;
}

うまくbindgenがコードを生成してくれているようです。

4. bindgenで生成されたコードをよりRustらしいコードでラップする

bindgenはCのヘッダからRustのコードを生成してくれる非常に便利なツールです。しかし、bindgenが生成するコードはアンセーフであり、またCライクなコードになっているためRustらしいコードではありません。 そのため、Rustのコードから使いやすくするためにラッパーライブラリを作る必要があります。

この章で目指すディレクトリ構造は次の通りです。

$ tree
.
├── Cargo.toml
├── fibonacci
│   ├── Cargo.toml
│   └── src
│       └── lib.rs
└── fibonacci-sys
    ├── build.rs
    ├── Cargo.lock
    ├── Cargo.toml
    └── src
        ├── c
        │   ├── fibonacci.c
        │   └── fibonacci.h
        └── lib.rs

今までの作業は fibonacci-sys パッケージ下で行ってきました。 この章では、 fibonacci-sys パッケージと同じ階層に fibonacci パッケージを作り、 fibonacci-sys パッケージと fibonacci パッケージを親ディレクトリの Cargo.toml でワークスペースとしてまとめるという方針で進めます。
パッケージの命名ルールについては「補足: bindgenで生成したコードのクレートの分け方と名前付けについて」で説明しています。

fibonacci-sys のbindgenが生成したコードを外部に公開するために、 fibonacci-sys/src/lib.rs を次のように実装します。

#![allow(non_upper_case_globals)]
#![allow(non_camel_case_types)]
#![allow(non_snake_case)]
#![allow(dead_code)]

include!(concat!(env!("OUT_DIR"), "/bindings.rs"));

fibonacci-sys にtarget/が残るのは気持ちが悪いので、cargo cleanできれいにしておきましょう。

$ # @fibonacci-sys directory
$ cargo clean

1つ上の親ディレクトリに移動して、fibonacci パッケージの生成と Cargo.toml でのワークスペースを定義を行います。
Cargo Workspaces - The Rust Programming Language

ワークスペースを定義することで、 fibonacci-sys パッケージと fibonacci パッケージを同じワークスペースとしてまとめることができます。

$ cd ..
$ cargo new --lib fibonacci
$ cat << eof >> Cargo.toml
[workspace]
members = ["fibonacci-sys", "fibonacci"]
eof

それでは、たった今作ったばかりの fibonacci パッケージにRustのラッパーを実装していきます。
fibonacci パッケージの Cargo.toml に依存パッケージを追加していきます。

$ cd fibonacci
$ cargo add fibonacci-sys --path ../fibonacci-sys
$ cargo add libc

fibonacciパッケージの Cargo.toml は次のようになります。

[package]
name = "fibonacci"
version = "0.1.0"
edition = "2018"

[dependencies]
fibonacci-sys = { path = "../fibonacci-sys" }
libc = "0.2.51"

これが最後の仕上げです。
fibonacci-sys のbindgen生成コードをラップするライブラリを fibonacci/src/lib.rs に実装します。

use fibonacci_sys as fibo;
use libc;

pub struct Fibonacci {
    handle: *mut fibo::Fibonacci_t,
}

impl Fibonacci {
    pub fn new() -> Self {
        Self {
            handle: unsafe { fibo::fibo_new() }
        }
    }

    pub fn next(&mut self) -> u32 {
        unsafe { fibo::fibo_next(self.handle) }
    }
}

impl Drop for Fibonacci {
    fn drop(&mut self) {
        unsafe {
            libc::free(self.handle as *mut core::ffi::c_void);
        }
        println!("free()");
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    #[test]
    fn fibonacci_test() {
        let expects: [u32; 10] = [0, 1, 1, 2, 3, 5, 8, 13, 21, 34];
        let mut fibo = Fibonacci::new();
        for &e in expects.iter() {
            assert_eq!(fibo.next(), e);
        }
    }
}

最後にテストを実行してみましょう。
このテストが通れば、 ラッパーライブラリの fibonacci クレートから fibonacci-sys クレートのバインディングライブラリを通してCのコードが正常に実行できていることになります。

$ cargo test fibonacci_test
    Finished dev [unoptimized + debuginfo] target(s) in 0.09s
     Running /home/ryo/code/sandbox/rust/ffi-fibonacci/target/debug/deps/fibonacci-53505d427ab47c43

running 1 test
test tests::fibonacci_test ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

   Doc-tests fibonacci

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

意図通り動いているようです。

若干の手間はありますが、ccやbindgenといったクレートのおかげで簡単にCのモジュールをRustのプロジェクトの中で使うことができました。

補足: bindgenで生成したコードのクレートの分け方と名前付けについて

この記事では、bindgenで生成したコードを含むクレートパッケージをfibonacci-sys、fibonacci-sysをラップしてRustらしいコードを提供するクレートパッケージをfibonacciとして分離しています。
これは次の記事を参考にしたやり方です。おそらくこれがbindgenを扱う時の流儀なのでしょう。