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.
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()
.
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.