Interface struct ‣ Generator ᴾᴴᴾ - chung-leong/zigar GitHub Wiki
JavaScript | PHP
The zigar.function.Generator parametric struct provides an interface to a PHP generator object.
It's analogous to a Promise except that it yields data multiple
times.
PHP-to-Zig function calls
When a Zig function accepts a Generator as an argument, on the PHP side it returns an
object that implements PHP's
Iterator interface:
const std = @import("std");
const zigar = @import("zigar");
const Generator = zigar.function.Generator(?error{OutOfMemory}![]u8, true);
pub fn start() !void {
try zigar.thread.use();
}
pub fn stop() void {
zigar.thread.end();
}
pub fn getStrings(generator: Generator) !void {
const thread = try std.Thread.spawn(.{}, generateStrings, .{generator});
thread.detach();
}
fn generateStrings(generator: Generator) void {
const a = generator.allocator;
for (0..10) |i| {
if (!generator.yield(std.fmt.allocPrint(a, "string {d}", .{i}))) break;
} else generator.end();
}
<?php
$m = zigar_use(__DIR__ . '/generator-example-1.zig');
try {
$m->start();
foreach ($m->getStrings() as $s) {
echo "$s\n";
}
} finally {
$m->stop();
}
string 0
string 1
string 2
string 3
string 4
string 5
string 6
string 7
string 8
string 9
The code above spawns a thread which sends strings to the PHP side, one after another. Generation would pause when the result sent previously hasn't been processed yet.
The second argument to zigar.function.Generator() determines whether the generator has
an allocator attached to it. This allocator allocates PHP memory. It works differently
compared to an allocator that a function would receive. The latter releases memory when the
function returns. A generator's allocator releases memory each time yield() is called.
The yield() method returns false when the foreach () loop on the PHP side is
broken by a break, return, or throw statement:
const std = @import("std");
const zigar = @import("zigar");
const Generator = zigar.function.Generator(?error{OutOfMemory}![]u8, true);
pub fn start() !void {
try zigar.thread.use();
}
pub fn stop() void {
zigar.thread.end();
}
pub fn getStrings(generator: Generator) !void {
const thread = try std.Thread.spawn(.{}, generateStrings, .{generator});
thread.detach();
}
fn generateStrings(generator: Generator) void {
const a = generator.allocator;
for (0..10) |i| {
std.debug.print("generating item {d}\n", .{i});
if (!generator.yield(std.fmt.allocPrint(a, "string {d}", .{i}))) break;
} else generator.end();
}
<?php
$m = zigar_use(__DIR__ . '/generator-example-2.zig');
try {
$m->start();
foreach ($m->getStrings() as $s) {
echo "$s\n";
break;
}
} finally {
echo "finally\n";
$m->stop();
}
generating item 0
string 0
generating item 1
finally
end() is simply yield(null) with the
return value ignored. It should not be called when yield() has previously returned false.
If the function returns an error, the generator would throw it on the PHP side:
const std = @import("std");
const zigar = @import("zigar");
const Generator = zigar.function.Generator(?error{OutOfMemory}![]u8, true);
pub fn start() !void {
try zigar.thread.use();
}
pub fn stop() void {
zigar.thread.end();
}
pub fn getStrings(_: Generator) !void {
return error.OutOfMemory;
}
<?php
$m = zigar_use(__DIR__ . '/generator-example-3.zig');
$m->start();
try {
foreach ($m->getStrings() as $s);
} catch (Exception $e) {
echo "$e\n";
} finally {
$m->stop();
}
ZigException: out of memory in /home/rwiggum/examples/generator-example-3.php:7
Stack trace:
#0 /home/rwiggum/examples/generator-example-3.php(7): generator-example-3->getStrings()
#1 {main}
You can use the pipe() method to generate results from a Zig iterator:
const std = @import("std");
const zigar = @import("zigar");
var gpa = std.heap.DebugAllocator(.{}).init;
const allocator = gpa.allocator();
const Generator = zigar.function.Generator(?[]const u8, false);
pub fn start() !void {
try zigar.thread.use();
}
pub fn stop() void {
zigar.thread.end();
}
pub fn splitSequence(text: []const u8, delimiter: []const u8, generator: Generator) !void {
const thread = try std.Thread.spawn(.{}, generateSequence, .{
text,
delimiter,
generator,
});
thread.detach();
}
fn generateSequence(text: []const u8, delimiter: []const u8, generator: Generator) void {
const iter = std.mem.splitSequence(u8, text, delimiter);
generator.pipe(iter);
}
<?php
$m = zigar_use(__DIR__ . '/generator-example-4.zig');
try {
$m->start();
foreach ($m->splitSequence('hello||world||123||chicken', '||') as $s) {
echo "$s\n";
}
} finally {
$m->stop();
}
hello
world
123
chicken
As in case of Promise, you can use zigar.thread.WorkQueue to handle the asynchronous generation of results:
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 scanDir = work_queue.asyncify(worker.scanDir);
const worker = struct {
pub fn scanDir(path: []const u8) !std.fs.Dir.Iterator {
const dir = try std.fs.openDirAbsolute(path, .{ .iterate = true });
return dir.iterate();
}
};
<?php
$m = zigar_use(__DIR__ . '/generator-example-5.zig');
$m->start(4);
try {
foreach ($m->scanDir(getcwd()) as $file) {
echo "$file->name ($file->kind)\n";
}
} finally {
$m->stop();
}
...
generator-example-1.js (file)
generator-example-1.zig (file)
generator-example-2.js (file)
generator-example-2.zig (file)
...
Because scanDir() returns an iterator, we have to use
asyncify() instead of
[promisify()](zigar.thread.WorkQueue(ns).promisify(self,-func-ᴾᴴᴾ) since the generated function
returns an async generator and not a promise.
Zig-to-PHP function calls
Generator can be used to return a series of results from PHP to Zig code:
const std = @import("std");
const zigar = @import("zigar");
const Generator = zigar.function.Generator(?[]const u8, false);
pub fn call(cb: *const fn (Generator) void) void {
cb(.{ .ptr = null, .callback = &callback });
}
fn callback(_: ?*anyopaque, payload: ?[]const u8) bool {
if (payload) |s| {
std.debug.print("received = {s}\n", .{s});
return true;
} else {
return false;
}
}
<?php
$m = zigar_use(__DIR__ . '/generator-example-6.zig');
$m->call(function() {
for ($i = 0; $i < 5; $i++) {
yield "string $i";
}
});
received = string 0
received = string 1
received = string 2
received = string 3
received = string 4
As in the case of Promise, you can use callback instead:
<?php
require __DIR__ . '/vendor/autoload.php';
use Revolt\EventLoop;
ini_set('zigar.event_loop', 'revolt');
EventLoop::defer(function() {
$m = zigar_use(__DIR__ . '/generator-example-6.zig');
$m->call(function($callback) {
for ($i = 0; $i < 5; $i++) {
$callback("string $i");
}
$callback(null);
});
});
EventLoop::run();
received = string 0
received = string 1
received = string 2
received = string 3
received = string 4
If the callback on the Zig side returns false, then the PHP generator function gets terminated
early. It would behave as though a return had been inserted just below the yield statement:
const std = @import("std");
const zigar = @import("zigar");
const Generator = zigar.function.Generator(?u32, false);
pub fn call(cb: *const fn (Generator) void) void {
cb(.{ .ptr = null, .callback = &callback });
}
fn callback(_: ?*anyopaque, payload: ?u32) bool {
if (payload) |num| {
std.debug.print("received = {d}\n", .{num});
return num < 3;
} else {
return false;
}
}
<?php
$m = zigar_use(__DIR__ . '/generator-example-7.zig');
$m->call(function() {
try {
for ($i = 0; $i < 10; $i++) {
echo "generating: ${i}\n";
yield $i;
}
} finally {
echo "finally\n";
}
echo "the end\n";
});
generating: 0
received = 0
generating: 1
received = 1
generating: 2
received = 2
generating: 3
received = 3
finally
You can pass an Allocator to the PHP function. The generator will allocate memory from it
when it converts regular PHP values to Zig objects:
const std = @import("std");
const zigar = @import("zigar");
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
const allocator = gpa.allocator();
const Avenger = struct {
real_name: []const u8,
superhero_name: []const u8,
age: u32,
fn deinit(self: *const @This(), a: std.mem.Allocator) void {
a.free(self.real_name);
a.free(self.superhero_name);
a.destroy(self);
}
};
const ErrorSet = error{Unexpected};
const Generator = zigar.function.Generator(?ErrorSet!*const Avenger, false);
pub fn call(cb: *const fn (std.mem.Allocator, Generator) void) void {
const generator = Generator.init(&gpa, callback);
cb(allocator, generator);
}
fn callback(ptr: *@TypeOf(gpa), payload: Generator.payload) bool {
std.debug.assert(ptr == &gpa);
if (payload == null) return false;
if (payload.?) |avenger| {
defer avenger.deinit(allocator);
std.debug.print("real_name = {s}, superhero_name = {s}, age = {d}\n", .{
avenger.real_name,
avenger.superhero_name,
avenger.age,
});
return true;
} else |err| {
std.debug.print("error = {s}\n", .{@errorName(err)});
return false;
}
}
<?php
$m = zigar_use(__DIR__ . '/generator-example-8.zig');
$m->call(function() {
$avengers = [
[
'real_name' => 'Tony Stack',
'superhero_name' => 'Ironman',
'age' => 53,
],
[
'real_name' => 'Natasha Romanoff',
'superhero_name' => 'Black Widow',
'age' => 37,
],
];
foreach ($avengers as $avenger) {
yield $avenger;
}
throw new Exception('Dog ate soul stone');
});
real_name = Tony Stack, superhero_name = Ironman, age = 53
real_name = Natasha Romanoff, superhero_name = Black Widow, age = 37
error = Unexpected
init() of Generator, like its
counterpart in Promise, will cast any compatible callback function to the required type.
Instead of ?*anyopaque, for convenience's sake we want the first argument to be
the actual pointer type--std.heap.GeneralPurposeAllocator(.{}){} in this case.
error{Unexpected} is used here to capture all possible exceptions.