ZigでWASMのお勉強
01 add
-
root.zig
を作成し編集
extern fn print(i32) void;
export fn add(a: i32, b: i32) i32 {
return a + b;
}
- ビルド
zig build-lib root.zig -target wasm32-freestanding -fno-entry --export=add -O ReleaseFast
-
Zig 0.14
でも0.12
のオプションでビルドOK - 成果物として、
libroot.a
とlibroot.a.o
の2つが生成される- このうち
libroot.a.o
がwasm
- このうち
-
wasm2wat libroot.a.o
でWAT
フォーマットで生成結果を確認できる
(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_pointer
とmemory
がエクスポートされている。
$ 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
実験機能の警告は出るが、正しく実行できてる。
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);
}
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
は不要で、コンパイラがいい感じにやってくれて便利。
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)