Rustのプロジェクトの中でCを使う
この記事ではRustのビルドシステムであるCargoを使って、Cのコードをビルドし、それをRustのコードから呼び出す方法について紹介します。
※記事作成時のRustのバージョンはrustc 1.33.0
です。
この記事で取り扱うRustの知識とクレートは次の通りです。
- build.rs
- Cargo.toml
- クレート
また、この記事に載っているソースコードは全て次のリポジトリに置いてあります。
ryochack/rust-ffi-c-tutorial
事前準備
cargo-editというCargo.tomlへのクレートの追加や削除を楽にしてくれるコマンドをこの記事では使用しています。
必須ではありませんが、とても便利なコマンドなので是非インストールしてみてください。
$ cargo install cargo-edit
目次
次の順番で説明を進めます。
- Cのコードを書く
- ccクレートを使ってCのコードをCargoからビルドする
- bindgenクレートを使ってRustバインディングを生成する
- 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を扱う時の流儀なのでしょう。
CircuitJS1をElectronアプリ化する
電子回路のシミュレーションツールを探していたところ、ブラウザ上で動的にシミュレートするサービス CircuitJS1 を見つけました。
ちょうどQucsという他の電子回路シミュレータを触り始めたところだったのですが、Qucsでは回路に変更を加えるたびにシミュレーションコマンドを意図的に実行しなくてはならないため、少し煩わしく感じていました。
以前に、iCircuitという動的に電子回路のシミュレーションができるiOSアプリを使っていたこともあり、似たような操作感のシミュレータは無いかと探していたところ、このCircuitJS1を見つけました。
CircuitJS1の操作感については、リンク先で実際に触ってみてもらうのがわかりやすいです。 CircuitJS1では、回路に追加した抵抗などによる変化を即座にシミュレーションに反映してくれるだけではなく、作成した回路図のURLリンクなども生成できるため、Markdownなどのテキストメモとの相性もいいと思います。
そんな素晴らしいCircuitJS1なのですが、使っているうちにデスクトップアプリとしてオフラインでも使用したい欲求が湧き上がってきました。
そこで今回、CircuitJS1をElectronアプリ化してみましたので、その手順をまとめてみました。
まず、CircuitJS1のGitHubリポジトリはこちらになります。
pfalstad/circuitjs1: Electronic Circuit Simulator in the Browser
READMEを読むと、Electronアプリとしてビルドするための方法が記載してありますので、それに沿って進めていきます。 おおまかには以下の手順になります。
それでは進めていきましょう。
必要なツールのインストール
以下のツールをそれぞれインストールします。
- Java8 (JDK8)
- Eclipse IDE for Java Developers
- electron
手元の環境ではArchLinuxとパッケージマネージャにyayを使っているので、以下のコマンドでインストールしました。
$ yay -S jdk8-openjdk eclipse-java electron
合わせて、CircuitJS1のコードも適当な場所にgit cloneしておきましょう。
$ git clone https://github.com/pfalstad/circuitjs1.git
EclipseからGWTコンパイル(JavaのコードをJavaScriptに変換)
Electronアプリとして動かすためには、Javaで書かれているコードをJavaScriptに変換しなくてはなりません。(GWT/Electronに詳しくないため、間違った理解をしているかもしれません)
そのためにGWT( Google Web Toolkit )を使って変換を試みます。
GWTプラグインのインストール
- インストールしたEclipseを起動して、適当なワークスペースを設定します。
- Help > Eclipse Marketplace から"GWT"で検索して、GWT Eclipse Pluginをインストールします。
Javaのバージョン確認
GWTはJava 8を使ってコンパイルするのがいいようです。 (僕は最初にJava 11を使ってコンパイルしたのですが、以下のエラーが出てGWTのコンパイルができませんでした。)
Compiling module com.lushprojects.circuitjs1.circuitjs1 [ERROR] Hint: Check that your module inherits 'com.google.gwt.core.Core' either directly or indirectly (most often by inheriting module 'com.google.gwt.user.User')
Eclipseが使用するJavaのバージョンは、 Window > Preferences > Java > Installed JREs で確認できます。 ここでJava 8が設定されていないようなら、設定し直す必要があります。
参考までに、手元の環境で設定されている値は以下になります。
JRE home: /usr/lib/jvm/java-8-openjdk JRE name: java-8-openjdk
CircuitJS1プロジェクトのインポートとGWTでのコンパイル
- Welcome画面から、"Import existing projects"をクリックします。
- Select root directory > Browse... でさきほどcloneしたcircuitjs1/ディレクトリを指定します。
- ツールバーにあるGWTボタンから GWT Compile Project... をクリックし、circuitjs1プロジェクトを選択してコンパイルを実行します。
- コンパイルに成功すると、 war/ディレクトリ下にWEB-INF/とcircuitjs1/が生成されます。
$ cd circuitjs1 $ ls war circuitjs1/ # <- generated circuitjs.html customfunction.html customlogic.html iframe.html shortrelay.php WEB-INF/ # <- generated
Electron用のディレクトリ構成を構築
Electronアプリとして起動させるために、Electron用のディレクトリ構成を作ります。
Application Distribution | Electron
(ここでは例としてWindows/Linux用のディレクトリ構成にします。macOSではディレクトリ構成が異なってきますのでご注意ください。)
$ cd circuitjs1 $ mkdir -p electron/resources $ cp -r app electron/resources/ $ cp -r war electron/resources/app/ $ tree -F -L 2 electron/resources/app/ electron/resources/app/ ├── index.html ├── main.js ├── package.json ├── preload.js ├── renderer.js └── war/ ├── circuitjs1/ ├── circuitjs.html ├── customfunction.html ├── customlogic.html ├── iframe.html ├── shortrelay.php └── WEB-INF/
Electronアプリとして起動
全ての準備が整いました。さっそくCircuitJS1をElectronアプリとして起動してみます。
$ cd circuitjs1 $ electron electron/resources/app/
これでCircuitJS1がElectronアプリとして起動するようになり、オフラインでも気軽に電子回路シミュレータが使えるようになりました。
Rustでプラットフォーム依存の処理を書く
#[cfg(...)] Attribute
Rustでは #[cfg(...)]
アトリビュートを使うことによって、OSやCPUに応じた条件コンパイルを行うことができる。
- Conditional Compilation
- cfgは複数条件指定可能(OR, AND, NOT)
#[cfg(any(unix, windows))]
#[cfg(all(unix, target_pointer_width = "32"))]
#[cfg(not(foo))]
- Attributes - The Rust Reference
例えば、次のように書くことでコンパイルターゲットのOSに応じたhello()
をビルドし、呼び出すことができる。
#[cfg(target_os = "windows")] fn hello() { println!("Hello, I'm Windows!"); } #[cfg(target_os = "linux")] fn hello() { println!("Hello, I'm Linux!"); } #[cfg(target_os = "macos")] fn hello() { println!("hello, I'm MacOS."); } fn main() { hello(); }
モジュールを分ける
各複数プラットフォーム向けの処理が小さい時には、前述のように1つのファイルの中にベタ書きでも問題ないが、コードが膨らんできた場合は各プラットフォーム毎にファイルを分けた方がいいだろう。
そこで、次のようなモジュール構成を考てみる。 nameモジュールを用意し、OS依存部分はnameモジュールの中に隠蔽することを目的とする。
Cargo.toml src/ ├── lib.rs └── name ├── linux.rs ├── macos.rs ├── mod.rs └── windows.rs
name/${os_dependent}.rs
OS依存の処理を name/以下の linux.rs, macos.rs, windows.rsにそれぞれ書いていく。
各モジュールはOSの名前をStringで返すだけのname()
関数を実装する。
name/linux.rs
pub fn name() -> String { "Linux".to_owned() }
name/macos.rs
pub fn name() -> String { "MacOS".to_owned() }
name/windows.rs
pub fn name() -> String { "Windows".to_owned() }
name/mod.rs
各OS依存の処理を他のモジュールから扱えるようにするために name/mod.rs を記述する。
name/mod.rs
#[cfg(target_os = "linux")] pub mod linux; #[cfg(target_os = "linux")] pub use self::linux::*; #[cfg(target_os = "macos")] pub mod macos; #[cfg(target_os = "macos")] pub use self::macos::*; #[cfg(target_os = "windows")] pub mod windows; #[cfg(target_os = "windows")] pub use self::windows::*;
ここでのmod.rsの役割は、コンパイル対象とするモジュールの切り替えと、他のモジュールへの再エキスポートだ。
コンパイル対象とするモジュールの切り替え
これは前述のように #[cfg(target_os = "linux")]
を使ってコンパイル対象のモジュールを指定する。
#[cfg(target_os = "linux")] pub mod linux;
他のモジュールへの再エクスポート
pub use
によって再エクスポートすることによって、他のモジュールからnameのOS依存モジュールを意識せずに使えるようにしている。
#[cfg(target_os = "linux")] pub use self::linux::*;
この記述がないと、nameモジュールのOS依存処理を使う時は常にモジュール名を明確に指定しなくてはならない。 これでは以下のように他のモジュールが常にOS依存を気にしたコードを書かなくてはならず、非常によろしくない。
mod name; let os_name = name::linux::name();
pub use
することによって、OS依存モジュール名を省略して使用することができるようになり、他のモジュールからOS依存を気にせずに使用できるようになる。
// mod.rsでpub use self::linux::*; mod name; let os_name = name::name();
lib.rs
最後に、今まで定義したnameモジュールを使ったlib.rsを次に記述する。
lib.rs
mod name; pub fn greeting() { println!("Hello, I'm {}.", name::name()); } // -> Hello, I'm ${YOUR_OS}. #[cfg(test)] mod tests { use super::*; #[cfg(target_os = "linux")] #[test] fn test_linux() { assert_eq!("Linux", name::name()); } #[cfg(target_os = "macos")] #[test] fn test_linux() { assert_eq!("MacOS", name::name()); } #[cfg(target_os = "windows")] #[test] fn test_linux() { assert_eq!("Windows", name::name()); } }
Linux上でテストを実行すると、name()は"Linux"の文字列を返してくれているのが確認できる。
$ cargo test Compiling hello v0.1.0 (/home/ryo/code/sandbox/rust/scraps/target_os) Finished dev [unoptimized + debuginfo] target(s) in 0.75s Running target/debug/deps/hello-69d1a57a65d70c5b running 1 test test tests::test_linux ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out Doc-tests hello running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
参考
Linux kernel moduleビルドのメモ
ArchLinux(4.17.10)上でのkernel moduleビルドを試したメモ。
Linux kernelのソースコードを入手する
prepare_kernel_source.sh
#!/usr/bin/env sh set -eu KERNEL_VERSION=`uname -r | sed -r 's/([0-9]+\.[0-9]+\.[0-9]+).*$/\1/'` LINUX_VERSION=linux-${KERNEL_VERSION} if [ -e $LINUX_VERSION ]; then echo "$LINUX_VERSION is already exist." exit 1 fi wget https://www.kernel.org/pub/linux/kernel/v4.x/${LINUX_VERSION}.tar.xz tar -xvJf ${LINUX_VERSION}.tar.xz ln -s ${LINUX_VERSION} kernel-source cd ${LINUX_VERSION} make clean && make mrproper # prepare kernel zcat /proc/config.gz > .config make oldconfig make prepare && make scripts
$ cd $HOME $ sh prepare_kernel_source.sh
kernel moduleのソースコードとMakefileを書く
hello.c
#include <linux/module.h> #include <linux/init.h> MODULE_LICENSE("MIT"); static int hello_init(void) { printk(KERN_INFO "Hello, World!\n"); return 0; } static void hello_exit(void) { printk(KERN_INFO "Goodbye...\n"); } module_init(hello_init); module_exit(hello_exit);
obj-m := hello.o KERNEL_SRC_DIR=${HOME}/kernel-source PWD:=$(shell pwd) BUILD_DIR=$(PWD) VERBOSE=0 all: make -C ${KERNEL_SRC_DIR} M=${BUILD_DIR} KBUILD_VERBOSE=${VERBOSE} modules clean: make -C ${KERNEL_SRC_DIR} M=${BUILD_DIR} clean
$ make
-> hello.ko
libelf-dev でのコンパイルエラーが起こったら
コンパイルエラー
$ make make -C /root/kernel-source M=/root/workspace/hello KBUILD_VERBOSE=0 modules make[1]: Entering directory '/root/kernel-source' Makefile:970: *** "Cannot generate ORC metadata for CONFIG_UNWINDER_ORC=y, please install libelf-dev, libelf-devel or elfutils-libelf-devel". Stop. make[1]: Leaving directory '/root/kernel-source' Makefile:8: recipe for target 'all' failed make: *** [all] Error 2
解決方法
$ apt-get install libelf-dev
Install/Remove kernel module
$ sudo insmod hello.ko $ lsmod -> Module Size Used by hello 16384 0 $ dmesg -> [11869.696026] Hello, World!
$ sudo rmmod hello $ lsmod $ dmesg -> [11900.796428] Goodbye...