Open5

ZigでWASMのお勉強

ktz_aliasktz_alias

いい感じのチュートリアル見つけたので、やってく

https://blog.mjgrzymek.com/blog/zigwasm

環境

MacOS: Ventura
Zig: 0.14.0-dev.2126+e27b4647d

  • WAT見るため、wabtインストールしておくと安心
  • ブラウザではなくスタンドアロンで実行したいので、wasmtimeなどのランタイムもインストールしておく。
ktz_aliasktz_alias

01 add

  1. root.zigを作成し編集
extern fn print(i32) void;

export fn add(a: i32, b: i32) i32 {
    return a + b;
}
  1. ビルド
zig build-lib root.zig -target wasm32-freestanding -fno-entry --export=add -O ReleaseFast
  • Zig 0.14でも0.12のオプションでビルドOK
  • 成果物として、libroot.alibroot.a.oの2つが生成される
    • このうちlibroot.a.owasm
  • wasm2wat libroot.a.oWATフォーマットで生成結果を確認できる
(module
  (type (;0;) (func (param i32 i32) (result i32)))
  (import "env" "__linear_memory" (memory (;0;) 0))
  (import "env" "__stack_pointer" (global (;0;) (mut i32)))
  (func $add (type 0) (param i32 i32) (result i32)
    local.get 1
    local.get 0
    i32.add))

WATの結果から分かるように、ランタイムに__linear_memory__stack_pointerを要求している。
もちろんそんなものないため、なんとかエクスポートしてやる必要がある。

方法は単純で、build-libではなくbuild-exeを使えばいい。

zig build-exe root.zig -target wasm32-freestanding -fno-entry --export=add -O ReleaseFast

WATの変換結果:

(module $root.wasm
  (type (;0;) (func (param i32 i32) (result i32)))
  (func $add (type 0) (param i32 i32) (result i32)
    local.get 1
    local.get 0
    i32.add)
  (memory (;0;) 16)
  (global $__stack_pointer (mut i32) (i32.const 1048576))
  (export "memory" (memory 0))
  (export "add" (func $add)))

期待通り、__stack_pointermemoryがエクスポートされている。

$ wasmtime --invoke add root.wasm 10 32
warning: using `--invoke` with a function that takes arguments is experimental and may break in the future
warning: using `--invoke` with a function that returns values is experimental and may break in the future
42

実験機能の警告は出るが、正しく実行できてる。

ktz_aliasktz_alias

Zigの制限

ZigによるWASM関数のエクスポートは、トップレベルのものに限られる。

const S = struct {
    export fn add(...) { ... }
};

のように、コンテナ内の関数はエクスポートされない。

コンテナ内の関数をエクスポートしたければ、トップレベル関数でラップする必要がある。

const S = struct {
    fn add(...) { ... }
};

export fn add(a: i32, b: i32) i32 {
    return S.add(a, b);
}
ktz_aliasktz_alias

02 Strings

オリチャー発動して(既に発動している)、実行できる成果物を作成する。

doubleStringの定義は同じ

const std = @import("std");

pub fn doubleString(s: [*]u8, length: usize, capacity: usize) i32 {
    if (capacity < length) return -1;

    const left = s[0..length];
    const right = s[length..length*2];
    std.mem.copyForwards(u8, right, left);

    return @as(i32, @intCast(length)) * 2;
}

ドライバは変更する。

const std = @import("std");

const doubleString = @import("./root.zig").doubleString;

pub fn main() void {
    const s0 = "Hello World";
    var s1: [s0.len * 2]u8 = undefined;
    std.mem.copyForwards(u8, s1[0..s0.len], s0);

    const len = doubleString(&s1, s0.len, s1.len);

    std.debug.print("{s} (len: {})\n", .{s1, len});
}

wasm32-wasiターゲットでビルドし、wasmtimeで実行。

$ zig build-exe main.zig -target wasm32-wasi -O ReleaseFast
$ wasmtime main.wasm
Hello WorldHello World (len: 22)

実行できる成果物の場合、exportは不要で、コンパイラがいい感じにやってくれて便利。

ktz_aliasktz_alias

03 JSON

reverseNames関数はそのまま

const std = @import("std");
const builtin = @import("builtin");

const allocator = platform: {
    if (builtin.os.tag == .wasi) {
        break:platform std.heap.wasm_allocator;
    }
    else {
        break:platform std.heap.page_allocator;
    }
};

const Person = struct {
    id: i32,
    name: []u8,
};

pub fn reverseNames(s: [*]u8, length: usize, capacity: usize) i32 {
    const input = s[0..length];
    const json = 
        std.json.parseFromSlice([]Person, allocator, input, .{})
        catch return -1
    ;
    defer json.deinit();
    
    for (json.value) |x| {
        std.mem.reverse(u8, x.name);
    }

    var output = std.ArrayList(u8).init(allocator);
    defer output.deinit();

    std.json.stringify(json.value, .{}, output.writer()) 
    catch return -2;

    const output_length = output.items.len;
    if (output_length > capacity) return -3;

    std.mem.copyForwards(u8, s[0..output_length], output.items);

    return @as(i32, @intCast(output_length));
}

アロケータをコンパイラフラグで分岐しているのは途中うまくいかず、zig run main.zigでコード間違っていないかどうかを確認したかったから。

ドライバ側は、2 Stringsと同様な感じで。

const std = @import("std");

const reverseNames = @import("./root.zig").reverseNames;

const source = 
    \\[
    \\  { "name": "John", "id": 1 },
    \\  { "name": "Jane", "id": 2 }
    \\]
;

pub fn main() void {
    var s: [source.len]u8 = undefined;
    std.mem.copyForwards(u8, s[0..source.len], source);
    std.debug.print("s.len{}, source.len: {}\n", .{s.len, source.len});

    const len = reverseNames(&s, source.len, source.len);

    std.debug.print("{s}\n (len: {})\n", .{s, len});
}

wasmtimeで実行

$ wasmtime main.wasm                            
platform: true
File oprn flags: r/true, w/false
source.len: 64, s.len: 64
[{"id":1,"name":"nhoJ"},{"id":2,"name":"enaJ"}]ane", "id": 2 }
]
 (len: 47)

File IOを試してみる

上の例ではリテラルを使用したが、JSON文字列をファイルに落とし、ファイルからの読み込みを行ってみる。
試行錯誤したが、ドライバを以下の感じに実装した。

const std = @import("std");
const builtin = @import("builtin");

const reverseNames = @import("./root.zig").reverseNames;

const allocator = platform: {
    if (builtin.os.tag == .wasi) {
        break:platform std.heap.wasm_allocator;
    }
    else {
        break:platform std.heap.page_allocator;
    }
};

pub fn main() void {
    std.debug.print("platform: {}\n", .{comptime builtin.os.tag == .wasi and !builtin.link_libc});
    const flags = std.fs.File.OpenFlags{};
    std.debug.print("File oprn flags: r/{}, w/{}\n", .{flags.isRead(), flags.isWrite()});

    var file = std.fs.cwd().openFile("person.json", .{}) 
    catch |err| {
        std.debug.print("Failed to open file (err: {})\n", .{err});
        return;
    };
    defer file.close();

    const meta = file.metadata() catch {
        std.debug.print("Failed to read file metadata\n", .{});
        return;
    };
    const size: usize = @intCast(meta.size());

    const s = 
        file.readToEndAllocOptions(allocator, size, size, @alignOf(u8), null)
        catch {
            std.debug.print("Failed to read file contents\n", .{});
            return;
        };
    defer allocator.free(s);

    const len = reverseNames(s.ptr, s.len, s.len);

    std.debug.print("{s}\n (len: {})\n", .{s, len});
}

試行錯誤したときのゴミが残っている(残しておいた)が、通常のファイル読み込みと同じ感覚で実装でビルド通った。

wasmtimeで実行すると・・・

$ wasmtime main.wasm 
platform: true
File oprn flags: r/true, w/false
Failed to open file (err: error.Unexpected)

なんか失敗した。

これは、wasmtimeにIO対象のフォルダを教えてあげる必要があるため。
以下のようにすると期待通りの結果となった。

$ wasmtime run --dir . main.wasm
platform: true
File oprn flags: r/true, w/false
[{"id":1,"name":"nhoJ"},{"id":2,"name":"enaJ"}]ane", "id": 2 }
]
 (len: 47)