Generator - chung-leong/zigar GitHub Wiki
The zigar.function.Generation
parametric struct provides an
interface to a JavaScript
Async Generator object.
It's analogous to a Promise
except that it yields data multiple times.
When a Zig function accepts a Generator
as an argument, on the JavaScript side it returns an
async generator:
const std = @import("std");
const zigar = @import("zigar");
const Generator = zigar.function.Generator(?error{OutOfMemory}![]u8);
pub fn start() !void {
try zigar.thread.use();
}
pub fn stop() void {
zigar.thread.end();
}
pub fn getStrings(a: std.mem.Allocator, generator: Generator) !void {
const thread = try std.Thread.spawn(.{}, generateStrings, .{ a, generator });
thread.detach();
}
fn generateStrings(a: std.mem.Allocator, generator: Generator) void {
for (0..10) |i| {
if (!generator.yield(std.fmt.allocPrint(a, "string {d}", .{i}))) break;
} else generator.end();
}
import { getStrings, start, stop } from './generator-example-1.zig';
try {
start();
for await (const s of getStrings()) {
console.log(s.string);
await new Promise(r => setTimeout(r, 100));
}
} finally {
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 JavaScript side, one after another. Generation would pause when result previously sent hasn't been processed yet.
The yield()
method returns false when the
for async ()loop on the JavaScript 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);
pub fn start() !void {
try zigar.thread.use();
}
pub fn stop() void {
zigar.thread.end();
}
pub fn getStrings(a: std.mem.Allocator, generator: Generator) !void {
const thread = try std.Thread.spawn(.{}, generateStrings, .{ a, generator });
thread.detach();
}
fn generateStrings(a: std.mem.Allocator, generator: Generator) void {
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();
}
import { getStrings, start, stop } from './generator-example-2.zig';
try {
start();
for await (const s of getStrings()) {
console.log(s.string);
break;
}
} finally {
console.log('finally');
stop();
}
generating item 0
string 0
generating item 1
finally
Note the order of the printed lines. The finally
clause is reached only after yield()
has
returned false (i.e. after the stoppage of content generation on the Zig side).
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 async generator would throw it on the JavaScript side:
const std = @import("std");
const zigar = @import("zigar");
const Generator = zigar.function.Generator(?error{OutOfMemory}![]u8);
pub fn start() !void {
try zigar.thread.use();
}
pub fn stop() void {
zigar.thread.end();
}
pub fn getStrings(_: std.mem.Allocator, _: Generator) !void {
return error.OutOfMemory;
}
import { getStrings, start, stop } from './generator-example-3.zig';
try {
start();
for await (const s of getStrings());
} catch (err) {
console.error(err);
} finally {
stop();
}
Error: Out of memory
at getStrings (<anonymous>:1:26192)
at file:///home/cleong/zigar.wiki/examples/generator-example-3.js:5:25 {
number: 1
}
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.GeneralPurposeAllocator(.{}){};
const allocator = gpa.allocator();
const Generator = zigar.function.Generator(?[]const u8);
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);
}
import { splitSequence, start, stop } from './generator-example-4.zig';
try {
start();
for await (const s of splitSequence('hello||world||123||chicken', '||')) {
console.log(s.string);
}
} finally {
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(thread_ns) = .{};
const allocator = zigar.mem.getDefaultAllocator();
pub fn start(threads: usize) !void {
try work_queue.init(.{
.allocator = allocator,
.n_jobs = threads,
});
}
pub fn stop(promise: zigar.function.Promise(void)) void {
work_queue.deinitAsync(promise);
}
const Generator = zigar.function.GeneratorOf(thread_ns.scanDir);
pub fn scanDir(path: []const u8, generator: Generator) !void {
try work_queue.push(thread_ns.scanDir, .{path}, generator);
}
const thread_ns = struct {
pub fn scanDir(path: []const u8) !std.fs.Dir.Iterator {
const dir = try std.fs.openDirAbsolute(path, .{ .iterate = true });
return dir.iterate();
}
};
import { availableParallelism } from 'os';
import { scanDir, start, stop } from './generator-example-5.zig';
try {
start(availableParallelism());
for await (const file of scanDir(process.cwd())) {
console.log(`${file.name.string} (${file.kind})`);
}
} finally {
await 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, the third argument to
push()
is expected to be a
compatible Generator
type. We use
zigar.function.GeneratorOf
to define it, based on the
return value of the iterator's next()
method
(IteratorError
!
Entry
in this case) with
the error set of the function itself
(OpenError
from
openDirAbsolute()
)
added in. If WorkQueue
manages to obtain an iterator, pipe()
is used to send its output
through to the JavaScript generator.
Generator
can be used to return a series of results from JavaScript to Zig code:
const std = @import("std");
const zigar = @import("zigar");
const Generator = zigar.function.Generator(?[]const u8);
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;
}
}
import { call } from './generator-example-6.zig';
call(async function*() {
for (let i = 0; i < 10; 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:
import { call } from './generator-example-6.zig';
call(function({ callback }) {
for (let i = 0; i < 5; i++) {
if (!callback(`string ${i}`)) {
break;
}
}
callback(null);
});
received = string 0
received = string 1
received = string 2
received = string 3
received = string 4
If the callback returns false
, then the generator is 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);
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;
}
}
import { call } from './generator-example-7.zig';
call(async function*() {
try {
for (let i = 0; i < 10; i++) {
console.log(`generating: ${i}`);
yield i;
}
} finally {
console.log(`finally`);
}
console.log(`the end`);
});
generating: 0
received = 0
generating: 1
received = 1
generating: 2
received = 2
generating: 3
received = 3
finally
You can pass an Allocator
to the JavaScript function. The generator will allocate memory from it
when it converts regular JavaScript 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);
pub fn call(cb: *const fn (std.mem.Allocator, Generator) void) void {
const generator = Generator.init(&allocator, callback);
cb(allocator, generator);
}
fn callback(a: *std.mem.Allocator, payload: Generator.payload) bool {
if (payload == null) return false;
if (payload.?) |avenger| {
defer avenger.deinit(a.*);
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}", .{@errorName(err)});
return false;
}
}
import { call } from './generator-example-8.zig';
call(async function*() {
const avengers = [
{
real_name: 'Tony Stack',
superhero_name: 'Ironman',
age: 53,
},
{
real_name: 'Natasha Romanoff',
superhero_name: 'Black Widow',
age: 37,
}
];
for (const avenger of avengers) {
yield avenger;
}
throw new Error('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
*std.mem.Allocator
.
error{Unexpected}
is used here to capture all possible errors.