Adds Zig example (#741)

Signed-off-by: knqyf263 <knqyf263@gmail.com>
This commit is contained in:
Teppei Fukuda
2022-08-11 11:31:06 +03:00
committed by GitHub
parent ec1f595225
commit bed4959f6d
10 changed files with 256 additions and 2 deletions

View File

@@ -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

View File

@@ -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.

View File

@@ -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))
}

View File

@@ -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)
}

View File

@@ -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();
}

BIN
examples/allocation/zig/testdata/greet.wasm vendored Executable file

Binary file not shown.

View File

@@ -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;
}