Promise - chung-leong/zigar GitHub Wiki

The zigar.function.Promise parametric struct provides an interface to a JavaScript Promise object. It's used to asynchronously return data to JavaScript code. It can also be used to receive data from JavaScript asynchronously.

JavaScript-to-Zig function calls

When a Zig function accepts a Promise as an argument, it becomes an async function on the JavaScript side:

const std = @import("std");
const zigar = @import("zigar");

const Promise = zigar.function.Promise(error{OutOfMemory}![]const u8);

pub fn getText(a: std.mem.Allocator, promise: Promise) void {
    promise.resolve(a.dupe(u8, "Hello world"));
}
import { getText } from './promise-example-1.zig';

const text = await getText();
console.log(text.string);
Hello world

Like Allocator, Promise is automatically provided by Zigar. On the JavaScript side getText() therefore has zero required arguments. It does accept an optional argument: options, which may contain callback:

import { getText } from './promise-example-1.zig';

const callback = (err, text) => {
    if (!err) {
        console.log(text.string);
    }
};
getText({ callback });

Providing a callback overrides the default behavior. The function no longer returns a Promise. The result of the call is sent to the callback instead.

If the function return anything other than void or null, the promise would be resolved with that value. This behavior mainly facilitates error handling:

const std = @import("std");
const zigar = @import("zigar");

const Promise = zigar.function.Promise(error{OutOfMemory}![]const u8);

pub fn getText(_: std.mem.Allocator, _: Promise) !void {
    return error.DingoAteMyBaby;
}
import { getText } from './promise-example-2.zig';

const text = await getText();
console.log(text.string);
Error: Dingo ate my baby
    at getText (<anonymous>:1:26374)
    at file:///home/cleong/zigar.wiki/examples/promise-example-2.js:3:20 {
  number: 101
}

Generally, you would perform time-consuming tasks in separate threads. In a order to resolve a promise in a different thread, you need to call zigar.thread.use() to make the main thread listen for function call requests from outside it. At the end of your program, you need to call zigar.thread.end() to allow the event loop to terminate.

const std = @import("std");
const zigar = @import("zigar");

const Promise = zigar.function.Promise(error{OutOfMemory}![]const u8);

pub fn start() !void {
    try zigar.thread.use();
}

pub fn stop() void {
    zigar.thread.end();
}

pub fn getText(a: std.mem.Allocator, promise: Promise) !void {
    const thread = try std.Thread.spawn(.{}, returnText, .{ a, promise });
    thread.detach();
}

fn returnText(a: std.mem.Allocator, promise: Promise) void {
    promise.resolve(a.dupe(u8, "Hello world"));
}
import { getText, start, stop } from './promise-example-3.zig';

try {
    start();
    const text = await getText();
    console.log(text.string);
} finally {
    stop();
}
Hello world

You can use zigar.thread.WorkQueue, a parametric struct that contains a queue and a thread pool, to handle the dispatching of asynchronous tasks:

const std = @import("std");
const zigar = @import("zigar");

var work_queue: zigar.thread.WorkQueue(thread_ns) = .{};

pub fn start(threads: usize) !void {
    try work_queue.init(.{
        .allocator = zigar.mem.getDefaultAllocator(),
        .n_jobs = threads,
    });
}

pub fn stop(promise: zigar.function.Promise(void)) void {
    work_queue.deinitAsync(promise);
}

const Promise = zigar.function.PromiseOf(thread_ns.calcFactorial);

pub fn calcFactorial(n: u32, promise: Promise) !void {
    try work_queue.push(thread_ns.calcFactorial, .{n}, promise);
}

const thread_ns = struct {
    pub fn calcFactorial(n: u32) !u4096 {
        if (n == 0) return 1;
        var f: u4096 = n;
        var i = n - 1;
        while (i > 0) : (i -= 1) {
            f, const overflow_bit = @mulWithOverflow(f, i);
            if (overflow_bit != 0) return error.IntegerOverflow;
        }
        return f;
    }
};
import { calcFactorial, start, stop } from './promise-example-4.zig';
import { availableParallelism } from 'os';

try {
    start(availableParallelism());
    const f = await calcFactorial(500);
    console.log(f);
} finally {
    await stop();
}
1220136825991110068701238785423046926253574342803192842192413588385845373153881
9976054964475022032818630136164771482035841633787220781772004807852051593292854
7790757193933060377296085908627042917454788242491272634430567017327076946106280
2310452644218878789465754777149863494367781037644274033827365397471386477878495
4384895955375379904232410612713269843277457155463099772027810145610811883737095
3101635632443298702956389662891165897476957208792692887128178007026517450776841
0719624390394322536422605234945850129918571501248706961568141625359056693423813
0088562492468915641267756544818865065938479517753608940057452389403357984763639
4490531306232374906644504882466507594673586207463792518420045936969298102226397
1952597190945217823331756934581508552332820762820023402626907898342451712006207
7146409794561161276291459512372299133401695523638509428855920187274337951730145
8635757082835578015873543276888868012039988238470215146760544540766353598417443
0480128938313896881639487469658817504506926365338175055478128640000000000000000
0000000000000000000000000000000000000000000000000000000000000000000000000000000
00000000000000000000000000000n

WorkQueue() expects a namespace. Its push() method accepts any public function in this namespace as the first argument, then a tuple containing arguments to that function, followed by a Promise to be resolved. In the code above we use zigar.function.PromiseOf() to define a promise type based on the return value of calcFactorial().

Zig-to-JavaScript function calls

Promise can be used to receive results asynchronously from JavaScript:

const std = @import("std");
const zigar = @import("zigar");

const Promise = zigar.function.Promise([]const u8);

pub fn call(cb: *const fn (Promise) void) void {
    cb(.{ .ptr = null, .callback = &callback });
}

fn callback(_: ?*anyopaque, s: []const u8) void {
    std.debug.print("received = {s}\n", .{s});
}
import { call } from './promise-example-5.zig';

call(async () => 'Hello world');
received = Hello world

The default value for ptr is null. It's explicitly set in the code above only for clarity purpose.

As in the case for JavaScript-to-Zig call, there's support for using a callback function instead of promise:

import { call } from './promise-example-5.zig';

call(({ callback }) => callback('Hello'));
call(({ callback }) => callback(null, 'World'));
received = Hello
received = World

You can pass both return value and error as the first argument (i.e. treating the argument as an Zig error union) or you can pass them separately (Node.js convention).

Unlike with synchronous calls, you can return JavaScript memory to Zig code with an async call since it involves a callback function. Zigar can guarantee the memory will remain valid in the duration of the call. In situations where the memory need to persist beyond the call, you may choose to pass an Allocator to the JavaScript side:

const std = @import("std");
const zigar = @import("zigar");

var gpa = std.heap.GeneralPurposeAllocator(.{}){};
const allocator = gpa.allocator();

const ErrorSet = error{ OutOfMemory, Unexpected };
const Promise = zigar.function.Promise(ErrorSet![]const u8);

pub fn call(cb: *const fn (std.mem.Allocator, Promise) void) void {
    const promise = Promise.init(&allocator, callback);
    cb(allocator, promise);
}

fn callback(a: *std.mem.Allocator, payload: Promise.payload) void {
    if (payload) |s| {
        std.debug.print("received = {s}\n", .{s});
        a.free(s);
    } else |err| {
        std.debug.print("error = {s}\n", .{@errorName(err)});
    }
}
import { call } from './promise-example-6.zig';

call(async () => 'Hello world');
received = Hello world

In this example, we're using Promise.init() to create the promise object. This function will conviniently cast any function with a compatible signature to *const fn (?*anyopaque, T) so you don't need to unbox the optional and perform a cast yourself. For demonstrative purpose we're passing a pointer to allocator here.

We're also using Promise.payload as the payload type, which is now an error union. In the previous example, if an error occurs on the JavaScript side, the result is a panic since there's no reasonable way to deal with it. Here an error can be returned:

import { call } from './promise-example-6.zig';

call(async () => { throw new Error('Out of memory') });
call(async () => { throw new Error('Dingo ate my baby') });
error = OutOfMemory
error = Unexpected

Unexpected is the "catch-all" error. Having it as part of the error set ensures that our code would never panic.

⚠️ **GitHub.com Fallback** ⚠️