Learning WASM #5

前回、dotnet上で動かしたwasmtimeの上でJS(QuickJS)を動かした。(ややこしいな

QuickJSの性質上、QuickJSで動いているJSとホストであるdotnetで通信させる方法が、ファイルを返した通信に限られる。

標準入出力でも行けないかと思ったけど、process分離して動いてるわけじゃないから標準入出力を共有してるためフックできなかった。

Cのfopenが実装されているので、ファイルだけでなくUNIXソケットファイルとかでも通信可能そうだったけどそうなるとホストがUNIXやLinuxじゃないとだめなので、開発しにくそうでやめた。おんなじ理由でtmpfsを使ったRAM上の通信もやめた。

せっかく、WASMを使ってるんだから、QucikJSに手を入れてホストと通信する関数をはやしてしまえばいいだろうということで書いてみた。

https://github.com/iwate/learn-wasm-with-dotnet/tree/main/challenge05

今回は簡単にwasmtimeのサンプルも記載されている引数なしのhello関数をホストに生やした。

using var engine = new Engine();
using var linker = new Linker(engine);
using var store = new Store(engine);

linker.DefineFunction("env", "hello", () => { Console.WriteLine("This is called from wasm."); });

QuickJSはCで書かれているので(しかもファイル数が少ない)、カスタムが簡単。

外部の関数を取り込むにはCの作法どうり、externで使いたい関数のシンボルを定義

extern void hello();

↓の感じでモジュールとして拡張できればかっちょいいなと思ったので、

import * as host from 'host'
host.hello();

quickjs_libc.cに他のモジュールを参考にhostモジュールを追加する

extern void hello();

static JSValue js_host_hello(JSContext *ctx, JSValueConst this_val, 
                                int argc, JSValueConst *argv)
{
    hello();
    return JS_UNDEFINED;
}

static const JSCFunctionListEntry js_host_funcs[] = {
    JS_CFUNC_DEF("hello", 0, js_host_hello),
};

static int js_host_init(JSContext *ctx, JSModuleDef *m)
{
    return JS_SetModuleExportList(ctx, m, js_host_funcs,
                                  countof(js_host_funcs));
}

JSModuleDef *js_init_module_host(JSContext *ctx, const char *module_name)
{
    JSModuleDef *m;
    m = JS_NewCModule(ctx, module_name, js_host_init);
    if (!m)
        return NULL;
    JS_AddModuleExportList(ctx, m, js_host_funcs, countof(js_host_funcs));
    return m;
}

ただ、こんなエラーが出てすんなりとはビルドがうまくいかない。

wasm-ld: error: CMakeFiles/qjs.dir/src/quickjs-libc.c.obj: undefined symbol: hello

さっきのexternで定義したホストの関数が特定できない。実行時にリンクされるから、この段階はundefinedでいいんだけど無視してくれない。

wasm-ldのオプション調べたらどうやら --allow-undefined をつければいいらしく、CMakeLists.txtに設定を足す

SET(CMAKE_EXE_LINKER_FLAGS  "${CMAKE_EXE_LINKER_FLAGS} --allow-undefined")

でもそれでもエラー。なぜか無視してくれないから頭を抱えたけど、カンマ区切りで付与することに途中で気づいた。CMakeむずい。

SET(CMAKE_EXE_LINKER_FLAGS  "${CMAKE_EXE_LINKER_FLAGS},--allow-undefined")

無事ビルドできたqjs.wasmを実行する

using System;
using System.IO;
using Wasmtime;

var dir = AppDomain.CurrentDomain.BaseDirectory;
var script =
@"
import * as host from 'host';
host.hello();
";
File.WriteAllText(Path.Combine(dir, "main.js"), script);

using var engine = new Engine();
using var module = Module.FromFile(engine, "qjs.wasm");
using var linker = new Linker(engine);
using var store = new Store(engine);

linker.DefineFunction("env", "hello", () => { Console.WriteLine("This is called from wasm."); });

linker.DefineWasi();

store.SetWasiConfiguration(new WasiConfiguration()
    .WithPreopenedDirectory(dir, ".")
    .WithArgs("--", "main.js"));

var instance = linker.Instantiate(store, module);

var run = instance.GetFunction(store, "_start");

run.Invoke(store);

実行結果はもちろん

This is called from wasm.

やったね。hostの関数を呼び出せた。これでファイルを介さずに通信できる、ホスト機能やデータが使える。