Learning WASM #9

.NET 8.0のリリースも間近に迫りました。


LTSの更新であれやこれやを更新して周らないといけないかと思うと憂鬱です。新しいバージョンのリリースが素直に喜べないないんて歳を重ねたってことなのかな、なんて思う今日この頃。


でも、8が全く楽しみでないかというとそうでもありません。AOTが対応になってWASIバイナリへのコンパイルも簡単にできるようになります。


ということでやってみました。



コンパイルするにはwasi-experimental workloadとWASI-SDKが必要です。


まとめてDockerfileにしておきました。


ここまでは、前にもやったことがあるし、実はそこまで興味ない。


興味があるのはエントリーポイントのあるWASIバイナリではなく、関数がexportされているライブラリとしてのWASIバイナリです。


これができると、WASMを使ったプラグインを.NETで書くことができます。


今まは、wasmtimeを使ってホストしかかけてなかったけどゲスト側もC#で書きたい。.NET7のころからできてたみたいですが手を出してなかったのでこの機会にやってみました。



コンパイルするには、wasm-experimental、WASI-SDK、emscriptenが必要。

先のwasiappと違ってこっちはdotnet ILCをLLVM版に置き換えないといけない。

Linux版は、net7.0までしかないので今の所Windowsでしか.NET8でできないので、DockerfileはWindows Container用になってます。



WASI関数として払い出したのは、次の2つの関数。



using System.Runtime.InteropServices;

namespace lib;

public static class Class1
{
    [UnmanagedCallersOnly(EntryPoint = "MyAdd")]
    public static int MyAdd(int a, int b) 
    {
        return a + b;
    }

    [UnmanagedCallersOnly(EntryPoint = "HelloWorld")]
    public static IntPtr HelloWorld() 
    {
        var ptr = Marshal.AllocHGlobal(13);
        Marshal.WriteByte(ptr, 0x48);
        Marshal.WriteByte(ptr+1, 0x65);
        Marshal.WriteByte(ptr+2, 0x6c);
        Marshal.WriteByte(ptr+3, 0x6c);
        Marshal.WriteByte(ptr+4, 0x6f);
        Marshal.WriteByte(ptr+5, 0x2c);
        Marshal.WriteByte(ptr+6, 0x57);
        Marshal.WriteByte(ptr+7, 0x6f);
        Marshal.WriteByte(ptr+8, 0x72);
        Marshal.WriteByte(ptr+9, 0x6c);
        Marshal.WriteByte(ptr+10, 0x64);
        Marshal.WriteByte(ptr+11, 0x21);
        Marshal.WriteByte(ptr+12, 0x00);
        return ptr;
    }
}


これをWASMにビルドして、Wasmtimeで読み込んで使います。


using System.Text;
using Wasmtime;

var wasmfile = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "lib.wasm");
var wasi = new WasiConfiguration();

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

linker.DefineWasi();
store.SetWasiConfiguration(wasi);

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

var init = instance.GetAction("_initialize");
if (init is null)
{
    Console.WriteLine("error: MyAdd export is missing");
    return;
}

init();

var add = instance.GetFunction<int, int, int>("MyAdd");
if (add is null)
{
    Console.WriteLine("error: MyAdd export is missing");
    return;
}

Console.WriteLine(add(1,2));


var helloworld = instance.GetFunction<int>("HelloWorld");
if (helloworld is null)
{
    Console.WriteLine("error: HelloWorld export is missing");
    return;
}

var ptr = helloworld();
var mem = instance.GetMemory("memory");
var str = Encoding.ASCII.GetString(mem!.GetSpan(ptr, 13));

Console.WriteLine(str);


実行結果


3
Hello,World!


関数のexportはUnmanagedなコードになるのでどこまでマネージドでかけるかは研究が必要そう。


あと、dotnet runtimeを動かせるようにするために事前に_initialize関数を実行しないといけない。これがプラグインシステムでは難点だなと思った。

Rustで作ったWASMファイルはこの手の初期化関数が必要ないので、WASMでプラグインかけます系のホストがあっても、これが呼べないがために.NETで書いたWASMはつかえないのでは。

自分でホストを作る分にはどうにでもなるけど。