-
Notifications
You must be signed in to change notification settings - Fork 3
JavaScript to Zig function conversion
The constructor of a Zig function type can be used to convert a JavaScript function into a Zig one:
const std = @import("std");
pub const Callback = fn (i32, []const u8) void;
pub fn setCallback(_: ?*const Callback) void {}
import { Callback } from './javascript-fn-example-1.zig';
function hello(number, text) {
console.log(`number = ${number}, text = ${text.string}`);
}
const callback = new Callback(hello);
console.log(callback);
[Function: hello] fn (i32, []const u8) void {
[Symbol(memory)]: DataView {
byteLength: 0,
byteOffset: 0,
buffer: ArrayBuffer {
[Uint8Contents]: <>,
byteLength: 0,
[Symbol(zig)]: [Object]
},
[Symbol(zig)]: { address: 140473954349096n, len: 0 }
}
}
The constructor creates a Zig-to-JavaScript bridge, a small dynamically generated Zig function that invokes the given JavaScript function. You can see its address in the print-out.
In the example above, we can pass callback
to setCallback()
. It wouldn't do anything,
of course, as it's not implemented yet. Let us do that now:
const std = @import("std");
const zigar = @import("zigar");
pub const Callback = fn (i32, []const u8) void;
pub fn none(_: i32, _: []const u8) void {}
var callback: *const Callback = &none;
pub fn setCallback(cb: ?*const Callback) void {
zigar.function.release(callback);
callback = cb orelse &none;
}
pub fn runCallback() void {
callback(123, "Hello world");
}
import { Callback, runCallback, setCallback } from './javascript-fn-example-2.zig';
function hello(number, text) {
console.log(`number = ${number}, text = ${text.string}`);
}
const callback = new Callback(hello);
setCallback(callback);
runCallback();
setCallback(null);
number = 123, text = Hello world
The Zig-to-JavaScript bridge sits in manually managed memory. When it's no longer needed you have
to free it using zigar.function.release()
. Besides releasing
memory on the Zig side, doing so also removing the reference on the JavaScript function, allowing
it to be garbage-collected eventually. If the function given is an ordinary Zig function (which
callback
points to initially) release()
does nothing.
Taking advantage of Zigar's auto-vification mechanism, you generally don't need to explicitly create the function object:
import { runCallback, setCallback } from './javascript-fn-example-2.zig';
setCallback((number, text) => console.log(`number = ${number}, text = ${text.string}`));
runCallback();
setCallback(null);
number = 123, text = Hello world
An Allocator
can be passed to the JavaScript side
when the function is expected to return variable-length data. Consult its documentation for
details.
If a JavaScript callback returns a
Promise
,
code execution on the Zig side will pause until the promise is resolved. The following example will
not work, however:
const std = @import("std");
const zigar = @import("zigar");
const ErrorSet = error{ Deadlock, Unexpected };
const Callback = fn () ErrorSet!i32;
pub fn none() ErrorSet!i32 {
return error.Unexpected;
}
var callback: *const Callback = &none;
pub fn setCallback(cb: ?*const Callback) void {
zigar.function.release(callback);
callback = cb orelse &none;
}
pub fn runCallback() void {
if (callback()) |result| {
std.debug.print("result = {d}\n", .{result});
} else |err| {
std.debug.print("error = {s}\n", .{@errorName(err)});
}
}
import { runCallback, setCallback } from './javascript-fn-example-3.zig';
setCallback(async () => 1234);
runCallback();
error = Deadlock
In the above code, because callback
is invoked in the main thread, the main thread would end up
waiting for itself. In real-world usage, Zig-to-JavaScript calls (both sync and async) will nearly
always occur in a different thread. The following works properly:
const std = @import("std");
const zigar = @import("zigar");
const ErrorSet = error{ Deadlock, Unexpected };
const Callback = fn () ErrorSet!i32;
fn none() ErrorSet!i32 {
return error.Unexpected;
}
pub fn start() !void {
try zigar.thread.use();
}
pub fn shutdown() void {
zigar.thread.end();
}
var callback: *const Callback = &none;
pub fn setCallback(cb: ?*const Callback) void {
zigar.function.release(callback);
callback = cb orelse &none;
}
pub fn runCallback() !void {
const thread = try std.Thread.spawn(.{}, runCallbackInThread, .{});
thread.detach();
}
pub fn runCallbackInThread() void {
if (callback()) |result| {
std.debug.print("result = {d}\n", .{result});
} else |err| {
std.debug.print("error = {s}\n", .{@errorName(err)});
}
}
import { runCallback, setCallback, start, shutdown } from './javascript-fn-example-4.zig';
start();
setTimeout(() => shutdown(), 50);
setCallback(async () => 1234);
runCallback();
result = 1234
When using threads in Node.js, you must call zigar.thread.use()
to tell the
main thread (where JavaScript execution occurs) to listen for call requests from other threads.
When your app is ready to exit, you should call zigar.thread.end()
.
This allows Node's event loop to terminate.
Zig code can receive data from JavaScript asynchronously using
Promise
and
Generator
.
Examples can be found in their respected pages.