Interface struct ‣ Abort signal ᴾᴴᴾ - chung-leong/zigar GitHub Wiki

JavaScript | PHP


The zigar.function.AbortSignal struct provides an interface to a an AbortSignal object. It serves the same purpose as its JavaScript counterpart-- allowing early termination of a time-consuming asychronous task. It's always used alongside a Promise.

PHP-to-Zig function calls

Like other special arguments like Allocator and Promise, Zigar will provide an AbortSignal automatically on the PHP 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 provide your own signal object:

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);
}
<?php

require __DIR__ . '/vendor/autoload.php';

use Revolt\EventLoop;

ini_set('zigar.event_loop', 'revolt');

EventLoop::defer(function() {
    $m = zigar_use(__DIR__ . '/abort-signal-example-1.zig');
    try {
        $m->start();
        $signal = new AbortSignal();
        EventLoop::delay(0.2, function() use($signal) {
            $signal->abort();
        });
        $m->idle(signal: $signal);
    } catch (Exception $e) {
        echo $e->getMessage(), "\n";
    } finally {
        $m->stop();
    }
});
EventLoop::run();
ZigException Object
(
    [error] => aborted
)

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 function passed to EventLoop::delay() calls the abort signal's abort() method.

AbortSignal contains a pointer to a i32 that's initially zero. It is set to one when the abort() is called on PHP side. 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 {
        // allowing the value to wrap around
        while (signal.off()) slice[index] +%= 1;
        return slice;
    }
};
<?php

require __DIR__ . '/vendor/autoload.php';

use Revolt\EventLoop;

ini_set('zigar.event_loop', 'revolt');

EventLoop::defer(function() {
    $m = zigar_use(__DIR__ . '/abort-signal-example-2.zig');
    $m->start();
    try {
        $signal = new AbortSignal;
        EventLoop::delay(0.2, function() use($signal) {
            $signal->abort();
        });
        $numbers = $m->pointless(signal: $signal);
        print_r($numbers);
    } finally {
        $m->stop();
    }
});
EventLoop::run();
[]u32 Object
(
    [0] => 2612968
    [1] => 2133449
    [2] => 3935198
    [3] => 1426306
    [4] => 2049790
    [5] => 2185686
    [6] => 2344400
    [7] => 2432831
    [8] => 1600109
    [9] => 1029532
    [10] => 1777823
    [11] => 974398
    [12] => 1005438
    [13] => 894564
    [14] => 1456208
    [15] => 645046
    [16] => 627468
    [17] => 883586
    [18] => 2859776
    [19] => 1055861
    [20] => 2940929
    [21] => 2189870
    [22] => 769844
    [23] => 2410026
    [24] => 1035624
    [25] => 1349586
    [26] => 995647
    [27] => 1163327
    [28] => 989173
    [29] => 755891
    [30] => 1133803
    [31] => 1208647
)

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, we just return the pointless results.

Zig-to-PHP function calls

In theory, AbortSignal can be used in calls from Zig to PHP. The called function will received a PHP AbortSignal object as a named argument:

const zigar = @import("zigar");

pub fn call(cb: *const fn(zigar.function.AbortSignal) void ) {
    var value: i32 = 1;
    cb(.{ .ptr = &value });
}
<?php

$m = zigar_use(__DIR__ . '/abort-signal-example-3.zig');

$m->call(function($signal) {
    print_r($signal);
    echo "on() => ", $signal->on(), "\n";
    echo "off() => ", $signal->off(), "\n";
});
AbortSignal Object
(
    [ptr] => *const i32 Object
        (
        )

)
on() => 1
off() => 

Interface structs