6-3. ケーススタディ Zephyr binding

Rust Embedded devices WGでもRTOSとRustとのインテグレーションはissue #62で議論中です。

ここでは、Cで作られたRTOSであるZephyrをターゲットに、RTOSとのインテグレーションを実験してみます。ZephyrのAPIを利用して、Rustからprintln!マクロを使って、コンソールに文字を出力します。また、RTOSのような複雑なCプロジェクトとのインテグレーションが困難な理由を考察します。

そのために、次のことができるようにします。

  1. CからRustのAPIを呼び出す
  2. RustからZephyrのAPIを呼び出す

双方のバインディングは、cbindgenおよびbindgenを用いて自動生成します。ここで掲載する方法には、まだまだ改善の余地があることに注意して下さい。

次のコードが動くようにします。

#include <rustlib.h>

int main(void) {
	rust_main();
}

まずC言語から、Rustのrust_main関数を呼び出します。バインディング用のヘッダファイルrustlib.hcbindgenで自動生成します。

#[no_mangle]
pub extern "C" fn rust_main() {
    println!("Hello {}", "Rust");
}

Rustにはprintln!マクロを実装します。このprintln!マクロは、ZephyrのAPIを利用してコンソールに文字列を出力します。Zephyr APIのバインディングは、bindgenで自動生成します。

最終的に、C (main) → Rust (rust_main) → C (Zephyr API)というコールグラフになります。

環境

これから示すインテグレーション例を試すために必要な環境です。カッコ内は、著者が試したバージョンです。

  • Rust (stable 1.35.0)
  • cbindgen (0.8.3)
  • bindgen (0.49.0)
  • Zephyr v.1.14
  • Zephyr SDK (0.10.0)
  • west (v0.5.8)
  • qemu-system-arm (2.11.1)

CからRustのAPIを呼び出す

下のRust関数に対するヘッダファイルを作成します。

#[no_mangle]
pub extern "C" fn rust_main() { /* ... */ }

ビルドスクリプトでも容易に生成できますが、今回はZephyrとのインテグレーション上、Makefileを使う必要があるため、Makefile内で次のコマンドを呼び出します。

cbindgen src/lib.rs -l c -o lib/rustlib.h

これで、次のヘッダファイルが生成されます。

cat hello/lib/rustlib.h
#include <stdarg.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdlib.h>

void rust_main(void);

このヘッダファイルをCでインクルードします。

#include <rustlib.h>

int main(void) {
	rust_main();
}

今回は非常に簡潔です。構造体を引数にしたり、ヒープメモリの管理などリソース管理が加わると、より複雑になります。

RustからZephyrのAPIを呼び出す

こちらの方が難易度が高いです。下準備も色々と必要です。

一般的なライブラリと異なり、多くの組込みOSでは必要最小限の機能だけを組み込んでバイナリを形成します。ビルド時のコンフィギュレーション次第で、ユーザーアプリケーションが利用できるAPIが増減します。そのため、全てのコンフィギュレーションで利用可能なバインディングを作ることは、難しいです。このケーススタディでも、固定のコンフィギュレーションに対してバインディングを作成します。

今回、Zephyrは、デフォルトのZephyr kernel APIに加えて、newlibという組込み用標準CライブラリのAPIを有効化します。このnewlibを有効化することにより、C標準ライブラリの一部がZephyrアプリケーションで利用可能になります。

cat prj.config
# General config
CONFIG_NEWLIB_LIBC=y

この設定により、次のようなC標準ライブラリAPIが利用可能になります。

  • I/O
    • printf, fwrite, etc
  • 文字列
    • strtoul, atoi, etc
  • メモリ
    • malloc, free, etc

これらAPIのバインディングを自動生成しつつ、Rustっぽく使えるようにラッピングしていきます。

バインディングの自動生成

まず、第一関門です。ここで難しい点は、2つあります。

  1. OSのコンフィギュレーションによって利用できるAPIが異なる
  2. 一部APIがZephyrのビルドシステムで自動生成するヘッダに依存している

上記理由から、一度ターゲットとするZephyrをビルドした後で、bindgenを使用することにしました。まず、空のアプリケーションを用意して、Zephyrをビルドします。

cat src/main.c
int main(void) {
	return;
}
# Zephyrプロジェクトのディレクトリ
source zephyr-env.sh

# ケーススタディプロジェクトのディレクトリ
west build -b cortex_qemu_m3 hello

これで、build/zephyr/include.generatedに必要なヘッダファイルが生成されます。Zephyrの環境変数を利用しながら、bindgenでバインディングを生成します。

$ bindgen --use-core --ctypes-prefix cty zephyr-sys/headers/bindings.h -o zephyr-sys/src/bindings.rs -- -I${ZEPHYR_BASE}/include -I${ZEPHYR_BASE}/arch/arm/include -I./build/zephyr/include/generated -m32

前述の通り、stdクレートにあるFFI型は利用できないため、ctyクレートを使います。--ctypes-prefix ctyの部分です。

バインディングを生成するためのヘッダファイルは以下の通りです。

cat zephyr-sys/headers/bindings.h
#include <autoconf.h>
#include <stdio.h>

Zephyrのビルドシステムで自動生成されるヘッダautoconf.h内には、アーキテクチャ依存のマクロ定義など、重要な定義が数多く含まれます。Zephyrのヘッダファイルおよびソースファイルは、このautoconf.hに含まれるマクロが定義されていることが前提になっています。そこで、バインディング作成時にも、まず最初にautoconf.hをインクルードしています。

--以降 (-I${ZEPHYR_BASE}から後) のオプションは、clangに与えるオプションです。-Iでインクルードパスを、-m32でターゲットアーキテクチャが32ビットであることを指示しています。

これで、bindings.rsを得ます。例えば、printfのバインディングは、次のように生成されています。

extern "C" {
    pub fn printf(fmt: *const cty::c_char, ...) -> cty::c_int;
}

このbindings.rsをクレートとして利用できるようにします。次のlib.rsを作成します。

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

include!("bindings.rs");

Rustとしてはあくまでのno_stdな環境となるため、#![no_std]アトリビュートが必要です。バインディングを作成したCソースファイルは、Rustの命名規則に沿っていません。そこで、Rustコンパイラのlintで警告が出ないように、命名規則のlintルールを除外しています。

lib.rsbindings.rszephyr-sysクレートとしてまとめれば、バインディングの生成は完了です。

ラッピングクレートの作成

zephyr-sysクレートのバインディングは、C APIをそのまま変換しただけなので、Rustらしい安全で使いやすいAPIになっていません。例えば、printfをRustから呼び出そうとすると、次のようなコードになります。

    unsafe {
        zephyr::printf(b"Hello from %s\0".as_ptr() as *const cty::c_char,
                       b"Rust\0".as_ptr());
    }

お世辞にも使いやすいとは言えません。その上、文字列をナル文字 (\0) で終端し忘れると、未定義動作に突入します。

そこで、zephyr-sysをラッピングして、Rustらしく使えるAPIを実装します。ここが、Cとのインテグレーションで一番大変なところです。

ここでは、Zephyrのnewlib APIを使って、アプリケーションから安全に利用できるprintln!マクロを作ります。ベースは、print!マクロで実装したマクロと同じです。異なる点は、fmt::Writeトレイトのwrite_strメソッドの実装で、Zephyrのfwriteを呼び出す点です。

/// Pseudo writer which uses Zephyr `fwrite` API.
/// Because `fwrite` does not guarantee its atomicity, this wrapper
/// does not provide any lock mechanism.
pub struct DebugWriter {}

impl fmt::Write for DebugWriter {
    fn write_str(&mut self, s: &str) -> fmt::Result {
        // safe: `fwrite` does not need to guarantee the atomicity.
        unsafe {
            zephyr_sys::fwrite(s.as_ptr() as *const cty::c_void, s.len(), 1, stdout_as_ptr_mut());
        }
        Ok(())
    }
}

今回の実装では、マルチスレッドで動作した場合のアトミック性については考慮しません。Zephyrのfwrite API自体がアトミック性を保証していないため、Rust側で工夫をしても、システム全体のアトミック性が保証できないためです。

もし、C APIが (例えばMutexなどにより) アトミック性を保証する仕組みを持つ場合、DebugWriterを空実装にせず、然るべきロック機能を持たせると良いでしょう。

今回、fwriteを用いてprintln!マクロを実装した理由は、ランタイムコストを減らすためです。別解として、printfを用いる実装が考えられますが、文字列のフォーマットはRust処理系で安全性が保証されているため、Cのフォーマットを使う理由がありません。それどころか、printfのフォーマット処理分、余分なランタイムコストがかかります。

Rustのstrは、バイト数を取得するlenメソッドを備えているため、stdoutに対して、指定バイト数書き込む実装にすると、ランタイムコストが少なくなります。

最後になりますが、このクレートをzephyrクレートとします。Cargo.tomlファイルにzephyr-sysクレートへの依存関係を追加します。

[dependencies]
cty = "0.2.0"
zephyr-sys = { path = "../zephyr-sys" }

アプリケーション作成

アプリケーションは、上記で作成したprintln!マクロを呼び出すだけです。

use zephyr::{print, println};

#[no_mangle]
pub extern "C" fn rust_main() {
    println!("Hello {}", "Rust");
}

このクレートをhelloクレートとして、zephyrクレートへの依存を追加します。

[dependencies]
cty = "0.2.0"
zephyr = { path = "../zephyr" }

このクレートは、staticライブラリとしてビルドし、Zephyrとリンクします。

[lib]
name = "rustlib"
crate-type = ["staticlib"]

このクレートは、Makefileを使ってビルドします。これは主にZephyrとのインテグレーション上の理由です。おおよそ、次のコマンドが実行されるようにMakefileを作成します。

cargo build
cargo objcopy -- --weaken lib/librustlib.a
cbindgen src/lib.rs -l c -o lib/rustlib.h

cargoでプロジェクトのstaticライブラリを作成し、objcopyでシンボルをweakにします。これは、Rustコンパイラビルトインのmemcopymemsetといった関数が、Zephyrのシンボルと衝突してしまうためです。

余談ですが、現状、Rustにはビルド後のバイナリやライブラリを操作するためのポストビルドスクリプトの仕組みがありません。 今回のように、objcopyなどを使いたい場合には、Makefileなど外部ビルドシステムに依存しなければなりません。

また、このクレートはCから呼び出されるため、Cのバインディングを生成します (先述の通り)。

ビルドシステムへのインテグレーション

最後の仕事です。ZephyrはビルドシステムにCMakeを採用しています。Zephyrのビルドプロセス中で外部ビルドシステムを呼び出し、ライブラリをリンクする方法が確立されています。

詳細な説明は省略しますが、cmakeExternalProjectを用いて、CMakeLists.txtを次の通り記述します。

# Include External Rust Library
include(ExternalProject)

set(rust_prj hello)
set(rust_src_dir   ${CMAKE_CURRENT_SOURCE_DIR}/${rust_prj})
set(rust_build_dir ${CMAKE_CURRENT_BINARY_DIR}/${rust_prj})

ExternalProject_Add(
  rust_project                 # Name for custom target
  PREFIX     ${rust_build_dir} # Root dir for entire project
  SOURCE_DIR ${rust_src_dir}
  BINARY_DIR ${rust_src_dir}   # This particular build system is invoked from the root
  CONFIGURE_COMMAND ""         # Skip configuring the project, e.g. with autoconf
  BUILD_COMMAND make
  INSTALL_COMMAND ""      # This particular build system has no install command
  BUILD_BYPRODUCTS ${rust_src_dir}/lib/librust.a
)

add_library(rust_lib STATIC IMPORTED GLOBAL)
add_dependencies(
  rust_lib
  rust_project
)

set_target_properties(rust_lib PROPERTIES IMPORTED_LOCATION ${rust_src_dir}/lib/librust.a)
set_target_properties(rust_lib PROPERTIES INTERFACE_INCLUDE_DIRECTORIES ${rust_src_dir}/lib)

target_link_libraries(app PUBLIC rust_lib)

今回は、先の手順で作成したMakefileを呼び出す形にしました。これでようやく全ての準備が整いました。

動作確認

次のCアプリケーションを作成して、src/main.cとします。

#include <stdio.h>

int main(void) {
	rust_main();
}

次のコマンドで今回のプロジェクトをビルドし、QEMUで実行できます。

mkdir build && cd $_
cmake -GNinja -DBOARD=qemu_cortex_m3 ..
ninja run

実行すると無事にHello Rustが表示されます。

To exit from QEMU enter: 'CTRL+a, x'[QEMU] CPU: cortex-m3
qemu-system-arm: warning: nic stellaris_enet.0 has no peer
***** Booting Zephyr OS zephyr-v1.14.0 *****
Hello Rust

考察

この通り、RTOSのような複雑なCプロジェクトとRustとのインテグレーションには、特有の困難さがあります。まず、ラッピングするAPIの数が膨大です。今回は、単一機能に対してのみラッピングAPIを作成しましたが、これをRTOSがアプリケーションに提供するAPIの数分こなさなければなりません。

加えて、RTOSではターゲットシステムごとに、アプリケーションが利用できるAPIが増減します。この要素をどのように統一的に扱うか、を解決する必要があります。今後のコミュニティの動きに注目しましょう。

コラム〜マイコン上でRustで書いたWASMアプリケーションが動く!?〜

2019年5月、WebAssembly Micro Runtimeというマイコン上で動作するWASMランタイムが公開されました。このWASMランタイムは、Zephyr上で動かすことができます。

それほど苦労せずに、256KBのRAMが搭載されているマイコン上でRustのアプリケーションを実行できました。アプリケーション実行までの簡単な手順をWebAssembly Micro RuntimeでRustアプリをマイコンで動かす!で公開しています。ランタイムの性能が気になるところですが、WASMが動くようになればターゲットアーキテクチャを気にしなくてもRustアプリケーションが動かせるようになるため、今後もWASM Micro Runtimeに注目しましょう。

参考

freertos.rs