Skip to content

Support for global variables #9

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Aug 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 20 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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:
>
Expand Down Expand Up @@ -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 = {
Expand All @@ -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);
Expand All @@ -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;
}
};

```
14 changes: 14 additions & 0 deletions example/src/main.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
14 changes: 13 additions & 1 deletion example/static/script.js
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down
2 changes: 1 addition & 1 deletion simple/static/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
})();
Expand Down
163 changes: 142 additions & 21 deletions src/generate_js.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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], .{});
Expand Down Expand Up @@ -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) {
Expand All @@ -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 {
Expand All @@ -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();
Expand All @@ -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 = {
Expand All @@ -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;
}
Expand Down Expand Up @@ -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) {
Expand All @@ -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) {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -352,25 +363,100 @@ 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);
\\ this._handles.set(1, window);
\\ 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| {
Expand Down Expand Up @@ -456,6 +542,16 @@ const ArgType = enum {
object,
};

const GlobalType = enum {
i32,
i64,
u32,
u64,
f32,
f64,
bool,
};

const NameParser = struct {
slice: []const u8,

Expand All @@ -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;
}
Expand Down Expand Up @@ -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) => {
Expand All @@ -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));
\\ },
Expand Down
Loading