Interface struct ‣ Promise ᴾᴴᴾ - chung-leong/zigar GitHub Wiki
JavaScript | PHP
The zigar.function.Promise parametric struct provides an interface to a promise object on the PHP side. Since modern PHP employs a non-coloring async model, this object isn't exposed to the your code. Zigar creates it automatically when you call a Zig function that expects a promise and automatically performs the await operation. The current fiber gets suspended by the event loop. When the promise's callback is invoked, the event loop resumes the fiber. You get the result as you would from a normal function.
Promise can also be used to send data from PHP to the Zig side asynchronously.
PHP-to-Zig function calls
When a Zig function accepts a Promise as an argument, it becomes an async function on the
PHP side. You can't tell this is what's happening by looking at the PHP code however:
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"));
}
<?php
$m = zigar_use(__DIR__ . '/promise-example-1.zig');
$text = $m->getText();
echo "$text\n";
Hello world
Like Allocator, Promise is automatically provided by Zigar.
On the PHP side getText() therefore has zero required arguments.
Using threads
The example above is not particularly realistic. Promises generally don't get fulfilled immediately. Their entire purpose is to allow time-consuming tasks to be offloaded to other threads.
In a order to resolve a promise from 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"));
}
<?php
$m = zigar_use(__DIR__ . '/promise-example-3.zig');
try {
$m->start();
$text = $m->getText();
echo "$text\n";
} finally {
$m->stop();
}
Hello world
In the PHP code above, you don't see an event loop being used. By default, php-zigar is set to use a temporary event loop. This temporary event loop is active only when a promise or a generator is active. Once the promise resolves or the generator finishes, the loop becomes inactive again. It's designed to allow async code to work in both async and non-async PHP applications.
The following example demonstrates the use of the same code in an async context, with Revolt as the event loop:
<?php
require_once(__DIR__ . '/vendor/autoload.php');
use Revolt\EventLoop;
ini_set('zigar.event_loop', 'revolt');
EventLoop::defer(function() {
$m = zigar_use(__DIR__ . '/promise-example-3.zig');
$m->start();
try {
echo "Waiting for result\n";
$text = $m->getText();
echo "$text\n";
} finally {
$m->stop();
}
});
EventLoop::defer(function() {
echo "Doing something else\n";
});
EventLoop::run();
Waiting for result
Doing something else
Hello world
As you can see, the second deferred function was able to execute while the first was waiting for
the result getText().
Using a work queue
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(worker) = .{};
pub const startup = work_queue.promisify(.startup);
pub const shutdown = work_queue.promisify(.shutdown);
pub const calcFactorial = work_queue.promisify(worker.calcFactorial);
const worker = 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;
}
};
<?php
$m = zigar_use(__DIR__ . '/promise-example-4.zig');
try {
$m->startup();
$f = $m->calcFactorial(500);
echo "$f\n";
} finally {
$m->shutdown();
}
1220136825991110068701238785423046926253574342803192842192413588385845373153881
9976054964475022032818630136164771482035841633787220781772004807852051593292854
7790757193933060377296085908627042917454788242491272634430567017327076946106280
2310452644218878789465754777149863494367781037644274033827365397471386477878495
4384895955375379904232410612713269843277457155463099772027810145610811883737095
3101635632443298702956389662891165897476957208792692887128178007026517450776841
0719624390394322536422605234945850129918571501248706961568141625359056693423813
0088562492468915641267756544818865065938479517753608940057452389403357984763639
4490531306232374906644504882466507594673586207463792518420045936969298102226397
1952597190945217823331756934581508552332820762820023402626907898342451712006207
7146409794561161276291459512372299133401695523638509428855920187274337951730145
8635757082835578015873543276888868012039988238470215146760544540766353598417443
0480128938313896881639487469658817504506926365338175055478128640000000000000000
0000000000000000000000000000000000000000000000000000000000000000000000000000000
00000000000000000000000000000n
WorkQueue() expects a namespace. Its
promisify method generates an async version
of the given function. The special arguments .startup and .shutdown instruct it to generate
start-up and shut-down functions for the work queue.
Calling startup() isn't required. The work queue would automatically start up itself with a
single thread when you use it in an uninitialized state:
<?php
require_once(__DIR__ . '/vendor/autoload.php');
use Revolt\EventLoop;
ini_set('zigar.event_loop', 'revolt');
EventLoop::defer(function() {
$m = zigar_use(__DIR__ . '/promise-example-4.zig');
try {
$f = $m->calcFactorial(500);
echo "$f\n";
} finally {
$m->shutdown();
}
});
EventLoop::run();
When the code is used in an non-async context, the call to shutdown() can be omitted too, since
the temporary event loop would already be in an active state:
<?php
$m = zigar_use(__DIR__ . '/promise-example-4.zig');
$f = $m->calcFactorial(500);
echo "$f\n";
Using callback in lieu of awaiting
If you supply a function as the named argument callback, Zigar would call it with the result
instead of returning it:
<?php
require_once(__DIR__ . '/vendor/autoload.php');
use Revolt\EventLoop;
ini_set('zigar.event_loop', 'revolt');
EventLoop::defer(function() {
$m = zigar_use(__DIR__ . '/promise-example-4.zig');
try {
$m->calcFactorial(500, callback: function($f) {
$fragment = substr($f, 0, 40);
echo "callback received: $fragment...\n";
});
} finally {
$m->shutdown();
}
});
EventLoop::run();
callback received: 1220136825991110068701238785423046926253...
Zig-to-PHP function calls
Promise can be used to receive results asynchronously from PHP:
const std = @import("std");
const zigar = @import("zigar");
const Error = error{ DingoAteMyBaby, SomeoneFartedInYourGeneralDirection };
const Promise = zigar.function.Promise(Error![]const u8);
pub fn call(cb: *const fn (Promise) void) void {
cb(.{ .ptr = null, .callback = &callback });
}
fn callback(_: ?*anyopaque, result: Error![]const u8) void {
if (result) |str| {
std.debug.print("received = {s}\n", .{str});
} else |err| {
std.debug.print("error = {}\n", .{err});
}
}
<?php
$m = zigar_use(__DIR__ . '/promise-example-5.zig');
$m->call(function () {
return 'Hello world';
});
$m->call(function () {
throw new Exception('dingo ate my baby');
});
received = Hello world
error = error.DingoAteMyBaby
The default value for ptr is null. It's explicitly set in the code above only for clarity
purpose.
As in the case for PHP-to-Zig call, there's support for using a callback function instead of promise:
<?php
$m = zigar_use(__DIR__ . '/promise-example-5.zig');
$m->call(function ($callback) {
$callback('Hello world');
});
$m->call(function ($callback) {
$callback(new Exception('someone farted in your general direction'));
});
received = Hello world
error = error.SomeoneFartedInYourGeneralDirection
The argument is passed by name, so it must be $callback.
Unlike with synchronous calls, you can return PHP 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 PHP 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(&gpa, callback);
cb(allocator, promise);
}
fn callback(ptr: *@TypeOf(gpa), payload: Promise.payload) void {
std.debug.assert(ptr == &gpa);
if (payload) |s| {
std.debug.print("received = {s}\n", .{s});
allocator.free(s);
} else |err| {
std.debug.print("error = {s}\n", .{@errorName(err)});
}
}
<?php
$m = zigar_use(__DIR__ . '/promise-example-6.zig');
$m->call(function($allocator) {
return $allocator->dupe('Hello world');
});
$m->call(function() {
return 'Hello world';
});
received = 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 demonstration purpose we're passing a pointer to gpa here.
We're also using Promise.payload as the payload type to avoid typing it again.
Like $callback, $allocator is a passed-by-name argument. You generally don't need to use it as
Zigar would use it automatically when converting the return value (from a string to []const u8
in this example).
The promise's error set includes Unexpected. This is the catch-all error. Exceptions with
unrecognized messages are returned as error.Unexpected:
<?php
$m = zigar_use(__DIR__ . '/promise-example-6.zig');
$m->call(function() {
throw new Exception('out of memory');
});
$m->call(function() {
throw new Exception('my hovercraft is full of eels');
});
error = OutOfMemory
error = Unexpected
If it's not part of the set (or if the payload is not an error union at all), non-matching exceptions would result in a panic on the Zig side.