• 如何使用WASI SDK生成wasm文件
  • 发布于 2个月前
  • 454 热度
    0 评论
  • 长青诗
  • 1 粉丝 45 篇博客
  •   
背景
WASM 是一项非常令人兴奋的技术,它高效、安全、可移植性强,打破了语言间的隔离,令跨语言的交互变得更加容易。在 .NET 中,wasm-tools 可以生成一组在 web 环境运行的 wasm 文件,它内置 JS 互操作 API,可简化与浏览器的交互;而实验性 .NET WASI SDK,这会生成一个 wasi 标准下的独立  .wasm  文件,可以在任何 WebAssembly 宿主环境中运行,不需要任何 JavaScript 支持,但是 .NET WASI SDK 无法进行 AOT,生成的 wasm 文件会带有运行时,内容会比较大。

这里记录下如何使用 WASI SDK 生成 wasm 文件。

环境
.Wasi.Sdk
安装依赖
dotnet add package Wasi.Sdk --prerelease
编写 Wasi 代码
新建控制台应用 WasmLib:
public class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine("Hello from Main!");
    }
}
编译
运行 dotnet build 命令,将会生成 wasm 文件:bin\Debug\net6.0\WasmLib.wasm

非 Web 环境运行 wasm
安装 Wasmtime
Wasmtime 是在非 Web 环境用于执行 wasm 和 wasi 的运行时环境。
dotnet add package Wasmtime --prerelease
引入 wasm 文件
// 堆代码 duidaima.com
var waisPath = "xxx\bin\Debug\net6.0\WasmLib.wasm";
var wasi = new WasiConfiguration()
 .WithInheritedStandardInput()
 .WithInheritedStandardOutput()
 .WithInheritedStandardError();

using var engine = new Engine();
using var store = new Store(engine);
store.SetWasiConfiguration(wasi);

using var linker = new Linker(engine);
linker.DefineWasi();

+ using var module = Module.FromFile(engine, waisPath);
dynamic instance = linker.Instantiate(store, module);
// _start 函数对应的就是 C# 中的 Main 函数
var main = instance.GetFunction("_start");
main.Invoke();
现在,我们将可以在控制台观察到 Hello from Main! 的输出:

自定义 Wasi 入口函数
默认情况下,Wasi.Sdk 只会为 main 函数生成入口,如果想要将自定义函数导出,则需要自己编写一个 c 存根函数,并在 c 中调用 C# 函数,过程如下:

编写 C#
在 WasmLib 中添加类 WasmClass:
public class WasmClass
{
 public static void Run()
 {
  Console.WriteLine("Hello from WasmClass.Run");
 }
}
native
在项目下新加文件夹 _native_,并该文件夹下添加 stub-wasi.c:
#include <mono-wasi/driver.h>
#include <assert.h>
#include <stdio.h>

MonoMethod* method_HandleIncomingRequest;
__attribute__((export_name("run")))
void run() {
    if (!method_HandleIncomingRequest) {
        method_HandleIncomingRequest =
            lookup_dotnet_method("WasmLib.dll", "WasmLib", "WasmClass", "Run", -1);
    }

    MonoObject* exception;
    void* method_params[] = { };
    mono_wasm_invoke_method(method_HandleIncomingRequest, NULL, method_params, &exception);

    assert(!exception);
}
首先通过 lookup_dotnet_method("WasmLib.dll", "WasmLib", "WasmClass", "Run", -1); 获取 WasmLib.dll 中 WasmLib 命名空间下的 WasmClass 类的 Run 方法,然后通过 mono_wasm_invoke_method 调用C#中的该方法。注意,这里使用 __attribute__ 用来表明该函数在对应的 wasi 函数名为 run。

关于 mono-wasi/driver.h,可以在 这里 找到源码,lookup_dotnet_method 和 mono_wasm_invoke_method 方法的签名,都可以在里面找到。

使用自定义导出函数
现在,我们可以通过 instance.GetAction("run") 使用 WasmClass.Run 函数:
var wasi = new WasiConfiguration()
    .WithInheritedStandardInput()
    .WithInheritedStandardOutput()
    .WithInheritedStandardError();

using var engine = new Engine();
using var store = new Store(engine);
store.SetWasiConfiguration(wasi);

using var linker = new Linker(engine);
linker.DefineWasi();

using var module = Module.FromFile(engine, wasmFile);

var instance = linker.Instantiate(store, module);
//var main = instance.GetFunction<int, int>("main");
// 必须先执行下 _start(main) 函数
var main = instance.GetFunction("_start");
main.Invoke();


+ var run = instance.GetAction("run");
+ run.Invoke();
注意,在执行自定义的导出的函数前,必须需要先调用 _start(main) 函数,这样才会初始化 C# 的运行时。

传参
WebAssembly 类型系统还很少,只有四种数字类型(i32/int、i64/long、f32/float和f64/double)。目前,如果要使用复杂类型(例如字符串、对象、数组、结构体),需要将它们序列化为线性内存,并提供它们所在位置的引用。通过指针的形式将参数传递给存根 c 函数,然后 c 再将其转换为 C# 中的相关类型。

使用字符串
定义 wasm 函数
首先新增一个 wasm 中导出的函数 Echo:
public class WasmClass
{
 // ...
 
    public static void Echo(int times, string msg)
    {
        for (var i = 0; i < times; i++)
        {
            Console.WriteLine($"Echo: {msg}");
        }
    }
}
定义wasm 函数存根
MonoMethod* method_HandleEcho;
__attribute__((export_name("echo")))
void echo(int times, char* msg) {
    if (!method_HandleEcho) {
        method_HandleEcho =
            lookup_dotnet_method("WasmLib.dll", "WasmLib", "WasmClass", "Echo", -1);
    }
    MonoObject* exception;
    MonoString* msg_trans = mono_wasm_string_from_js(msg);
    void* method_params[] = {
        &times,
        msg_trans
    };
    mono_wasm_invoke_method(
        method_HandleEcho, 
        NULL, 
        method_params, 
        &exception
    );
    assert(!exception);
    free(msg);
}
使用 wasm 函数
...
// 必须先执行下 _start(main) 函数
var main = instance.GetFunction("_start");
main.Invoke();


var address = CreateWasmString(instance, "hi wasm");
var echoTimes = 5;

var echo = instance.GetFunction("echo");
echo?.Invoke(echoTimes, address);


int CreateWasmString(Instance instance, string value)
{
    value += "\0";

    var mem = instance.GetMemory("memory");
    var wasmMalloc = instance.GetFunction<int, int>("malloc");
    var len = Encoding.UTF8.GetByteCount(value);
    var startAddress = wasmMalloc.Invoke(len);
    mem!.WriteString(startAddress, value, Encoding.UTF8);

    return startAddress;
}
线性内存是另一个重要的 WebAssembly 构建块,通常用于表示已编译的 C/C++ 应用程序的整个堆。从 JavaScript 的角度来看,线性内存(以下简称“内存”)可以被认为是可调整大小的  ArrayBuffer ,它针对加载和存储的低开销沙箱进行了精心优化。

使用数组
前面提到,WebAssembly ,只有四种数字类型,因此要使用数组,只能通过指针,将数组的位置和长度传递给 c 存根函数,然后转为 C# 中的数组。所有的类型,都可以序列化为 byte[] 后进行处理,这里记录下怎么使用 byte[] 进行数据交互,关键地方可以看注释。由于我对 C 语言不熟,这里可能走了弯路,或许可能有更加优秀的方式。

定义 wasm 函数:
public class WasmClass
{

    public static IntPtr ByteArrayParam(byte[] bytes)
    {
        // 将 byte[] 转为目标类型 int[]
        var intLen = bytes.Length / sizeof(int);
        var intArr = new int[intLen];
        for (var index = 0; index < intLen; index++)
        {
            intArr[index] = BitConverter.ToInt32(bytes, index * sizeof(int));
        }
    
        // 输出 int[]
        foreach (var nr in intArr)
        {
            Console.WriteLine($"ArrayParam val:{nr}");
        }

  // 准备返回的数据
        byte[] returnData = { 0x05, 0x01, 0x07 };

        unsafe
        {
            // 获取返回数据的指针
            int pointAddr = 0;
            fixed (byte* p = returnData)
            {
                IntPtr pn = (IntPtr)p;
                pointAddr = pn.ToInt32();
            }

            Console.WriteLine("data pointAddr:" + pointAddr);

   // 将返回数据的指针和长度封装为一个数组,并将其指针返回给调用方
            int[] returnDataInfo = new[]
            {
                pointAddr, returnData.Length
            };

            fixed (int* p = returnDataInfo)
            {
                IntPtr pn = (IntPtr)p;
                Console.WriteLine("return result pointer:" + pn);
                return pn;
            }
        }
    }
}
定义 wasm 存根
MonoClass* mono_get_byte_class(void);
MonoDomain* mono_get_root_domain(void);
int mono_unbox_int(MonoObject* obj);

// 用于将调用方的指针和数组长度生成为c#中的数组
MonoArray* mono_wasm_typed_array_new(void* arr, int length) {
    MonoClass* typeClass = mono_get_byte_class();
    MonoArray* buffer = mono_array_new(mono_get_root_domain(), typeClass, length);
    // 1 == sizeof(byte)
    int p = mono_array_addr_with_size(buffer, 1, 0);
    // length == length * sizeof(byte)
    memcpy(p, arr, length);
    return buffer;
}

// 存根函数
MonoMethod* method_HandleArrayParam;
__attribute__((export_name("array_param")))
int array_param(void* nrs_ptr, int nrs_len)
{
    if (!method_HandleArrayParam) {
        method_HandleArrayParam =
            lookup_dotnet_method("WasmLib.dll", "WasmLib", "WasmClass", "ByteArrayParam", -1);
    }

    MonoObject* exception;
    MonoArray* nrs_trans = nrs_ptr ? mono_wasm_typed_array_new(nrs_ptr, nrs_len) : NULL;
    
    void* method_params[] = {
        nrs_trans
    };
    MonoObject* res = mono_wasm_invoke_method(
        method_HandleArrayParam,
        NULL,
        method_params,
        &exception
    );

    assert(!exception);
    free(nrs_ptr);

 // C#返回的是一个指针,MonoObject 是对其的装箱,需要使用 mono_unbox_int 将其拆箱为一个 int 值(指针地址)
    int p = mono_unbox_int(res);
    return p;
}
使用 wasm 函数
void RunByteArrayParam(string wasmFile)
{
    ...
    var main = instance.GetFunction("_start");
    main.Invoke();

    // 调用 wasm
    var (start, byteLen) = CreateWasmArray(instance, new[] { 4, 5 });
    var arrayParamFunc = instance.GetFunction<int, int, int>("array_param")!;
    int res = arrayParamFunc.Invoke(start, byteLen);

    // 根据 wasm 返回的指针,获取数据
    var mem = instance.GetMemory("memory")!;
    long dataPointerStart = mem.ReadInt32(res);
    long dataLen = mem.ReadInt32(res + sizeof(int));
    // 根据 数据的指针 和 长度 解析数据
    byte[] data = new byte[dataLen];
    for (int i = 0; i < dataLen; i++)
    {
        byte b = mem.ReadByte(dataPointerStart + i);
        data[i] = b;
        Console.WriteLine($"byte array {i} is {b}");
    }


    (int start, int len) CreateWasmArray(Instance instance, int[] value)
    {
        var mem = instance.GetMemory("memory");
        var wasmMalloc = instance.GetFunction<int, int>("malloc");

        var len = value.Length * sizeof(int);
        var start = wasmMalloc.Invoke(len);
        Console.WriteLine($"{start}, {len}");
        var index = 0;
        for (int i = 0; i < len; i += sizeof(int))
        {
            mem!.WriteInt32(start + i, value[index++]);
        }

        return (start, len);
    }
}
小结
可以看到,wasm 对于复杂类型的传递有多复杂。不过目前有的提议,增加一个 anyref 类型,宿主环境可以使用 anyref 包装复杂类型,而 wasm 则可以通过 anyref 获取具体类型。
用户评论