Learning WASM #6

前回はqjsからhostの関数を呼び出した。


今回は、実際に通信できるように readwrite を生やしてみる。




C側は putsgetLine を参考にこんな風に実装。dotnet側には1byteずつread writeする関数を生やして、jsモジュールとしては改行コードもしくはEOFまで書き込み読み込みし続ける`readLine`、`writeLine`として実装する。


attribute((import_name("read")))extern int host_read();
attribute((import_name("write")))extern void host_write(int c);

static JSValue js_host_read_line(JSContext *ctx, JSValueConst this_val, 
                                int argc, JSValueConst *argv)
{
    int c;
    DynBuf dbuf;
    JSValue obj;

    js_std_dbuf_init(ctx, &dbuf);
    for(;;) {
        c = host_read();
        if (c == EOF) {
            if (dbuf.size == 0) {
                /* EOF */
                dbuf_free(&dbuf);
                return JS_NULL;
            } else {
                break;
            }
        }
        if (c == '\n')
            break;
        if (dbuf_putc(&dbuf, c)) {
            dbuf_free(&dbuf);
            return JS_ThrowOutOfMemory(ctx);
        }
    }
    obj = JS_NewStringLen(ctx, (const char *)dbuf.buf, dbuf.size);
    dbuf_free(&dbuf);
    return obj;
}

static JSValue js_host_write_line(JSContext *ctx, JSValueConst this_val, 
                                int argc, JSValueConst *argv)
{
    int i,j;
    size_t len;
    const char *str;
    
    for(i = 0; i < argc; i++) {
        str = JS_ToCStringLen(ctx, &len, argv[i]);
        if (!str)
            return JS_EXCEPTION;

        for (j = 0; j < len; j++) {
            host_write(str[j]);
        }

        host_write('\n');

        JS_FreeCString(ctx, str);
    }
    return JS_UNDEFINED;
}

static const JSCFunctionListEntry js_host_funcs[] = {
    JS_CFUNC_DEF("readLine", 0, js_host_read_line),
    JS_CFUNC_DEF("writeLine", 1, js_host_write_line),
};

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


dotnet側はとりあえず`Stream`に直結させるリングバッファとか考えたけどこれが手っ取り早い


using System;
using System.IO;
using System.Text;
using Wasmtime;

var dir = AppDomain.CurrentDomain.BaseDirectory;
var script =
@"
import * as host from 'host';
console.log('-- start js');
host.writeLine('Hello, I\'m js!');
const str = host.readLine();
console.log(str);
console.log('-- end js');

";
File.WriteAllText(Path.Combine(dir, "main.js"), script);

using var rbuf = new MemoryStream(Encoding.UTF8.GetBytes("Hello, I'm dotnet!\n"));
using var wbuf = new MemoryStream();

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", "read", () => {
    return (int)rbuf.ReadByte();
});

linker.DefineFunction("env", "write", (int c) => {
    wbuf.WriteByte((byte)c);
});

linker.DefineWasi();

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

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

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

run.Invoke(store);

wbuf.Seek(0, SeekOrigin.Begin);
var str = Encoding.UTF8.GetString(wbuf.ToArray());
Console.WriteLine(str);


実行結果はこんな感じ。 qjs内でdotnetから読み込んだ文字列を標準出力して、実行完了後、dotnet側でjsから受け取った文字列を標準出力してる。


-- start js
Hello, I'm dotnet!
-- end js
Hello, I'm js!


これで、文字列のやり取りができるようになったので、JSON RPCはできそう。


ただ、ループで回してたりするからjsからdotnetにRPCしている中でdotnetからjsにRPCするような、行ったり来たりするRPCはできそうにない。


まあそれは出来なくても大丈夫なはず。


次はJSON RPCを組んでみようと思う。