WebAssembly Micro RuntimeでRustのアプリを動かす!

はじめに

WebAssembly Micro Runtimeでは、64KB程度のRAMが搭載されたマイコンであれば、wasmアプリケーションを動かすことができます。

github.com

Rustはwasmをターゲットにビルドできます。

後は、わかるね?

アプリケーションの作り方

WebAssembly Micro Runtimeのサンプルコードを修正します。 下のようにおもむろにwasmのバイナリが埋め込まれているので、これを自作のバイナリに差し替えます。

|> src/test_wasm.h

unsigned char wasm_test_file[] = { 0x00, 0x61, 0x73, 0x6D, 0x01, 0x00, 0x00,
        0x00, 0x00, 0x0D, 0x06, 0x64, 0x79, 0x6C, 0x69, 0x6E, 0x6B, 0xC0, 0x80,
        0x04, 0x04, 0x00, 0x00, 0x01, 0x13, 0x04, 0x60, 0x01, 0x7F, 0x00, 0x60,
// ...
        0x70, 0x69, 0x6E, 0x67, 0x55, 0x52, 0x4C, 0x0E, 0x61, 0x2E, 0x6F, 0x75,
        0x74, 0x2E, 0x77, 0x61, 0x73, 0x6D, 0x2E, 0x6D, 0x61, 0x70 };

将来的にはよりスマートな方法が必要ですが、当面は良いでしょう。

Rustツールチェイン

wasmのターゲットをインストールするだけです。

rustup target add wasm32-unknown-unknown

wasmバイナリ作成

cargo new hello_wasm --lib

|> Cargo.toml

[package]
name = "hello_wasm"
version = "0.1.0"
authors = ["tomoyuki-nakabayashi <tomo.wait.for.it.yuki@gmail.com>"]
edition = "2018"

[dependencies]
cty = "0.2.0"

[lib]
crate-type = ["cdylib"]

crate-typeはcdylibになるようです。 wasmバイナリのサイズを小さくするために、#![no_std]アトリビュートを使用するため、stdC言語の型定義が使えません。 そこで、ctyクレートを使って、C言語の型を表現します。

WebAssembly Micro Runtimeの方でprintfのbindingが用意されている(要確認)ので、一旦FFIを手動で用意して、呼び出します。

|> src/lib.rs

#![no_std]
use cty;

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

#[no_mangle]
pub extern "C" fn main() -> i32 {
    unsafe {
        printf(b"Hello from Rust\0".as_ptr() as *const cty::c_char)
    }
}

use core::panic::PanicInfo;
#[panic_handler]
#[no_mangle]
pub extern "C" fn panic(_: &PanicInfo) -> ! {
    loop {}
}
cargo build --target wasm32-unknown-unknown --release

この時点で約300バイトと十分に小さいですが、念の為、wasm-stripを使ってさらにバイナリを小さくします。

$ ls -lha target/wasm32-unknown-unknown/release/
-rwxr-xr-x 2 tomoyuki tomoyuki  306 Jun 15 14:12 hello_wasm.wasm
wasm-strip target/wasm32-unknown-unknown/release/hello_wasm.wasm

strip後のサイズです。180バイトまで小さくなります。

$ ls -lha target/wasm32-unknown-unknown/release/
-rwxr-xr-x 2 tomoyuki tomoyuki  180 Jun 15 14:13 hello_wasm.wasm

wasm-objdumpを使って逆アセンブルすると、次のような感じです。

$ wasm-objdump -x target/wasm32-unknown-unknown/release/hello_wasm.wasm 

hello_wasm.wasm:        file format wasm 0x1

Section Details:

Type[3]:
 - type[0] (i32, i32) -> i32
 - type[1] () -> nil
 - type[2] () -> i32
Import[1]:
 - func[0] sig=0 <env.printf> <- env.printf
Function[2]:
 - func[1] sig=1
 - func[2] sig=2 <main>
Table[1]:
 - table[0] type=funcref initial=1 max=1
Memory[1]:
 - memory[0] pages: initial=17
Global[3]:
 - global[0] i32 mutable=1 - init i32=1048576
 - global[1] i32 mutable=0 <__heap_base> - init i32=1048592
 - global[2] i32 mutable=0 <__data_end> - init i32=1048592
Export[4]:
 - memory[0] -> "memory"
 - global[1] -> "__heap_base"
 - global[2] -> "__data_end"
 - func[2] <main> -> "main"
Code[2]:
 - func[1] size=2
 - func[2] size=16 <main>
Data[1]:
 - segment[0] size=16 - init i32=1048576
  - 0100000: 4865 6c6c 6f20 6672 6f6d 2052 7573 7400  Hello from Rust.

hexdumpを使って、適当にC言語に埋め込める形式を出力します。

$ hexdump wasm.wasm -e '16/1 "0x%02x, "' -e '"\n"'
0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, 0x01, 0x0e, 0x03, 0x60, 0x02, 0x7f, 0x7f, 0x01,
0x7f, 0x60, 0x00, 0x00, 0x60, 0x00, 0x01, 0x7f, 0x02, 0x0e, 0x01, 0x03, 0x65, 0x6e, 0x76, 0x06,
...

これを、WebAssembly Micro Runtimeのサンプルコードに埋め込みます。

|> src/test_wasm.h

unsigned char wasm_test_file[] = {
0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, 0x01, 0x0e, 0x03, 0x60, 0x02, 0x7f, 0x7f, 0x01,
0x7f, 0x60, 0x00, 0x00, 0x60, 0x00, 0x01, 0x7f, 0x02, 0x0e, 0x01, 0x03, 0x65, 0x6e, 0x76, 0x06,
// ...
};

実行

qemu_x86をターゲットに実行します。 OSはZephyrです。

$ ninja run
SeaBIOS (version rel-1.12.0-0-ga698c8995f-prebuilt.qemu.org)
Booting from ROM..***** Booting Zephyr OS zephyr-v1.14.0 *****
Hello from Rust

無事動きました! と言いたいところですが、少しごまかしが入っています。

data segmentの置き場所問題

どうもRustでwasmをターゲットにビルドすると、データセグメントが0x10_0000 (1MB)から始まる領域に置かれるようです。

$ wasm-objdump -x target/wasm32-unknown-unknown/release/hello_wasm.wasm 
...
Data[1]:
 - segment[0] size=16 - init i32=1048576
  - 0100000: 4865 6c6c 6f20 6672 6f6d 2052 7573 7400  Hello from Rust.

一方(?)、サンプルアプリケーションの方では、wasm用のヒープ領域を512KB確保しています。

|> src/main.c

static char global_heap_buf[512 * 1024] = { 0 };

この状態でアプリケーションを実行するとメモリ確保が失敗します。

Booting from ROM..***** Booting Zephyr OS zephyr-v1.14.0 *****
Instantiate memory failed: allocate memory failed.

どうもランタイムでは、データセグメントも含めて1つのメモリ領域に割り当てているように見えます。

データセグメントが1MBから始まるので、ヒープ領域を2MB確保すると、上述の通り、アプリケーションが動作します。 intelさんとこは8MBもRAMが載っていてようござんすねぇ

|> src/main.c

static char global_heap_buf[2048 * 1024] = { 0 };

ということで、後は、Rustでサンプルアプリケーションをビルドする際にデータセグメントのアドレスを指定できれば、 64KB程度のRAMが載ったマイコンボードで動きそうです!