Abort signal - chung-leong/zigar GitHub Wiki
The zigar.function.AbortSignal
struct provides an interface to a JavaScript
AbortSignal object. It gives you
the ability to terminate a time-consuming asychronous task. It's always used alongside a
Promise
.
JavaScript-to-Zig function calls
Like other special arguments like Allocator and Promise
, Zigar will
provide an AbortSignal
automatically on the JavaScript side when it's in a function's list of
arguments. This signal would be completely non-functional though, since you wouldn't have a mean
to activate it. To actually abort an async operation, you must create your own
AbortController
and pass its
signal:
const std = @import("std");
const zigar = @import("zigar");
const Promise = zigar.function.Promise(error{Aborted}!void);
pub fn start() !void {
try zigar.thread.use();
}
pub fn stop() void {
zigar.thread.end();
}
pub fn idle(promise: Promise, signal: zigar.function.AbortSignal) !void {
const thread = try std.Thread.spawn(.{}, spin, .{ promise, signal });
thread.detach();
}
pub fn spin(promise: Promise, signal: zigar.function.AbortSignal) void {
while (signal.off()) {}
promise.resolve(error.Aborted);
}
import { idle, start, stop } from './abort-signal-example-1.zig';
try {
start();
const controller = new AbortController();
const { signal } = controller;
setTimeout(() => controller.abort(), 200);
await idle({ signal });
} catch (err) {
console.log(err);
} finally {
stop();
}
[Error: Aborted] { number: 169 }
The Zig code above spawns a thread that doesn't do anything except spin in place until the abort
signal comes on. This happens when the timer function calls the abort controller's abort()
method.
AbortSignal
contains a pointer to a i32
that's initially zero. It is set to one when the
JavaScript AbortSignal
object emits an
abort
event. On the
Zig side, the on()
and
off()
methods simply check this i32
for the
corresponding values.
One AbortSignal
can cause early termination in multiple threads. The following code demonstrates
how 32 threads are told to stop performing a pointless task:
const std = @import("std");
const zigar = @import("zigar");
var gpa = std.heap.DebugAllocator(.{}).init;
const allocator = gpa.allocator();
const Promise = zigar.function.PromiseOf(thread_ns.pointless);
var work_queue: zigar.thread.WorkQueue(thread_ns) = .{};
pub fn start(promise: zigar.function.Promise(void)) !void {
try work_queue.init(.{
.allocator = allocator,
.n_jobs = 32,
});
work_queue.waitAsync(promise);
}
pub fn stop(promise: zigar.function.Promise(void)) void {
work_queue.deinitAsync(promise);
}
pub fn pointless(a: std.mem.Allocator, promise: Promise, signal: zigar.function.AbortSignal) !void {
const slice = try a.alloc(u32, 32);
const multipart_promise = try promise.partition(allocator, 32);
for (0..32) |i| {
slice[i] = 0;
try work_queue.push(thread_ns.pointless, .{ slice, i, signal }, multipart_promise);
}
}
const thread_ns = struct {
pub fn pointless(slice: []u32, index: usize, signal: zigar.function.AbortSignal) []u32 {
while (signal.off()) slice[index] += 1;
return slice;
}
};
import { pointless, start, stop } from './abort-signal-example-2.zig';
try {
await start();
const signal = AbortSignal.timeout(200);
const numbers = await pointless({ signal });
console.log(numbers.valueOf());
} finally {
await stop();
}
[
2662940, 2603501, 2779043, 2993669,
951147, 1908218, 910741, 997887,
1880495, 829757, 709525, 1447493,
1097731, 851886, 1767386, 843635,
691862, 1164306, 582661, 606771,
1009091, 733561, 1389341, 1020940,
1761980, 594635, 948148, 885895,
1422610, 1521989, 1441227, 1108398
]
We rely on WorkQueue
for management of our threads. We use
Promise.partition()
to create a
new promise object that would resolve the original promise after its
resolve()
method has been called the given
number of times (32). The threads themselves just increment an element of an array continually,
stopping only when the abort signal turns on. Instead of returning an error indicating an abort has
occurred, here we just return the pointless results.
Zig-to-JavaScript function calls
In theory, AbortSignal
can be used in calls from Zig to JavaScript. The called function will
received a Javascript AbortSignal
object through its options
argument:
const zigar = @import("zigar");
pub fn call(cb: *const fn(zigar.function.AbortSignal) void ) {
var value: i32 = 1;
cb(.{ .ptr = &value });
}
import { call } from './abort-signal-example-3.zig';
call(({ signal }) => console.log(signal));
AbortSignal { aborted: true }
Usage scenarios for the feature are hard to imagine though. And the actual implementation is not
very efficient, relying on an interval function to check if the value of the signal's i32
pointer target has changed.