🧬

wasm-packで作ったLife GameをレトロPC(X68000)で動かす

2024/02/13に公開

今回は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 buildpkgディレクトリに出来上がったwasm_game_of_life_bg.wasmが変換対象です。
wasm2c[5]のワークディレクトリをセットアップしてここにwasmファイルをコピーします。
ワークディレクトリにあるmakefile[6]のビルド対象に下記のようにwasmファイルを追加して準備完了です。

makefile
# *.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.wasmlifegame.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との間でやり取りをするためのコードが記載されています。まず我々の注目したいgreetalertから見ていきます。

wasm_game_of_life_bg.js
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のリニアメモリ上に領域を確保してそこに文字列をコピーする処理になっているようです。

wasm_game_of_life_bg.js
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で領域を確保し、文字列をコピーしています。

wasi.c
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デコードしていますね。
今回は英数字だけを扱うことにしてエンコードは無視します。

wasm_game_of_life_bg.js
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言語では下記のように実装しました。
領域を確保してコピーしているだけです。

wasi.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;
}

ここまでの関数を利用してgreetalertは下記のようになります。

wasi.c
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を呼び出すだけにします。

wasi.c
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に渡します。

wasi.c
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ライブラリを使って描画する処理にしました。

wasi.c
#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秒ぐらいまで短縮できました。まだまだ最適化の余地はありそうですが今回はこのくらいにしたいと思います。

lib.rs
        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にアクセスしてみます。

脚注
  1. Tutorial: Conway's Game of Life ↩︎ ↩︎ ↩︎

  2. wikipedia:X68000 ↩︎

  3. wasm-pack ↩︎

  4. RustやZigのコードをWebAssembly経由でレトロPC(X68000)で実行する ↩︎

  5. wasmx68.wb ↩︎

  6. コミット:437249eでwasmのルールを追加しました ↩︎

  7. wasm2cの変更はこちらのコミット:4bf9ff4になります ↩︎

  8. __wbg_はwasm-packのbindgenがつける接頭辞です、またマングリングの英数文字列が接尾辞として追加されています ↩︎

Discussion