diff --git a/.github/workflows/examples.yaml b/.github/workflows/examples.yaml index 2be5c6e5..de715149 100644 --- a/.github/workflows/examples.yaml +++ b/.github/workflows/examples.yaml @@ -90,6 +90,9 @@ jobs: - name: Build Rust examples run: make build.examples.rust + - name: Build Zig examples + run: make build.examples.zig + - name: Build Emscripten examples run: | ./emsdk/emsdk activate ${{env.EMSDK_VERSION}} diff --git a/.gitignore b/.gitignore index 1b12d07f..59fdf45f 100644 --- a/.gitignore +++ b/.gitignore @@ -31,3 +31,6 @@ package-lock.json /coverage.txt .vagrant + +zig-cache/ +zig-out/ diff --git a/Makefile b/Makefile index 7b1b4f6e..f4eaf134 100644 --- a/Makefile +++ b/Makefile @@ -46,6 +46,13 @@ build.bench: build.examples.as: @cd ./examples/assemblyscript/testdata && npm install && npm run build +.PHONY: build.examples.zig +build.examples.zig: examples/allocation/zig/testdata/greet.wasm + +%.wasm: %.zig + @(cd $(@D); zig build) + @mv $(@D)/zig-out/lib/$(@F) $(@D) + tinygo_sources := $(wildcard examples/*/testdata/*.go examples/*/*/testdata/*.go examples/*/testdata/*/*.go) .PHONY: build.examples.tinygo build.examples.tinygo: $(tinygo_sources) diff --git a/examples/allocation/README.md b/examples/allocation/README.md index fae2ecfe..1f8059c1 100644 --- a/examples/allocation/README.md +++ b/examples/allocation/README.md @@ -14,11 +14,12 @@ for binary serialization. * [Rust](rust) - Calls Wasm built with `cargo build --release --target wasm32-unknown-unknown` * [TinyGo](tinygo) - Calls Wasm built with `tinygo build -o X.wasm -scheduler=none --no-debug -target=wasi X.go` +* [Zig](zig) - Calls Wasm built with `zig build` Note: Each of the above languages differ in both terms of exports and runtime behavior around allocation, because there is no WebAssembly specification for -it. For example, TinyGo exports allocation functions while Rust does not. Also, -Rust eagerly collects memory before returning from a Wasm function while TinyGo +it. For example, TinyGo exports allocation functions while Rust and Zig don't. +Also, Rust eagerly collects memory before returning from a Wasm function while TinyGo does not. We still try to keep the examples as close to the same as possible, and diff --git a/examples/allocation/zig/README.md b/examples/allocation/zig/README.md new file mode 100644 index 00000000..0d3f2a6c --- /dev/null +++ b/examples/allocation/zig/README.md @@ -0,0 +1,20 @@ +## Zig allocation example + +This example shows how to pass strings in and out of a Wasm function defined in Zig, built with `zig build`. + +Ex. +```bash +$ go run greet.go wazero +wasm >> Hello, wazero! +go >> Hello, wazero! +``` + +Under the covers, [greet.zig](testdata/greet.zig) does a few things of interest: +* Uses `@ptrToInt` to change a Zig pointer to a numeric type +* Uses `[*]u8` as an argument to take a pointer and slices it to build back a string + +The Zig code exports "malloc" and "free", which we use for that purpose. + +Note: This example uses `@panic()` rather than `unreachable` to handle errors +since `unreachable` emits a call to panic only in `Debug` and `ReleaseSafe` mode. +In `ReleaseFast` and `ReleaseSmall` mode, it would lead into undefined behavior. diff --git a/examples/allocation/zig/greet.go b/examples/allocation/zig/greet.go new file mode 100644 index 00000000..ac50652f --- /dev/null +++ b/examples/allocation/zig/greet.go @@ -0,0 +1,124 @@ +package main + +import ( + "context" + _ "embed" + "errors" + "fmt" + "log" + "os" + + "github.com/tetratelabs/wazero" + "github.com/tetratelabs/wazero/api" +) + +// greetWasm was compiled using `zig build` +// +//go:embed testdata/greet.wasm +var greetWasm []byte + +// main shows how to interact with a WebAssembly function that was compiled from Zig. +// +// See README.md for a full description. +func main() { + if err := run(); err != nil { + log.Panicln(err) + } +} + +func run() error { + // Choose the context to use for function calls. + ctx := context.Background() + + // Create a new WebAssembly Runtime. + r := wazero.NewRuntimeWithConfig(wazero.NewRuntimeConfig(). + // Enable WebAssembly 2.0 support. + WithWasmCore2(), + ) + defer r.Close(ctx) // This closes everything this Runtime created. + + // Instantiate a Go-defined module named "env" that exports a function to + // log to the console. + _, err := r.NewModuleBuilder("env"). + ExportFunction("log", logString). + Instantiate(ctx, r) + if err != nil { + return err + } + + // Instantiate a WebAssembly module that imports the "log" function defined + // in "env" and exports "memory" and functions we'll use in this example. + compiled, err := r.CompileModule(ctx, greetWasm, wazero.NewCompileConfig()) + if err != nil { + return err + } + + mod, err := r.InstantiateModule(ctx, compiled, wazero.NewModuleConfig().WithStdout(os.Stdout).WithStderr(os.Stderr)) + if err != nil { + return err + } + + // Get references to WebAssembly functions we'll use in this example. + greet := mod.ExportedFunction("greet") + greeting := mod.ExportedFunction("greeting") + + malloc := mod.ExportedFunction("malloc") + free := mod.ExportedFunction("free") + + // Let's use the argument to this main function in Wasm. + name := os.Args[1] + nameSize := uint64(len(name)) + + // Instead of an arbitrary memory offset, use Zig's allocator. Notice + // there is nothing string-specific in this allocation function. The same + // function could be used to pass binary serialized data to Wasm. + results, err := malloc.Call(ctx, nameSize) + if err != nil { + return err + } + namePtr := results[0] + if namePtr == 0 { + return errors.New("malloc failed") + } + // We have to free this pointer when finished. + defer free.Call(ctx, namePtr, nameSize) + + // The pointer is a linear memory offset, which is where we write the name. + if !mod.Memory().Write(ctx, uint32(namePtr), []byte(name)) { + return fmt.Errorf("Memory.Write(%d, %d) out of range of memory size %d", + namePtr, nameSize, mod.Memory().Size(ctx)) + } + + // Now, we can call "greet", which reads the string we wrote to memory! + _, err = greet.Call(ctx, namePtr, nameSize) + if err != nil { + return err + } + + // Finally, we get the greeting message "greet" printed. This shows how to + // read-back something allocated by Zig. + ptrSize, err := greeting.Call(ctx, namePtr, nameSize) + if err != nil { + return err + } + + greetingPtr := uint32(ptrSize[0] >> 32) + greetingSize := uint32(ptrSize[0]) + // The pointer is a linear memory offset, which is where we write the name. + if bytes, ok := mod.Memory().Read(ctx, greetingPtr, greetingSize); !ok { + return fmt.Errorf("Memory.Read(%d, %d) out of range of memory size %d", + greetingPtr, greetingSize, mod.Memory().Size(ctx)) + } else { + fmt.Println("go >>", string(bytes)) + } + + return nil +} + +func logString(ctx context.Context, m api.Module, offset, byteCount uint32) { + buf, ok := m.Memory().Read(ctx, offset, byteCount) + if !ok { + log.Panicf("Memory.Read(%d, %d) out of range", offset, byteCount) + } + fmt.Println(string(buf)) +} diff --git a/examples/allocation/zig/greet_test.go b/examples/allocation/zig/greet_test.go new file mode 100644 index 00000000..4a944c86 --- /dev/null +++ b/examples/allocation/zig/greet_test.go @@ -0,0 +1,18 @@ +package main + +import ( + "testing" + + "github.com/tetratelabs/wazero/internal/testing/maintester" + "github.com/tetratelabs/wazero/internal/testing/require" +) + +// Test_main ensures the following will work: +// +// go run greet.go wazero +func Test_main(t *testing.T) { + stdout, _ := maintester.TestMain(t, main, "greet", "wazero") + require.Equal(t, `wasm >> Hello, wazero! +go >> Hello, wazero! +`, stdout) +} diff --git a/examples/allocation/zig/testdata/build.zig b/examples/allocation/zig/testdata/build.zig new file mode 100644 index 00000000..2afa5141 --- /dev/null +++ b/examples/allocation/zig/testdata/build.zig @@ -0,0 +1,13 @@ +const std = @import("std"); +const CrossTarget = std.zig.CrossTarget; + +pub fn build(b: *std.build.Builder) void { + // Standard release options allow the person running `zig build` to select + // between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall. + const mode = b.standardReleaseOptions(); + + const lib = b.addSharedLibrary("greet", "greet.zig", .unversioned); + lib.setTarget(CrossTarget{ .cpu_arch = .wasm32, .os_tag = .freestanding }); + lib.setBuildMode(mode); + lib.install(); +} diff --git a/examples/allocation/zig/testdata/greet.wasm b/examples/allocation/zig/testdata/greet.wasm new file mode 100755 index 00000000..0f5ce483 Binary files /dev/null and b/examples/allocation/zig/testdata/greet.wasm differ diff --git a/examples/allocation/zig/testdata/greet.zig b/examples/allocation/zig/testdata/greet.zig new file mode 100644 index 00000000..f164037d --- /dev/null +++ b/examples/allocation/zig/testdata/greet.zig @@ -0,0 +1,65 @@ +const std = @import("std"); +const allocator = std.heap.page_allocator; + +extern "env" fn log(ptr: [*]const u8, size: u32) void; + +// _log prints a message to the console using log. +pub fn _log(message: []const u8) void { + log(message.ptr, message.len); +} + +pub export fn malloc(length: usize) ?[*]u8 { + const buff = allocator.alloc(u8, length) catch return null; + return buff.ptr; +} + +pub export fn free(buf: [*]u8, length: usize) void { + allocator.free(buf[0..length]); +} + +pub fn _greeting(name: []const u8) ![]u8 { + return try std.fmt.allocPrint( + allocator, + "Hello, {s}!", + .{name}, + ); +} + +// _greet prints a greeting to the console. +pub fn _greet(name: []const u8) !void { + const s = try std.fmt.allocPrint( + allocator, + "wasm >> {s}", + .{name}, + ); + _log(s); +} + +// greet is a WebAssembly export that accepts a string pointer (linear memory offset) and calls greet. +pub export fn greet(message: [*]const u8, size: u32) void { + const name = _greeting(message[0..size]) catch |err| @panic(switch (err) { + error.OutOfMemory => "out of memory", + else => "unexpected error", + }); + _greet(name) catch |err| @panic(switch (err) { + error.OutOfMemory => "out of memory", + else => "unexpected error", + }); +} + +// greeting is a WebAssembly export that accepts a string pointer (linear memory +// offset) and returns a pointer/size pair packed into a uint64. +// +// Note: This uses a uint64 instead of two result values for compatibility with +// WebAssembly 1.0. +pub export fn greeting(message: [*]const u8, size: u32) u64 { + const g = _greeting(message[0..size]) catch return 0; + return stringToPtr(g); +} + +// stringToPtr returns a pointer and size pair for the given string in a way +// compatible with WebAssembly numeric types. +pub fn stringToPtr(s: []const u8) u64 { + const p: u64 = @ptrToInt(s.ptr); + return p << 32 | s.len; +}