最低限のtoolchainでRustとWebAssembly

Overview

最低限のtoolchainでWebAssemblyを活用してみました。cargo web, wasm pack, wasm-bindgen 等色々と便利なツールがありますが、あえて使わずに全部自分でゴリゴリ。便利なツール使う前に苦しさを自分で実感しないと、ツールの仕様でハマった時に自分で解決出来なくなるのではないかと思いついつい低レイヤーに手を出してしまったお話。

Today’s toolchain

  • rustc: Rustのコンパイラ, stableでも使えます。ローカルでは1.40使っています。
  • google-chrome: ブラウザー, 最近のバージョンなら何でもOK(厳密にはこちらを参照: https://caniuse.com/#feat=wasm)

以上となります。cargoも使わず、本当の最低限でwasmを書いてみましょう。

Setup

  1. Install latest stable rust
  2. Install the wasm target
$ rustup target add wasm32-unknown-unknown

Hello world

超簡単な足し算を実装。実際ブラウザー上で動くWebAssemblyに関してはライブラリー関数しか用意できない。今回は本当の最低限のバイナリーを作りたいので、libstd無しで書くため組込の開発に似ていますね。。。適当にパニックも無限ループで対応

#![no_main]    // this file does not contain a main function
#![no_std]     // we will not be using libstd

#[no_mangle]   // we do not want to mangle the symbol when exporting
pub extern fn add(a: i32, b: i32) -> i32 { a + b }

// we need to specify the panic handler because we are not using libstd
use core::panic::PanicInfo;
#[panic_handler]
fn panic(_panic: &PanicInfo<'_>) -> !{ loop {} }
$ rustc --target wasm32-unknown-unknown ./example1.rs 
$ stat --printf="%s\n" ./example1.wasm
651450

あれ?650kB? 確かにコンパイルは上手くいったが、謎に巨大なバイナリーはかれてしまいました。絶対何かおかしいが、これ以上低レイヤーに向かう前に軽くWebAssemblyのバイナリーフォーマットの説明をさせて下さい。

WebAssembly binary format

規格はこちらに綺麗に説明されているので省略しますが、ざっくりとセクションが12種類程あって、その一つが関数を定義するためのセクション(Function section)。Rust Tokyoで使ったスライドを引用していますが以下のような形で、外部からインポートした関数(Import section)、外部にエキスポートする関数(Export section)、そして実際のコード(Code section)、に分かれている。その他、型を定義するType sectionや、メモリー領域を定義するMemory sectionがあったりする。文字列(&strとか)はData sectionに入る。

image.png

Hello worldの分析

$ wasm-objdump -h ./example1.wasm 
[...省略...]
     Type start=0x0000000a end=0x00000025 (size=0x0000001b) count: 5
 Function start=0x00000027 end=0x0000002e (size=0x00000007) count: 6
    Table start=0x00000030 end=0x00000035 (size=0x00000005) count: 1
   Memory start=0x00000037 end=0x0000003a (size=0x00000003) count: 1
   Global start=0x0000003c end=0x00000055 (size=0x00000019) count: 3
   Export start=0x00000057 end=0x00000082 (size=0x0000002b) count: 4
     Elem start=0x00000084 end=0x0000008c (size=0x00000008) count: 1
     Code start=0x0000008f end=0x000001a7 (size=0x00000118) count: 6
     Data start=0x000001a9 end=0x000001fe (size=0x00000055) count: 1
   Custom start=0x00000202 end=0x0002a1d3 (size=0x00029fd1) ".debug_info"
[...省略...]

デバッグっぽいデータが色々あるので、その辺は吹き飛ばせるだろう。本来はFunctionセクションに1つしか関数が無いはずなのに、何故か6つもある。何が入っているのだろう。。。

$ wasm-objdump -x ./example1.wasm 
[...省略...]
Function[6]:
 - func[0] sig=0 <add>
 - func[1] sig=1 <rust_begin_unwind>
 - func[2] sig=1 <_ZN4core3ptr18real_drop_in_place17h812c5b87254dd4a7E>
 - func[3] sig=2 <_ZN4core9panicking5panic17hb5daa85c7c72fc62E>
 - func[4] sig=3 <_ZN4core9panicking9panic_fmt17hdeb7979ab6591473E>
 - func[5] sig=4 <_ZN36_$LT$T$u20$as$u20$core..any..Any$GT$7type_id17hb5877568404f30deE>
Table[1]:
 - table[0] type=funcref initial=3 max=3
Memory[1]:
 - memory[0] pages: initial=17
Global[3]:
 - global[0] i32 mutable=1 - init i32=1048576
 - global[1] i32 mutable=0 <__data_end> - init i32=1048652
[...省略...]

使わない関数が色々と出てくるので、一旦rustc opt-level=1でコンパイルしてみましょう。ちなみに、func[0]#[no_mangle]指定したので関数名がただの<add>になっています。mangleしたままのadd_one()関数を入れてobjdumpするとこんな事になります:

Function[7]:
 - func[0] sig=0 <add>
 - func[1] sig=1 <_ZN8example17add_one17h8f1b54fb7de5b457E>
 - func[2] sig=2 <rust_begin_unwind>
 - func[3] sig=2 <_ZN4core3ptr18real_drop_in_place17h812c5b87254dd4a7E>
$ rustc -C opt-level=1 --target wasm32-unknown-unknown ./example1.rs 
$ stat --printf="%s\n" ./example1.wasm
131
$ wasm-objdump -d ./example1.wasm 
[...省略...]
00006d func[0] <add>:
 00006e: 20 01                      | local.get 1
 000070: 20 00                      | local.get 0
 000072: 6a                         | i32.add
 000073: 0b                         | end

131バイト!圧倒的に小さくなりましたね。分かりやすいですね。ローカル関数はスタックにpushして、i32.add instruction呼べば完了。実はwasmの設計は割とシンプルなstack machineに似た構造になっている。

Call wasm Rust function from JavaScript

さて、早速WebAssemblyのadd関数コンパイル出来たのでウェブに埋め込んでみた。Rustからエキスポートされた関数を直接JSから呼べるので使いやすい。

<html><body><script>
    WebAssembly.instantiateStreaming(
      fetch('example1.wasm'), {})
      .then(wasm => {
        var add_func = wasm.instance.exports.add; // use the exported 'add' function
        var result = add_func(10, 20);
        console.log(result);
      });
</script></body></html>

Call JavaScript from wasm Rust function

もちろん逆もしたいですよね。。。RustからJSの関数呼べないと。これには、WebAssemblyにJSの関数をインポートする必要がある。RustからするとFFI的な扱いで、そのような関数がインポート出来るという前提で書ける。もちろん、JSの関数はunsafe扱いです!

#![no_main]
#![no_std]

#[link(wasm_import_module = "imports")]
extern {
  fn console_log(x: i32);
}

#[no_mangle]
pub extern fn sayFive() {
  unsafe { console_log(5); }
}

use core::panic::PanicInfo;
#[panic_handler]
fn panic(_panic: &PanicInfo<'_>) -> !{ loop {} }

普段Cのライブラリとかとつなぎこみ時はlinkerいじりをしますが、WebAssemblyの場合はJSからWebAssemblyを初期化するときの引数として渡します。

<html><body><script>
    WebAssembly.instantiateStreaming(
      fetch('example2.wasm'), {
        imports: {
          console_log: (x) => console.log(x),
        }
      })
      .then(wasm => {
        var add_func = wasm.instance.exports.add;
        var result = add_func(10, 20);
        console.log(result);
      });
</script></body></html>

最後に

以外と簡単にwasmの関数が作れたが、本気で使うにはもちろんオブジェクトを渡す必要があり、linear memoryを活用しないといけなく、ここまで簡単にはいかないんですよね。wasm_bindgenを活用すれば大分楽になるんですが、wasmは相当デバッギングが難しいのは現状です。そこまでDWARF詳しくないのでこれから勉強してみたいんですが、なんか根本的にWASMのアドレスの考え方とLLDBと相性が悪いらしい。erikmcclureさんのブログ参照

TaigaMerlin
  • TaigaMerlin
  • 半導体からフロントエンドまでのフルスタックエンジニア