wasm-packで作ったLife GameをレトロPC(X68000)で動かす
今回はRust and WebAssemblyのTutorial[1]にあるLife GameをX68000[2]で動かします。
この記事ではwasm-pack[3]を使ってビルドしたwasmバイナリを以前の記事[4]で用意したwasm2c
でX68000の実行形式に変換して実行ます。wasm-packはブラウザ上でjavascriptとインタラクションしながら動くWebAssemblyバイナリをRust言語で開発するためのツールです。wasmとjavascriptとの間のやり取りを参考にwasmとC言語とのやり取りを作成していくことになります。
Hello World
まずは手始めにTutorial[1:1]の最初に出てくるHello WorldをX68000で実行できるようにしていきます。
この節で対象とするのは以下のRustコードになります。
mod utils;
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
extern "C" {
fn alert(s: &str);
}
#[wasm_bindgen]
pub fn greet(name: &str) {
alert(&format!("Hello, {}!", name));
}
今回はC言語で書いたmain
関数からRustのgreet
関数に文字列を渡して、C言語のalert
関数が実行されるという動作を目指します。簡単なコードですがwasmとC言語の間でどのように文字列のようなデータ構造をやり取りするのか確認するのにちょうどよい例題になると思います。
Tutorialのとおりに進めてwasm-pack build
でpkg
ディレクトリに出来上がったwasm_game_of_life_bg.wasm
が変換対象です。
wasm2c
[5]のワークディレクトリをセットアップしてここにwasmファイルをコピーします。
ワークディレクトリにあるmakefile
[6]のビルド対象に下記のようにwasmファイルを追加して準備完了です。
# *.wasm
WASM_BIN = wasm_game_of_life_bg.wasm
# *.rsソースファイル
RS_SRCS =
モジュール名の取り扱い
ビルド環境の準備ができたのでここからはトライアンドエラーで進めていきます。
makeを実行すると早速下記のエラーで止まってしまいます。
wasm_game_of_life_bg.c:176:6: error: expected identifier or ‘(’ before ‘.’ token
176 | void ./wasm_game_of_life_bg.js___wbg_alert_d3b6e8db27c82dfa(uint32_t, uint32_t);
| ^
wasm_game_of_life_bg.c: In function ‘f20’:
wasm_game_of_life_bg.c:7089:9: error: expected expression before ‘.’ token
7089 | ./wasm_game_of_life_bg.js___wbg_alert_d3b6e8db27c82dfa(l15, l14);
| ^
不正な名称の関数が出来上がっているようです。jsのファイル名が先頭についていますね。
これはこの記事で使っているwasm2cがモジュール名と関数名を結合した文字列をC言語の関数名として使っているためです。
前回まではwasiのインタフェースを使うwasmだったためインポートセクションをwat形式で表すと下記のようになっていました。wasi_snapshot_preview1
モジュールのproc_exit
という意味です。
(type (;0;) (func (param i32)))
(import "wasi_snapshot_preview1" "proc_exit" (func (;0;) (type 0)))
今回のwasmのインポートは下記のようになっています。モジュール名がjs
ファイルになっています。
(type (;0;) (func (param i32 i32) (result i32)))
(import "./wasm_game_of_life_bg.js" "__wbg_alert_d3b6e8db27c82dfa" (func (;0;) (type 1)))
今回はwasi以外のモジュールからインポートする場合はモジュール名を省いた関数名を使うことにします。[7]
ファイルネーム長
wasm2cをビルドしなおしてmakeを実行すると今度は下記のエラーで止まります。
リンカがオブジェクトファイルを見つけられない状態のようです。
ファイル名が長すぎるからですね、wasm_game_of_life_bg.wasm
をlifegame.wasm
にリネームすることにします。
Not found : _build/m68000/wasm_game_of_life_bg.o
Undefined symbol(s) in _build/m68000/lib/wasi.o
_wasm__start
_wasm_memory
関数のバインド
ここまでの修正を行うと下記のリンカエラーで止まるようになります。
wasi.cの中からwasmのエントリーとして呼び出しているstart
関数が見つからないのと、wasmがインポートしているalert
関数が見つからないというのがエラーの内容です[8]。今回のwasmはエクスポートしている関数がgreet
関数ですし、alert
関数はまだ実装していないので当然の結果となります。これらの関数のつなぎこみをwasm-pack
の生成するjavascriptファイルを参考に実装していきます。
Undefined symbol(s) in _build/m68000/lib/wasi.o
_wasm__start
Undefined symbol(s) in _build/m68000/lifegame.o
___wbg_alert_d3b6e8db27c82dfa
pkg/wasm_game_of_life_bg.js
ファイル
wasm-pack build
の結果を格納するpkg
ディレクトリにはwasm_game_of_life_bg.js
というjavascriptファイルが生成されています。この中にjavascriptとwasmとの間でやり取りをするためのコードが記載されています。まず我々の注目したいgreet
とalert
から見ていきます。
export function greet(name) {
const ptr0 = passStringToWasm0(name, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len0 = WASM_VECTOR_LEN;
wasm.greet(ptr0, len0);
}
export function __wbg_alert_d3b6e8db27c82dfa(arg0, arg1) {
alert(getStringFromWasm0(arg0, arg1));
};
greet
は引数として受け取った文字列をpassStringToWasm0
関数でwasmのリニアメモリ上の文字列に変換して、リニアメモリ上のポインタと文字列長をwasm上のwasm.greet
に渡すという処理になっています。
一方、alert
はwasmから文字列のポインタと文字列長を受け取って、これをgetStringFromWasm0
関数でjavascriptの文字列に変換してからブラウザ組み込みのalert
関数に渡す処理となっています。
両処理とも文字列の変換が必要だということがわかりました。
C言語とwasmの間の文字列のやり取り
javascript版のpassStringToWasm0
関数とgetStringFromWasm0
関数を参考にC言語の処理を作っていきます。
まず、passStringToWasm0
関数から見ていきます。関係ない部分を省略すると、wasmからエクスポートされているmalloc関数を使ってwasmのリニアメモリ上に領域を確保してそこに文字列をコピーする処理になっているようです。
function passStringToWasm0(arg, malloc, realloc) {
// ...省略
let len = arg.length;
let ptr = malloc(len, 1) >>> 0;
const mem = getUint8Memory0();
let offset = 0;
for (; offset < len; offset++) {
const code = arg.charCodeAt(offset);
if (code > 0x7F) break;
mem[ptr + offset] = code;
}
// ...省略
WASM_VECTOR_LEN = offset;
return ptr;
}
これをC言語側では以下のように実装しました。
wasm___wbindgen_malloc
で領域を確保し、文字列をコピーしています。
struct WasmString {
uint32_t ptr;
uint32_t length;
};
void passStringToWasm0(struct WasmString* wasm_string, uint8_t* arg) {
uint32_t length = strlen(arg);
uint32_t ptr = wasm___wbindgen_malloc(length, 1);
uint8_t *const m = *wasm_memory;
memcpy(&m[ptr], arg, length);
wasm_string->ptr = ptr;
wasm_string->length = length;
}
続いてgetStringFromWasm0
関数です。
wasmのリニアメモリから持ってきた内容をutf-8
デコードしていますね。
今回は英数字だけを扱うことにしてエンコードは無視します。
let cachedTextDecoder = new lTextDecoder('utf-8', { ignoreBOM: true, fatal: true });
function getStringFromWasm0(ptr, len) {
ptr = ptr >>> 0;
return cachedTextDecoder.decode(getUint8Memory0().subarray(ptr, ptr + len));
}
これをC言語では下記のように実装しました。
領域を確保してコピーしているだけです。
uint8_t* getStringFromWasm0(struct WasmString* wasm_string) {
uint8_t *const m = *wasm_memory;
uint8_t* str = malloc(wasm_string->length + 1);
if (str == NULL) panic("without enough memory.");
memcpy(str, &m[wasm_string->ptr], wasm_string->length);
str[wasm_string->length] = UINT8_C(0);
return str;
}
ここまでの関数を利用してgreet
とalert
は下記のようになります。
void alert(uint8_t* message) {
fprintf(stderr, "%s\n", message);
}
void __wbg_alert_d3b6e8db27c82dfa(uint32_t ptr, uint32_t length) {
struct WasmString wasm_string;
wasm_string.ptr = ptr;
wasm_string.length = length;
uint8_t* str = getStringFromWasm0(&wasm_string);
alert(str);
free(str);
}
void great(uint8_t* name) {
struct WasmString wasm_string;
passStringToWasm0(&wasm_string, name);
wasm_greet(wasm_string.ptr, wasm_string.length);
}
main
関数ではgreet
を呼び出すだけにします。
int main(int argc, char **argv) {
global_argc = argc;
global_argv = argv;
great("X68000");
}
なんだかRustよりCのコードばかり書いているような気がしますが、そこは気にせず先に進みます。
ヒープサイズの調整
ここまで実装するとmakeがエラーなしで通るようになります。
早速実行してみます。
$ run68 WPHELLO.X
Abnormal program termination
アボートしてしまいますね、memory.grow
が失敗しているようです。
今回はHeap領域として3MByteが必要なようです。
これを調整すると下記のように正常に動作するようになります。
$ run68 WPHELLO.X
Hello, X68000!
Life Game
ようやくお待ちかねのLife Gameの実装に移っていきます。
Tutorial[1:2]ではテキストでグリッド表示するLife Gameを作成したのちにCanvasに表示を行う変更を加えていくという段階を踏んでいますが、ここではCanvas版をX68000で実行できるようにしていきます。
RustのコードはTutorialを見てください。ここでは記載を省略します。
関数のバインド
Hello Worldで関数のつなぎ込みにも慣れたのでここではどんどん実装していきます。
Life Gameのwasmは例外を投げるための関数をインポートしているのでまずはこれを実装します。
今回は単純にpanic
関数でプログラムを終了するコードとします。
例外メッセージは先ほどの関数を利用してC言語の文字列にしてpanic
に渡します。
void __wbindgen_throw(uint32_t ptr, uint32_t length) {
struct WasmString wasm_string;
wasm_string.ptr = ptr;
wasm_string.length = length;
panic(getStringFromWasm0(&wasm_string));
}
TutorialではCanvasに描画をしている処理を今回はX-BASICライブラリを使って描画する処理にしました。
#define XBAS_SCREEN_SIZE_256x256 0
#define XBAS_SCREEN_MODE_16BITx1 3
#define XBAS_SCREEN_RES_HIGH 1
#define XBAS_SCREEN_SW_ON 1
void drawCells(uint32_t universe, uint32_t width, uint32_t height, uint32_t target) {
uint8_t *const m = *wasm_memory;
uint32_t cells = wasm_universe_cells(universe);
uint8_t *const data = &m[cells];
target &= 1;
uint32_t offset = target * 256;
fill(0+offset, 0, 255+offset, 255, 0);
for (uint16_t y=0; y<height; y++) {
for (uint16_t x=0; x<width; x++) {
if (data[y*width+x] != 0) {
fill(x*4+offset, y*4, x*4+2+offset, y*4+2, 0xffff);
}
}
}
home(0, offset, 0);
}
int main(int argc, char **argv) {
global_argc = argc;
global_argv = argv;
uint8_t* data = calloc(64*64, 1);
b_init();
screen(
XBAS_SCREEN_SIZE_256x256,
XBAS_SCREEN_MODE_16BITx1,
XBAS_SCREEN_RES_HIGH,
XBAS_SCREEN_SW_ON
);
window(0, 0, 511, 511);
wipe();
uint32_t universe = wasm_universe_new();
uint32_t width = wasm_universe_width(universe);
uint32_t height = wasm_universe_height(universe);
uint32_t draw_target = 0;
drawCells(universe, width, height, draw_target);
for(;;) {
wasm_universe_tick(universe);
draw_target ^= 1;
drawCells(universe, width, height, draw_target);
}
b_exit(0);
}
解説なしにRustの構造体に実装した関数を呼び出していますが、wasm_universe_new
で生成した構造体のポインタをself
として渡しているだけなので特に解説することもないかと思います。
最適化
早速makeして動かしていきます。
動きますね。ただなんだか思ったより遅い感じがします。1コマ描画するのに15秒ぐらいかかっているようです。Chrome上でCanvas描画が爆速で動くのを見た後だけに余計遅く感じます。
生成されたコードを見ていくと多重ループの一番内側の処理で32ビットの乗除を贅沢に実行していますね。68000でこれを実行するのは酷な感じがします。
*| lifegame.c:6445: l74 %= l75;
move.l 70(sp),-(sp) * move.l 70(%sp),-(%sp) | %sfp,
move.l 82(sp),-(sp) * move.l 82(%sp),-(%sp) | %sfp,
move.l d1,46(sp) * move.l %d1,46(%sp) |,
move.l d2,50(sp) * move.l %d2,50(%sp) |,
jbsr ___umodsi3 * jsr __umodsi3 |
*| lifegame.c:6447: l74 *= l75;
addq.l #4,sp * addq.l #4,%sp |,
move.l d0,(sp) * move.l %d0,(%sp) | tmp298,
move.l d4,-(sp) * move.l %d4,-(%sp) | l6,
jbsr ___mulsi3 * jsr __mulsi3 |
addq.l #8,sp * addq.l #8,%sp |,
move.l d0,d7 * move.l %d0,%d7 | tmp299, tmp235
*| lifegame.c:6452: l75 %= l76;
move.l d4,-(sp) * move.l %d4,-(%sp) | l6,
move.l 46(sp),d2 * move.l 46(%sp),%d2 |,
move.l 124(sp),a0 * move.l 124(%sp),%a0 | %sfp,
pea (a0,d2.l) * pea (%a0,%d2.l) |
jbsr ___umodsi3 * jsr __umodsi3 |
addq.l #8,sp * addq.l #8,%sp |,
剰余を計算しているところをビットマスクに変更するなどの変更を加えると1コマ7秒ぐらいまで短縮できました。まだまだ最適化の余地はありそうですが今回はこのくらいにしたいと思います。
let neighbor_row = (row + delta_row) & (self.height()-1);
let neighbor_col = (column + delta_col) & (self.width()-1);
まとめ
今回はwasm-pack
を使ってRustコードからビルドした"Hello world"と"Life Game"をX68000で実行してみました。結果として下記の課題が見えてきたと思います。
- bindgenのようなツールが無いとつなぎ込みコード書くのが大変
- 実行する処理に対して過大なHeap領域が必要になっている
- 最終的なアセンブリコードを意識しないとそれなりに遅い処理となる
次回予告
次回はRustのコードからX68000のIOにアクセスしてみます。
-
コミット:
437249e
でwasmのルールを追加しました ↩︎ -
wasm2cの変更はこちらのコミット:
4bf9ff4
になります ↩︎ -
__wbg_
はwasm-packのbindgenがつける接頭辞です、またマングリングの英数文字列が接尾辞として追加されています ↩︎
Discussion