Learning WASM #7

前回はqjsのカスタモジュールにreadメソッドとwriteメソッドを生やして、hostのdotnetランタイムと文字列のやり取りをできるようにした。


今回は、文字列がやり取りできるようになったのでJSON-RPCを流して、dotnetからjsの関数をコールしてその結果を得たい。




RPC処理ロジックをカスタムモジュールにするには、Cで書く必要がありちょっと大変なので、いったんmain.jsの中にJavaScriptで書いた


import * as host from 'host';
import * as std from 'std';
import ejs from './ejs.mjs';
ejs.fileLoader = function(filename) {
    const file = std.open(filename,'r');
    return file.readAsString();
};

const handlers = {};
function register(methodName, handler) {
    handlers[methodName] = handler;
}
function listen() {
    while(true) {
        const line = host.readLine();
        
        if (line == '.quit') {
            break;
        }

        const data = JSON.parse(line);
        const func = handlers[data.method];
        if (typeof func == 'function' && Array.isArray(data.params)) {
            func.apply(null, data.params.concat([function (result) {
                host.writeLine(JSON.stringify({
                    jsonrpc: '2.0',
                    result,
                    id: data.id,
                }));
            }]));
        }
    }
}
register('transform', function (payload, callback) {
    ejs.renderFile('template.ejs', payload, {}, function (err, str) {
        if (err) {
            std.err.puts(err);
        }
        else {
            const transformed = {
                name: payload.name,
                html: str
            };
            callback(transformed);
        }
    })
});

listen();


関数を登録する registerとhostとのやり取りを開始するlistenを作成した。 json-rpcにはないが、一応終了させるための .quitを受信するとlistenが終了しプログラムが終了する。



dotnetは前回と同じ、ペイロードとなる rbufの初期値だけ違う


var rpccall = JsonSerializer.Serialize(new
{
    jsonrpc = "2.0",
    method = "transform",
    @params = new[] { new { name = "iwate"} },
    id = Guid.NewGuid().ToString()
});

using var rbuf = new MemoryStream(Encoding.UTF8.GetBytes($"{rpccall}\n.quit\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);


実行結果はこんな感じ、EJSの実行結果がJSON-RPC形式で返ってきてる。


{"jsonrpc":"2.0","result":{"name":"iwate","html":"<html>\r\n<body>\r\n<p>Hello, Wasmtime! I am iwate.</p>\r\n</body>\r\n</html>"},"id":"e4c82100-a5b8-4389-bcbf-41b77682f311"}


次は、dotnet側をTask化して、main.jsをlisten状態で常駐させておいて、任意のタイミングで呼び出せるようにしたい。

それか、qjs側のJSON-RPC処理部のCモジュール化とJSON-RPCエラー系の実装か。


どれ最初にやろうかな。まよう。


自由にjs処理を呼べる下地ができてきていよいよ楽しくなってきた。