diff --git a/README.md b/README.md index 4a66ecc..2e0ce30 100644 --- a/README.md +++ b/README.md @@ -62,12 +62,16 @@ Zjb functions which return a value from Javascript require specifying which type - `void` is a valid type for method calls which have no return value. Zjb supports multiple ways to expose Zig functions to Javascript: -- `zjb.exportFn` exposes the function with the passed name to Javascript. This supports `zjb.Handle`, so if you pass an object from a Javascript function, a handle will automaticlaly be created and passed into Zig. It is the responsibility of the Zig function being called to call `release` on any handles in its arguments at the appropriate time to avoid memory leaks. +- `zjb.exportFn` exposes the function with the passed name to Javascript. This supports `zjb.Handle`, so if you pass an object from a Javascript function, a handle will automatically be created and passed into Zig. It is the responsibility of the Zig function being called to call `release` on any handles in its arguments at the appropriate time to avoid memory leaks. - `zjb.fnHandle` uses `zjb.exportFn` and additionally returns a `zjb.ConstHandle` to that function. This can be used as a callback argument in Javascript functions. - Zig's `export` keyword on functions works as it always does in WASM, but doesn't support `zjb.Handle` correctly. +Simple Zig global variables can also be exposed to Javascript: +- `zjb.exportGlobal` exposes the variable with the passed address to Javascript. This supports `bool`, `i32`, `i64`, `u32`, `u64`, `f32`, and `f64`. Property descriptors will be created with get/set methods that provide access to the variable. + A few extra notes: + `zjb.string([]const u8)` decodes the slice of memory as a utf-8 string, returning a Handle. The string will NOT update to reflect changes in the slice in Zig. `zjb.global` will be set to the value of that global variable the first time it is called. As it is intended to be used for Javascript objects or classes defined in the global scope, that usage will be safe. For example, `console`, `document` or `Map`. If you use it to retrieve a value or object you've defined in Javascript, ensure it's defined before your program runs and doesn't change. @@ -76,6 +80,8 @@ The \_ArrayView functions (`i8ArrayView`, `u8ArrayView`, etc) create the respect `dataView` is similar in functionality to the ArrayView functions, but returns a DataView object. Accepts any pointer or slice. +The generated Javascript also includes a shortcut function named `dataView` to get an up-to-date cached `DataView` of the entire WebAssembly `Memory`. + > [!CAUTION] > There are three important notes about using the \_ArrayView and dataView functions: > @@ -108,6 +114,12 @@ const Zjb = class { this._next_handle++; return result; } + dataView() { + if (this._cached_data_view.buffer.byteLength !== this.instance.exports.memory.buffer.byteLength) { + this._cached_data_view = new DataView(this.instance.exports.memory.buffer); + } + return this._cached_data_view; + } constructor() { this._decoder = new TextDecoder(); this.imports = { @@ -123,6 +135,8 @@ const Zjb = class { }; this.exports = { }; + this.instance = null; + this._cached_data_view = null; this._export_reverse_handles = {}; this._handles = new Map(); this._handles.set(0, null); @@ -131,6 +145,11 @@ const Zjb = class { this._handles.set(3, this.exports); this._next_handle = 4; } + setInstance(instance) { + this.instance = instance; + const initialView = new DataView(instance.exports.memory.buffer); + this._cached_data_view = initialView; + } }; ``` diff --git a/example/src/main.zig b/example/src/main.zig index d3872be..0f0252d 100644 --- a/example/src/main.zig +++ b/example/src/main.zig @@ -165,6 +165,20 @@ fn incrementAndGet(increment: i32) callconv(.C) i32 { return value; } +var test_var: f32 = 1337.7331; +fn checkTestVar() callconv(.C) f32 { + return test_var; +} + +fn setTestVar() callconv(.C) f32 { + test_var = 42.24; + return test_var; +} + comptime { zjb.exportFn("incrementAndGet", incrementAndGet); + + zjb.exportGlobal("test_var", &test_var); + zjb.exportFn("checkTestVar", checkTestVar); + zjb.exportFn("setTestVar", setTestVar); } diff --git a/example/static/script.js b/example/static/script.js index 6e9e950..6ae2d89 100644 --- a/example/static/script.js +++ b/example/static/script.js @@ -7,8 +7,20 @@ var zjb = new Zjb(); (function() { WebAssembly.instantiateStreaming(fetch("example.wasm"), {env: env, zjb: zjb.imports}).then(function (results) { - zjb.instance = results.instance; + zjb.setInstance(results.instance); results.instance.exports.main(); + + console.log("reading zjb global from zig", zjb.exports.checkTestVar()); + console.log("reading zjb global from javascript", zjb.exports.test_var); + + console.log("writing zjb global from zig", zjb.exports.setTestVar()); + console.log("reading zjb global from zig", zjb.exports.checkTestVar()); + console.log("reading zjb global from javascript", zjb.exports.test_var); + + console.log("writing zjb global from javascript", zjb.exports.test_var = 80.80); + console.log("reading zjb global from zig", zjb.exports.checkTestVar()); + console.log("reading zjb global from javascript", zjb.exports.test_var); + console.log("calling zjb exports from javascript", zjb.exports.incrementAndGet(1)); console.log("calling zjb exports from javascript", zjb.exports.incrementAndGet(1)); console.log("calling zjb exports from javascript", zjb.exports.incrementAndGet(1)); diff --git a/simple/static/index.html b/simple/static/index.html index 900d6b4..78a23fe 100644 --- a/simple/static/index.html +++ b/simple/static/index.html @@ -16,7 +16,7 @@ (function() { WebAssembly.instantiateStreaming(fetch("simple.wasm"), {env: env, zjb: zjb.imports}).then(function (results) { - zjb.instance = results.instance; + zjb.setInstance(results.instance); results.instance.exports.main(); }); })(); diff --git a/src/generate_js.zig b/src/generate_js.zig index 35e26a6..2bd1bbb 100644 --- a/src/generate_js.zig +++ b/src/generate_js.zig @@ -11,10 +11,12 @@ pub fn main() !void { return ExtractError.BadArguments; } - var imports = std.ArrayList([]const u8).init(alloc); - defer imports.deinit(); - var exports = std.ArrayList([]const u8).init(alloc); - defer exports.deinit(); + var importFunctions = std.ArrayList([]const u8).init(alloc); + defer importFunctions.deinit(); + var exportFunctions = std.ArrayList([]const u8).init(alloc); + defer exportFunctions.deinit(); + var exportGlobals = std.ArrayList([]const u8).init(alloc); + defer exportGlobals.deinit(); { var file = try std.fs.openFileAbsolute(args[3], .{}); @@ -56,7 +58,7 @@ pub fn main() !void { if (desc_type != 0) { // Not a function? return ExtractError.ImportTypeNotSupported; } - try imports.append(try alloc.dupe(u8, name)); + try importFunctions.append(try alloc.dupe(u8, name)); } } } else if (section_id == 7) { @@ -70,7 +72,9 @@ pub fn main() !void { _ = desc_index; if (desc_type == 0 and std.mem.startsWith(u8, name, "zjb_fn")) { - try exports.append(try alloc.dupe(u8, name)); + try exportFunctions.append(try alloc.dupe(u8, name)); + } else if (std.mem.startsWith(u8, name, "zjb_global")) { + try exportGlobals.append(try alloc.dupe(u8, name)); } } } else { @@ -79,8 +83,9 @@ pub fn main() !void { } } - std.sort.insertion([]const u8, imports.items, {}, strBefore); - std.sort.insertion([]const u8, exports.items, {}, strBefore); + std.sort.insertion([]const u8, importFunctions.items, {}, strBefore); + std.sort.insertion([]const u8, exportFunctions.items, {}, strBefore); + std.sort.insertion([]const u8, exportGlobals.items, {}, strBefore); var out_file = try std.fs.createFileAbsolute(args[1], .{}); defer out_file.close(); @@ -99,6 +104,12 @@ pub fn main() !void { \\ this._next_handle++; \\ return result; \\ } + \\ dataView() { + \\ if (this._cached_data_view.buffer.byteLength !== this.instance.exports.memory.buffer.byteLength) { + \\ this._cached_data_view = new DataView(this.instance.exports.memory.buffer); + \\ } + \\ return this._cached_data_view; + \\ } \\ constructor() { \\ this._decoder = new TextDecoder(); \\ this.imports = { @@ -109,7 +120,7 @@ pub fn main() !void { var func_args = std.ArrayList(ArgType).init(alloc); defer func_args.deinit(); - implement_functions: for (imports.items) |func| { + implement_functions: for (importFunctions.items) |func| { if (std.mem.eql(u8, lastFunc, func)) { continue; } @@ -160,7 +171,7 @@ pub fn main() !void { if (method != .get) { while (!(np.maybe("_") or np.slice.len == 0)) { - try func_args.append(try np.mustType()); + try func_args.append(try np.mustArgType()); } } switch (method) { @@ -185,7 +196,7 @@ pub fn main() !void { const ret_type = switch (method) { .new => ArgType.object, .set, .indexSet => ArgType.void, - else => try np.mustType(), + else => try np.mustArgType(), }; switch (method) { @@ -275,17 +286,17 @@ pub fn main() !void { var export_names = std.ArrayList([]const u8).init(alloc); defer export_names.deinit(); - for (exports.items) |func| { + for (exportFunctions.items) |func| { func_args.clearRetainingCapacity(); var np = NameParser{ .slice = func }; try np.must("zjb_fn_"); while (!(np.maybe("_") or np.slice.len == 0)) { - try func_args.append(try np.mustType()); + try func_args.append(try np.mustArgType()); } - const ret_type = try np.mustType(); + const ret_type = try np.mustArgType(); try np.must("_"); const name = np.slice; @@ -352,9 +363,12 @@ pub fn main() !void { } try writer.writeAll(";\n },\n"); } - try writer.writeAll(" };\n"); // end exports + + try writer.writeAll(" };\n"); // end export object try writer.writeAll( + \\ this.instance = null; + \\ this._cached_data_view = null; \\ this._export_reverse_handles = {}; \\ this._handles = new Map(); \\ this._handles.set(0, null); @@ -362,15 +376,87 @@ pub fn main() !void { \\ this._handles.set(2, ""); \\ this._handles.set(3, this.exports); \\ this._next_handle = 4; + \\ } \\ - ); + ); // end constructor try writer.writeAll( - \\ } - \\}; + \\ setInstance(instance) { + \\ this.instance = instance; + \\ const initialView = new DataView(instance.exports.memory.buffer); + \\ this._cached_data_view = initialView; \\ ); + for (exportGlobals.items) |global| { + var np = NameParser{ .slice = global }; + try np.must("zjb_global_"); + + const valueType = try np.mustGlobalType(); + + try np.must("_"); + + const name = np.slice; + try writer.writeAll(" {\n"); + try writer.writeAll(" const ptr = initialView.getUint32(instance.exports."); + try writer.writeAll(global); + try writer.writeAll(".value, true);\n"); + + try writer.writeAll(" Object.defineProperty(this.exports, \""); + try writer.writeAll(name); + try writer.writeAll("\", {\n"); + + switch (valueType) { + .i32 => try writer.writeAll( + \\ get: () => this.dataView().getInt32(ptr, true), + \\ set: v => this.dataView().setInt32(ptr, v, true), + \\ + ), + .i64 => try writer.writeAll( + \\ get: () => this.dataView().getBigInt64(ptr, true), + \\ set: v => this.dataView().setBigInt64(ptr, v, true), + \\ + ), + .u32 => try writer.writeAll( + \\ get: () => this.dataView().getUint32(ptr, true), + \\ set: v => this.dataView().setUint32(ptr, v, true), + \\ + ), + .u64 => try writer.writeAll( + \\ get: () => this.dataView().getBigUint64(ptr, true), + \\ set: v => this.dataView().setBigUint64(ptr, v, true), + \\ + ), + .f32 => try writer.writeAll( + \\ get: () => this.dataView().getFloat32(ptr, true), + \\ set: v => this.dataView().setFloat32(ptr, v, true), + \\ + ), + .f64 => try writer.writeAll( + \\ get: () => this.dataView().getFloat64(ptr, true), + \\ set: v => this.dataView().setFloat64(ptr, v, true), + \\ + ), + .bool => try writer.writeAll( + \\ get: () => Boolean(this.dataView().getUint8(ptr, true)), + \\ set: v => this.dataView().setUint8(ptr, v ? 1 : 0, true), + \\ + ), + } + + try writer.writeAll( + \\ enumerable: true, + \\ }); + \\ + ); + + try writer.writeAll(" }\n"); // end global + } + + try writer.writeAll(" }\n"); // end setInstance + + try writer.writeAll("};\n"); // end class + std.sort.insertion([]const u8, export_names.items, {}, strBefore); if (export_names.items.len > 1) { for (0..export_names.items.len - 1) |i| { @@ -456,6 +542,16 @@ const ArgType = enum { object, }; +const GlobalType = enum { + i32, + i64, + u32, + u64, + f32, + f64, + bool, +}; + const NameParser = struct { slice: []const u8, @@ -481,7 +577,32 @@ const NameParser = struct { } } - fn mustType(self: *NameParser) !ArgType { + fn mustGlobalType(self: *NameParser) !GlobalType { + if (self.maybe("i32")) { + return .i32; + } + if (self.maybe("i64")) { + return .i64; + } + if (self.maybe("u32")) { + return .u32; + } + if (self.maybe("u64")) { + return .u64; + } + if (self.maybe("f32")) { + return .f32; + } + if (self.maybe("f64")) { + return .f64; + } + if (self.maybe("bool")) { + return .bool; + } + return ExtractError.InvalidExportedName; + } + + fn mustArgType(self: *NameParser) !ArgType { if (self.maybe("n")) { return .number; } @@ -510,7 +631,7 @@ const builtins = [_][]const u8{ \\ }, , \\ "dataview": (ptr, len) => { - \\ return this.new_handle(new DataView(this.instance.exports.memory.buffer,ptr, len)); + \\ return this.new_handle(new DataView(this.instance.exports.memory.buffer, ptr, len)); \\ }, , \\ "throw": (id) => { @@ -530,7 +651,7 @@ const builtins = [_][]const u8{ \\ "handleCount": () => { \\ return this._handles.size; \\ }, - + , \\ "i8ArrayView": (ptr, len) => { \\ return this.new_handle(new Int8Array(this.instance.exports.memory.buffer, ptr, len)); \\ }, diff --git a/src/zjb.zig b/src/zjb.zig index 168983b..ab00d54 100644 --- a/src/zjb.zig +++ b/src/zjb.zig @@ -51,6 +51,13 @@ pub fn fnHandle(comptime name: []const u8, comptime f: anytype) ConstHandle { }.get(); } +pub fn exportGlobal(comptime name: []const u8, comptime value: anytype) void { + const T = @TypeOf(value.*); + validateGlobalType(T); + + return @export(value, .{ .name = "zjb_global_" ++ @typeName(T) ++ "_" ++ name }); +} + pub fn exportFn(comptime name: []const u8, comptime f: anytype) void { comptime var export_name: []const u8 = "zjb_fn_"; const type_info = @typeInfo(@TypeOf(f)).Fn; @@ -306,6 +313,13 @@ fn validateFromJavascriptArgumentType(comptime T: type) void { } } +fn validateGlobalType(comptime T: type) void { + switch (T) { + bool, i32, i64, u32, u64, f32, f64 => {}, + else => @compileError("unexpected type " ++ @typeName(T) ++ ". Supported types here: bool, i32, i64, u32, u64, f32, f64."), + } +} + fn shortTypeName(comptime T: type) []const u8 { return switch (T) { Handle, ConstHandle => "o",