From 10ec4ff814aa83cbfde8495da0d5e20c7fdb5a1b Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Tue, 3 Mar 2026 11:37:40 +0800 Subject: [PATCH] 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. --- src/browser/js/Caller.zig | 4 + src/browser/js/Context.zig | 13 ++- src/browser/js/Local.zig | 12 +- src/browser/webapi/streams/ReadableStream.zig | 105 ++++++------------ .../webapi/streams/TransformStream.zig | 2 +- 5 files changed, 57 insertions(+), 79 deletions(-) diff --git a/src/browser/js/Caller.zig b/src/browser/js/Caller.zig index 509adef0..01adc4f2 100644 --- a/src/browser/js/Caller.zig +++ b/src/browser/js/Caller.zig @@ -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()); diff --git a/src/browser/js/Context.zig b/src/browser/js/Context.zig index ba160058..a87d48d0 100644 --- a/src/browser/js/Context.zig +++ b/src/browser/js/Context.zig @@ -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); diff --git a/src/browser/js/Local.zig b/src/browser/js/Local.zig index 65e577e9..04a65bc7 100644 --- a/src/browser/js/Local.zig +++ b/src/browser/js/Local.zig @@ -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 }; } diff --git a/src/browser/webapi/streams/ReadableStream.zig b/src/browser/webapi/streams/ReadableStream.zig index a2033878..1aebb7c4 100644 --- a/src/browser/webapi/streams/ReadableStream.zig +++ b/src/browser/webapi/streams/ReadableStream.zig @@ -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", {}); } } diff --git a/src/browser/webapi/streams/TransformStream.zig b/src/browser/webapi/streams/TransformStream.zig index 1805da2d..7d5eacbe 100644 --- a/src/browser/webapi/streams/TransformStream.zig +++ b/src/browser/webapi/streams/TransformStream.zig @@ -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,