Open9

LiveSplit の Wasm 版 AutoSplitter を構築する

ゆでゆで

livesplit-auto-splitting にあるように、API として update() が公開されている WASM ならば認識されそう

AssemblyScript で以下のように記述してコンパイルしたものを asr-debugger に読み込ませられることを確認

export function update(): void {}
ゆでゆで

エコシステムが整っているのは Rust だが、せっかくなので Moonbit でやってみたい

https://discuss.moonbitlang.com/t/the-moonbit-update-0401/197#build-system-update-3 に従ってビルドしたものを読み込ませると、

Failed starting the auto splitter.

Caused by:
    The WebAssembly module has no exported memory called `memory`, which is a requirement.

とのこと

以下はそのコード

moon.pkg.json
{
    "link": {
        "wasm": {
            "exports": [
                "update"
            ]
        }
    }
}
update.mbt
pub fn update() -> Unit {
  ()
}
ゆでゆで

https://github.com/LiveSplit/livesplit-core/tree/master/crates/livesplit-auto-splitting#requirements-for-the-auto-splitters

In addition the WebAssembly module is expected to export a memory called memory.

とあった(見落とし)

https://zenn.dev/mizchi/articles/moonbit-202404-202408#webassembly.memory-の初期化方法 に従ってコンパイルターゲットを WasmGC に変更し、memory を export するようにしたら適切に読み込まれた

moon.pkg.json
{
  "link": {
    "wasm-gc": {
      "export-memory-name": "memory",
      "exports": ["update"]
    }
  }
}

wasm-gc ? -> https://zenn.dev/cybozu_frontend/articles/20231214_wasmgc

  • VM にガベージコレクタを含む言語を Wasm へ移植する際にこの機能の恩恵を受けることができます
ゆでゆで

Auto Splitter Runtime 側の API を使う

とりあえずこれでテキストの出力 API を呼び出せた

fn runtime_print_message(text_ptr : UInt, text_len : UInt) = "env" "runtime_print_message"

pub fn update() -> Unit {
  let _ = runtime_print_message(0, 8)
}

もちろん runtime_print_message の第一引数はポインタなので、 moonbit 側でメモリを確保してポインタを渡す方法を調査する

ところで moonbit はまだ async / await が使えないそうなので試せることを試したら一旦お流れになる可能性がある

ゆでゆで

(どうすんじゃいポインタ渡し)

ということで Wasm と Moonbit の触感をなんとなく知れたので、素直に
https://github.com/LiveSplit/auto-splitter-template の恩恵を預かることにした

ゆでゆで

時間が経ったが AssemblyScript 版
ArrayBuffer が内部的にはポインタなので、以下で問題なく文字列を扱えた

@external("env", "runtime_print_message")
declare function runtimePrintMessage(text_ptr: ArrayBuffer, text_len: usize): void;

export function update(): void {
  const str = String.UTF8.encode('sample');
  runtimePrintMessage(str, str.byteLength)
}
ゆでゆで

https://github.com/peter-jerry-ye/memory/blob/main/memory.wasm.mbtget_offset を拝借して文字列の出力ができた

extern "wasm" fn get_offset(bytes : Bytes) -> Int =
  #|(func (param i32) (result i32) local.get 0 call $moonbit.decref local.get 0 i32.const 8 i32.add)

fn runtime_print_message(text_ptr : Int, text_len : Int) = "env" "runtime_print_message"

pub fn update() -> Unit {
  let bytes = b"aaa"
  runtime_print_message(get_offset(bytes), bytes.length())
}

勉強不足が祟って思ったより wat が読めなかったので $moonbit.decref の中身を読むのをやめてしまったが、引っ張ってきているアドレスと実態は見えたのでまあよし