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.