Create Zig wrapper generator for js.Function creation

This allows us to leverage the Caller.Function.call method, which does type
mapping, caching, etc... and allows the Zig function callback to be written like
any other Zig WebAPI function.
This commit is contained in:
Karl Seguin
2026-03-03 11:37:40 +08:00
parent d2da0b7c0e
commit 10ec4ff814
5 changed files with 57 additions and 79 deletions

View File

@@ -509,6 +509,7 @@ pub const Function = struct {
as_typed_array: bool = false,
null_as_undefined: bool = false,
cache: ?Caching = null,
embedded_receiver: bool = false,
// We support two ways to cache a value directly into a v8::Object. The
// difference between the two is like the difference between a Map
@@ -579,6 +580,9 @@ pub const Function = struct {
var args: ParameterTypes(F) = undefined;
if (comptime opts.static) {
args = try getArgs(F, 0, local, info);
} else if (comptime opts.embedded_receiver) {
args = try getArgs(F, 1, local, info);
@field(args, "0") = @ptrCast(@alignCast(info.getData() orelse unreachable));
} else {
args = try getArgs(F, 1, local, info);
@field(args, "0") = try TaggedOpaque.fromJS(*T, info.getThis());

View File

@@ -539,6 +539,15 @@ fn postCompileModule(self: *Context, mod: js.Module, url: [:0]const u8, local: *
}
}
fn newFunctionWithData(local: *const js.Local, comptime callback: *const fn (?*const v8.FunctionCallbackInfo) callconv(.c) void, data: *anyopaque) js.Function {
const external = local.isolate.createExternal(data);
const handle = v8.v8__Function__New__DEFAULT2(local.handle, callback, @ptrCast(external)).?;
return .{
.local = local,
.handle = handle,
};
}
// == Callbacks ==
// Callback from V8, asking us to load a module. The "specifier" is
// the src of the module to load.
@@ -857,7 +866,7 @@ fn resolveDynamicModule(self: *Context, state: *DynamicModuleResolveState, modul
// last value of the module. But, for module loading, we need to
// resolve to the module's namespace.
const then_callback = local.newFunctionWithData(struct {
const then_callback = newFunctionWithData(local, struct {
pub fn callback(callback_handle: ?*const v8.FunctionCallbackInfo) callconv(.c) void {
var c: Caller = undefined;
c.initFromHandle(callback_handle);
@@ -881,7 +890,7 @@ fn resolveDynamicModule(self: *Context, state: *DynamicModuleResolveState, modul
}
}.callback, @ptrCast(state));
const catch_callback = local.newFunctionWithData(struct {
const catch_callback = newFunctionWithData(local, struct {
pub fn callback(callback_handle: ?*const v8.FunctionCallbackInfo) callconv(.c) void {
var c: Caller = undefined;
c.initFromHandle(callback_handle);

View File

@@ -82,13 +82,17 @@ pub fn createTypedArray(self: *const Local, comptime array_type: js.ArrayType, s
return .init(self, size);
}
pub fn newFunctionWithData(
pub fn newCallback(
self: *const Local,
comptime callback: *const fn (?*const v8.FunctionCallbackInfo) callconv(.c) void,
data: *anyopaque,
callback: anytype,
data: anytype,
) js.Function {
const external = self.isolate.createExternal(data);
const handle = v8.v8__Function__New__DEFAULT2(self.handle, callback, @ptrCast(external)).?;
const handle = v8.v8__Function__New__DEFAULT2(self.handle, struct {
fn wrap(info_handle: ?*const js.v8.FunctionCallbackInfo) callconv(.c) void {
Caller.Function.call(@TypeOf(data), info_handle.?, callback, .{ .embedded_receiver = true });
}
}.wrap, @ptrCast(external)).?;
return .{ .local = self, .handle = handle };
}

View File

@@ -279,7 +279,6 @@ pub fn pipeTo(self: *ReadableStream, destination: *WritableStream, page: *Page)
const PipeState = struct {
reader: *ReadableStreamDefaultReader,
writable: *WritableStream,
page: *Page,
context_id: usize,
resolver: ?js.PromiseResolver.Global,
@@ -294,107 +293,69 @@ const PipeState = struct {
state.* = .{
.reader = reader,
.writable = writable,
.page = page,
.context_id = page.js.id,
.resolver = resolver,
};
try state.pumpRead();
try state.pumpRead(page);
}
fn pumpRead(state: *PipeState) !void {
const local = state.page.js.local.?;
fn pumpRead(state: *PipeState, page: *Page) !void {
const local = page.js.local.?;
// Call reader.read() which returns a Promise
const read_promise = try state.reader.read(state.page);
const read_promise = try state.reader.read(page);
// Create JS callback functions for .then() and .catch()
const then_fn = local.newFunctionWithData(&onReadFulfilled, state);
const catch_fn = local.newFunctionWithData(&onReadRejected, state);
const then_fn = local.newCallback(onReadFulfilled, state);
const catch_fn = local.newCallback(onReadRejected, state);
_ = read_promise.thenAndCatch(then_fn, catch_fn) catch {
state.finish(local);
};
}
fn onReadFulfilled(callback_handle: ?*const js.v8.FunctionCallbackInfo) callconv(.c) void {
var c: js.Caller = undefined;
c.initFromHandle(callback_handle);
defer c.deinit();
const info = js.Caller.FunctionCallbackInfo{ .handle = callback_handle.? };
const state: *PipeState = @ptrCast(@alignCast(info.getData() orelse return));
if (state.context_id != c.local.ctx.id) return;
const l = &c.local;
defer l.runMicrotasks();
// Get the read result argument {done, value}
const result_val = info.getArg(0, l);
if (!result_val.isObject()) {
state.finish(l);
return;
}
const result_obj = result_val.toObject();
const done_val = result_obj.get("done") catch {
state.finish(l);
return;
const ReadData = struct {
done: bool,
value: js.Value,
};
fn onReadFulfilled(self: *PipeState, data_: ?ReadData, page: *Page) void {
const local = page.js.local.?;
const data = data_ orelse {
return self.finish(local);
};
const done = done_val.toBool();
if (done) {
if (data.done) {
// Stream is finished, close the writable side
state.writable.closeStream(state.page) catch {};
state.finishResolve(l);
self.writable.closeStream(page) catch {};
self.reader.releaseLock();
if (self.resolver) |r| {
local.toLocal(r).resolve("pipeTo complete", {});
}
return;
}
// Get the chunk value and write it to the writable side
const chunk_val = result_obj.get("value") catch {
state.finish(l);
return;
};
const value = data.value;
if (value.isUndefined()) {
return self.finish(local);
}
state.writable.writeChunk(chunk_val, state.page) catch {
state.finish(l);
return;
self.writable.writeChunk(value, page) catch {
return self.finish(local);
};
// Continue reading the next chunk
state.pumpRead() catch {
state.finish(l);
self.pumpRead(page) catch {
self.finish(local);
};
}
fn onReadRejected(callback_handle: ?*const js.v8.FunctionCallbackInfo) callconv(.c) void {
var c: js.Caller = undefined;
c.initFromHandle(callback_handle);
defer c.deinit();
const info = js.Caller.FunctionCallbackInfo{ .handle = callback_handle.? };
const state: *PipeState = @ptrCast(@alignCast(info.getData() orelse return));
if (state.context_id != c.local.ctx.id) return;
const l = &c.local;
defer l.runMicrotasks();
state.finish(l);
fn onReadRejected(self: *PipeState, page: *Page) void {
self.finish(page.js.local.?);
}
fn finishResolve(state: *PipeState, local: *const js.Local) void {
state.reader.releaseLock();
if (state.resolver) |r| {
local.toLocal(r).resolve("pipeTo complete", {});
}
}
fn finish(state: *PipeState, local: *const js.Local) void {
state.reader.releaseLock();
if (state.resolver) |r| {
fn finish(self: *PipeState, local: *const js.Local) void {
self.reader.releaseLock();
if (self.resolver) |r| {
local.toLocal(r).resolve("pipe finished", {});
}
}

View File

@@ -27,7 +27,7 @@ const TransformStream = @This();
pub const DefaultController = TransformStreamDefaultController;
pub const ZigTransformFn = *const fn (*TransformStreamDefaultController, js.Value) anyerror!void;
const ZigTransformFn = *const fn (*TransformStreamDefaultController, js.Value) anyerror!void;
_readable: *ReadableStream,
_writable: *WritableStream,