Skip to content

Commit

Permalink
add zware-run to run wasm binaries from the command-line
Browse files Browse the repository at this point in the history
An initial version of a command-line program that can take a WASM binary
on the command line and a function name and execute it.

It will find missing imports and populate them with "stubs". The stub will
log the call and then populate the results with "zeroed" values. An
enhancement could be to take multiple files on the command line and link
them together before execution.

A few examples of binaries I could use with this was:

   - binaries from the test suite
   - WASM4 binaries
   - the Zig wasm executable

Of the ones I tested this seems like a viable strategy to quickly run
the binaries from the test suite.  The WASM4/Zig binaries would usually
just call a function or two and then hit an assert because of the "zeroed"
results that are produced by the stubs.
  • Loading branch information
marler8997 committed Oct 16, 2024
1 parent 9cd6f56 commit cfdd5e4
Show file tree
Hide file tree
Showing 9 changed files with 327 additions and 11 deletions.
18 changes: 18 additions & 0 deletions build.zig
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,24 @@ pub fn build(b: *Build) !void {

try b.modules.put(b.dupe("zware"), zware_module);

{
const exe = b.addExecutable(.{
.name = "zware-run",
.root_source_file = b.path("src/cmdlinerunner.zig"),
.target = target,
.optimize = optimize,
});
exe.root_module.addImport("zware", zware_module);
const install = b.addInstallArtifact(exe, .{});
b.getInstallStep().dependOn(&install.step);
const run = b.addRunArtifact(exe);
run.step.dependOn(&install.step);
if (b.args) |args| {
run.addArgs(args);
}
b.step("run", "Run the cmdline runner zware-run").dependOn(&run.step);
}

const lib = b.addStaticLibrary(.{
.name = "zware",
.root_source_file = b.path("src/main.zig"),
Expand Down
229 changes: 229 additions & 0 deletions src/cmdlinerunner.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
const std = @import("std");
const zware = @import("zware");

fn oom(e: error{OutOfMemory}) noreturn {
@panic(@errorName(e));
}

const ImportStub = struct {
module: []const u8,
name: []const u8,
type: zware.FuncType,
};

const enable_leak_detection = false;
const global = struct {
var allocator_instance = if (enable_leak_detection) std.heap.GeneralPurposeAllocator(.{
.retain_metadata = true,
//.verbose_log = true,
}){} else std.heap.ArenaAllocator.init(std.heap.page_allocator);
const alloc = allocator_instance.allocator();
var import_stubs: std.ArrayListUnmanaged(ImportStub) = .{};
};

pub fn main() !void {
try main2();
if (enable_leak_detection) {
switch (global.allocator_instance.deinit()) {
.ok => {},
.leak => @panic("memory leak"),
}
}
}
fn main2() !void {
defer global.import_stubs.deinit(global.alloc);

const full_cmdline = try std.process.argsAlloc(global.alloc);
defer std.process.argsFree(global.alloc, full_cmdline);
if (full_cmdline.len <= 1) {
try std.io.getStdErr().writer().writeAll("Usage: zware-run FILE.wasm FUNCTION\n");
std.process.exit(0xff);
}

const pos_args = full_cmdline[1..];
if (pos_args.len != 2) {
std.log.err("expected {} positional cmdline arguments but got {}", .{ 2, pos_args.len });
std.process.exit(0xff);
}
const wasm_path = pos_args[0];
const wasm_func_name = pos_args[1];

var store = zware.Store.init(global.alloc);
defer store.deinit();

const wasm_content = content_blk: {
var file = std.fs.cwd().openFile(wasm_path, .{}) catch |e| {
std.log.err("failed to open '{s}': {s}", .{ wasm_path, @errorName(e) });
std.process.exit(0xff);
};
defer file.close();
break :content_blk try file.readToEndAlloc(global.alloc, std.math.maxInt(usize));
};
defer global.alloc.free(wasm_content);

var module = zware.Module.init(global.alloc, wasm_content);
defer module.deinit();
try module.decode();

const export_funcidx = try getExportFunction(&module, wasm_func_name);
const export_funcdef = module.functions.list.items[export_funcidx];
const export_functype = try module.types.lookup(export_funcdef.typeidx);
if (export_functype.params.len != 0) {
std.log.err("calling a function with parameters is not implemented", .{});
std.process.exit(0xff);
}

var instance = zware.Instance.init(global.alloc, &store, module);
defer if (enable_leak_detection) instance.deinit();

try populateMissingImports(&store, &module);

var zware_error: zware.Error = undefined;
instance.instantiateWithError(&zware_error) catch |err| switch (err) {
error.SeeContext => {
std.log.err("failed to instantiate the module: {}", .{zware_error});
std.process.exit(0xff);
},
else => |e| return e,
};
defer instance.deinit();

var in = [_]u64{};
const out_args = try global.alloc.alloc(u64, export_functype.results.len);
defer global.alloc.free(out_args);
try instance.invoke(wasm_func_name, &in, out_args, .{});
std.log.info("{} output(s)", .{out_args.len});
for (out_args, 0..) |out_arg, out_index| {
std.log.info("output {} {}", .{ out_index, fmtValue(export_functype.results[out_index], out_arg) });
}
}

fn getExportFunction(module: *const zware.Module, func_name: []const u8) !usize {
return module.getExport(.Func, func_name) catch |err| switch (err) {
error.ExportNotFound => {
const stderr = std.io.getStdErr().writer();
var export_func_count: usize = 0;
for (module.exports.list.items) |exp| {
if (exp.tag == .Func) {
export_func_count += 1;
}
}
if (export_func_count == 0) {
try stderr.print("error: this wasm binary has no function exports\n", .{});
} else {
try stderr.print(
"error: no export function named '{s}', pick from one of the following {} export(s):\n",
.{ func_name, export_func_count },
);
for (module.exports.list.items) |exp| {
if (exp.tag == .Func) {
try stderr.print(" {s}\n", .{exp.name});
}
}
}
std.process.exit(0xff);
},
};
}

fn populateMissingImports(store: *zware.Store, module: *const zware.Module) !void {
var import_funcidx: u32 = 0;
var import_memidx: u32 = 0;
for (module.imports.list.items, 0..) |import, import_index| {
defer switch (import.desc_tag) {
.Func => import_funcidx += 1,
.Mem => import_memidx += 1,
else => @panic("todo"),
};

if (store.import(import.module, import.name, import.desc_tag)) |_| {
continue;
} else |err| switch (err) {
error.ImportNotFound => {},
}

switch (import.desc_tag) {
.Func => {
const funcdef = module.functions.list.items[import_funcidx];
std.debug.assert(funcdef.import == import_funcidx);
const functype = try module.types.lookup(funcdef.typeidx);
global.import_stubs.append(global.alloc, .{
.module = import.module,
.name = import.name,
.type = functype,
}) catch |e| oom(e);
store.exposeHostFunction(
import.module,
import.name,
onMissingImport,
global.import_stubs.items.len - 1,
functype.params,
functype.results,
) catch |e2| oom(e2);
},
.Mem => {
const memdef = module.memories.list.items[import_memidx];
std.debug.assert(memdef.import.? == import_index);
try store.exposeMemory(import.module, import.name, memdef.limits.min, memdef.limits.max);
},
else => |tag| std.debug.panic("todo: handle import {s}", .{@tagName(tag)}),
}
}
}

fn onMissingImport(vm: *zware.VirtualMachine, context: usize) zware.WasmError!void {
const stub = global.import_stubs.items[context];
std.log.info("import function '{s}.{s}' called", .{ stub.module, stub.name });
for (stub.type.params, 0..) |param_type, i| {
const value = vm.popAnyOperand();
std.log.info(" param {} {}", .{ i, fmtValue(param_type, value) });
}
for (stub.type.results, 0..) |result_type, i| {
std.log.info(" result {} {}", .{ i, fmtValue(result_type, 0) });
try vm.pushOperand(u64, 0);
}
}

pub fn Native(comptime self: zware.ValType) type {
return switch (self) {
.I32 => i32,
.I64 => i64,
.F32 => f32,
.F64 => f64,
.V128 => u64,
.FuncRef => u64,
.ExternRef => u64,
};
}

fn cast(comptime val_type: zware.ValType, value: u64) Native(val_type) {
return switch (val_type) {
.I32 => @bitCast(@as(u32, @intCast(value))),
.I64 => @bitCast(value),
.F32 => @bitCast(@as(u32, @intCast(value))),
.F64 => @bitCast(value),
.V128 => value,
.FuncRef => value,
.ExternRef => value,
};
}

fn fmtValue(val_type: zware.ValType, value: u64) FmtValue {
return .{ .type = val_type, .value = value };
}
const FmtValue = struct {
type: zware.ValType,
value: u64,
pub fn format(
self: FmtValue,
comptime fmt: []const u8,
options: std.fmt.FormatOptions,
writer: anytype,
) !void {
_ = fmt;
_ = options;
switch (self.type) {
inline else => |t2| try writer.print("({s}) {}", .{ @tagName(t2), cast(t2, self.value) }),
}
}
};
44 changes: 44 additions & 0 deletions src/error.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
const std = @import("std");

const module = @import("module.zig");

/// A function can take a reference to this to pass extra error information to the caller.
/// A function that does this guarantees the reference will be populated if it returns error.SeeContext.
/// Error implements a format function.
/// The same error instance can be re-used for multiple calls.
///
/// Example usage:
/// ----
/// var zware_error: Error = undefined;
/// foo(&zware_error) catch |err| switch (err) {
/// error.SeeContext => std.log.err("foo failed: {}", .{zware_error}),
/// else => |err| return err,
/// };
/// ---
pub const Error = union(enum) {
missing_import: module.Import,
any: anyerror,

/// Called by a function that wants to both populate this error instance and let the caller
/// know it's been populated by returning error.SeeContext.
pub fn set(self: *Error, e: Error) error{SeeContext} {
self.* = e;
return error.SeeContext;
}
pub fn format(
self: Error,
comptime fmt: []const u8,
options: std.fmt.FormatOptions,
writer: anytype,
) !void {
_ = fmt;
_ = options;
switch (self) {
.missing_import => |import| try writer.print(
"missing {s} import '{s}' from module '{s}'",
.{ @tagName(import.desc_tag), import.name, import.module },
),
.any => |e| try writer.print("{s}", .{@errorName(e)}),
}
}
};
21 changes: 16 additions & 5 deletions src/instance.zig
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ const math = std.math;
const posix = std.posix;
const wasi = std.os.wasi;
const ArrayList = std.ArrayList;
const Error = @import("error.zig").Error;
const Module = @import("module.zig").Module;
const Store = @import("store.zig").ArrayListStore;
const Function = @import("store/function.zig").Function;
Expand Down Expand Up @@ -128,9 +129,17 @@ pub const Instance = struct {
}

pub fn instantiate(self: *Instance) !void {
var err: Error = undefined;
self.instantiate2(&err) catch switch (err) {
.missing_import => return error.MissingImport,
.any => |e| return e,
};
}

pub fn instantiateWithError(self: *Instance, err: *Error) !void {
if (self.module.decoded == false) return error.ModuleNotDecoded;

try self.instantiateImports();
try self.instantiateImports(err);
try self.instantiateFunctions();
try self.instantiateGlobals();
try self.instantiateMemories();
Expand All @@ -143,9 +152,11 @@ pub const Instance = struct {
}
}

fn instantiateImports(self: *Instance) !void {
fn instantiateImports(self: *Instance, err: *Error) error{ OutOfMemory, SeeContext }!void {
for (self.module.imports.list.items) |import| {
const import_handle = try self.store.import(import.module, import.name, import.desc_tag);
const import_handle = self.store.import(import.module, import.name, import.desc_tag) catch |e| switch (e) {
error.ImportNotFound => return err.set(.{ .missing_import = import }),
};
switch (import.desc_tag) {
.Func => try self.funcaddrs.append(import_handle),
.Mem => try self.memaddrs.append(import_handle),
Expand Down Expand Up @@ -361,7 +372,7 @@ pub const Instance = struct {
},
.host_function => |host_func| {
var vm = VirtualMachine.init(op_stack[0..], frame_stack[0..], label_stack[0..], self);
try host_func.func(&vm);
try host_func.func(&vm, host_func.context);
},
}
}
Expand Down Expand Up @@ -404,7 +415,7 @@ pub const Instance = struct {
},
.host_function => |host_func| {
var vm = VirtualMachine.init(op_stack[0..], frame_stack[0..], label_stack[0..], self);
try host_func.func(&vm);
try host_func.func(&vm, host_func.context);
},
}
}
Expand Down
4 changes: 2 additions & 2 deletions src/instance/vm.zig
Original file line number Diff line number Diff line change
Expand Up @@ -295,7 +295,7 @@ pub const VirtualMachine = struct {
next_ip = f.start;
},
.host_function => |hf| {
try hf.func(self);
try hf.func(self, hf.context);
next_ip = ip + 1;
},
}
Expand Down Expand Up @@ -350,7 +350,7 @@ pub const VirtualMachine = struct {
next_ip = func.start;
},
.host_function => |host_func| {
try host_func.func(self);
try host_func.func(self, host_func.context);

next_ip = ip + 1;
},
Expand Down
2 changes: 2 additions & 0 deletions src/main.zig
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
pub const Error = @import("error.zig").Error;
pub const Module = @import("module.zig").Module;
pub const FuncType = @import("module.zig").FuncType;
pub const Instance = @import("instance.zig").Instance;
pub const VirtualMachine = @import("instance/vm.zig").VirtualMachine;
pub const WasmError = @import("instance/vm.zig").WasmError;
Expand Down
Loading

0 comments on commit cfdd5e4

Please sign in to comment.