Skip to content

JavaScript to Zig function conversion

Chung Leong edited this page Mar 15, 2025 · 3 revisions

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

Allocator

An Allocator can be passed to the JavaScript side when the function is expected to return variable-length data. Consult its documentation for details.

Async function

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.

Clone this wiki locally