From 941dace7f9aa0bdd3969a9aa8b0b2e0604216339 Mon Sep 17 00:00:00 2001 From: Pierre Tachoire Date: Sat, 5 Jul 2025 16:45:01 -0700 Subject: [PATCH 1/4] enable conditionnal loading for polyfill --- src/browser/page.zig | 7 ++-- src/browser/polyfill/fetch.zig | 2 +- src/browser/polyfill/polyfill.zig | 43 ++++++++++++++-------- src/cdp/cdp.zig | 2 +- src/cdp/domains/page.zig | 3 -- src/log.zig | 1 + src/main_wpt.zig | 2 - src/runtime/js.zig | 61 ++++++++++++++++++++++++++++++- src/runtime/testing.zig | 1 + 9 files changed, 93 insertions(+), 29 deletions(-) diff --git a/src/browser/page.zig b/src/browser/page.zig index 0b9a0439..f13b76a2 100644 --- a/src/browser/page.zig +++ b/src/browser/page.zig @@ -95,6 +95,8 @@ pub const Page = struct { state_pool: *std.heap.MemoryPool(State), + polyfill_loader: polyfill.Loader = .{}, + pub fn init(self: *Page, arena: Allocator, session: *Session) !void { const browser = session.browser; self.* = .{ @@ -117,10 +119,7 @@ pub const Page = struct { }), .main_context = undefined, }; - self.main_context = try session.executor.createJsContext(&self.window, self, self, true); - - // load polyfills - try polyfill.load(self.arena, self.main_context); + self.main_context = try session.executor.createJsContext(&self.window, self, self, true, Env.GlobalMissingCallback.init(&self.polyfill_loader)); _ = try session.browser.app.loop.timeout(1 * std.time.ns_per_ms, &self.microtask_node); // message loop must run only non-test env diff --git a/src/browser/polyfill/fetch.zig b/src/browser/polyfill/fetch.zig index 1438022a..04bfa65c 100644 --- a/src/browser/polyfill/fetch.zig +++ b/src/browser/polyfill/fetch.zig @@ -16,7 +16,7 @@ test "Browser.fetch" { var runner = try testing.jsRunner(testing.tracking_allocator, .{}); defer runner.deinit(); - try @import("polyfill.zig").load(testing.allocator, runner.page.main_context); + try @import("polyfill.zig").Loader.load("fetch", source, runner.page.main_context); try runner.testCases(&.{ .{ diff --git a/src/browser/polyfill/polyfill.zig b/src/browser/polyfill/polyfill.zig index f7baddc9..e7e69600 100644 --- a/src/browser/polyfill/polyfill.zig +++ b/src/browser/polyfill/polyfill.zig @@ -23,25 +23,36 @@ const log = @import("../../log.zig"); const Allocator = std.mem.Allocator; const Env = @import("../env.zig").Env; -const modules = [_]struct { - name: []const u8, - source: []const u8, -}{ - .{ .name = "polyfill-fetch", .source = @import("fetch.zig").source }, -}; +pub const Loader = struct { + done: struct { + fetch: bool = false, + } = .{}, -pub fn load(allocator: Allocator, js_context: *Env.JsContext) !void { - var try_catch: Env.TryCatch = undefined; - try_catch.init(js_context); - defer try_catch.deinit(); + pub fn load(name: []const u8, source: []const u8, js_context: *Env.JsContext) !void { + var try_catch: Env.TryCatch = undefined; + try_catch.init(js_context); + defer try_catch.deinit(); - for (modules) |m| { - _ = js_context.exec(m.source, m.name) catch |err| { - if (try try_catch.err(allocator)) |msg| { - defer allocator.free(msg); - log.fatal(.app, "polyfill error", .{ .name = m.name, .err = msg }); + _ = js_context.exec(source, name) catch |err| { + if (try try_catch.err(js_context.call_arena)) |msg| { + log.fatal(.app, "polyfill error", .{ .name = name, .err = msg }); } return err; }; } -} + + pub fn missing(self: *Loader, name: []const u8, js_context: *Env.JsContext) bool { + if (!self.done.fetch and std.mem.eql(u8, name, "fetch")) { + // load the polyfill once. + self.done.fetch = true; + + const _name = "fetch"; + const source = @import("fetch.zig").source; + log.debug(.polyfill, "dynamic load", .{ .property = name }); + load(_name, source, js_context) catch |err| { + log.fatal(.app, "polyfill load", .{ .name = name, .err = err }); + }; + } + return false; + } +}; diff --git a/src/cdp/cdp.zig b/src/cdp/cdp.zig index 27544063..a1d25e42 100644 --- a/src/cdp/cdp.zig +++ b/src/cdp/cdp.zig @@ -569,7 +569,7 @@ const IsolatedWorld = struct { // Currently we have only 1 page/frame and thus also only 1 state in the isolate world. pub fn createContext(self: *IsolatedWorld, page: *Page) !void { if (self.executor.js_context != null) return error.Only1IsolatedContextSupported; - _ = try self.executor.createJsContext(&page.window, page, {}, false); + _ = try self.executor.createJsContext(&page.window, page, {}, false, null); } }; diff --git a/src/cdp/domains/page.zig b/src/cdp/domains/page.zig index 36150619..f2c7d9d0 100644 --- a/src/cdp/domains/page.zig +++ b/src/cdp/domains/page.zig @@ -284,9 +284,6 @@ pub fn pageCreated(bc: anytype, page: *Page) !void { if (bc.isolated_world) |*isolated_world| { // We need to recreate the isolated world context try isolated_world.createContext(page); - - const polyfill = @import("../../browser/polyfill/polyfill.zig"); - try polyfill.load(bc.arena, &isolated_world.executor.js_context.?); } } diff --git a/src/log.zig b/src/log.zig index e00621ee..b50cfc3c 100644 --- a/src/log.zig +++ b/src/log.zig @@ -39,6 +39,7 @@ pub const Scope = enum { unknown_prop, web_api, xhr, + polyfill, }; const Opts = struct { diff --git a/src/main_wpt.zig b/src/main_wpt.zig index 3f2396ee..94a51132 100644 --- a/src/main_wpt.zig +++ b/src/main_wpt.zig @@ -126,8 +126,6 @@ fn run( }); defer runner.deinit(); - try polyfill.load(arena, runner.page.main_context); - // loop over the scripts. const doc = parser.documentHTMLToDocument(runner.page.window.document); const scripts = try parser.documentGetElementsByTagName(doc, "script"); diff --git a/src/runtime/js.zig b/src/runtime/js.zig index 80585970..d62a1714 100644 --- a/src/runtime/js.zig +++ b/src/runtime/js.zig @@ -346,7 +346,7 @@ pub fn Env(comptime State: type, comptime WebApis: type) type { // when the handle_scope is freed. // We also maintain our own "context_arena" which allows us to have // all page related memory easily managed. - pub fn createJsContext(self: *ExecutionWorld, global: anytype, state: State, module_loader: anytype, enter: bool) !*JsContext { + pub fn createJsContext(self: *ExecutionWorld, global: anytype, state: State, module_loader: anytype, enter: bool, global_callback: ?GlobalMissingCallback) !*JsContext { std.debug.assert(self.js_context == null); const ModuleLoader = switch (@typeInfo(@TypeOf(module_loader))) { @@ -375,6 +375,30 @@ pub fn Env(comptime State: type, comptime WebApis: type) type { const global_template = js_global.getInstanceTemplate(); global_template.setInternalFieldCount(1); + // Configure the missing property callback on the global + // object. + if (global_callback != null) { + const configuration = v8.NamedPropertyHandlerConfiguration{ + .getter = struct { + fn callback(c_name: ?*const v8.C_Name, raw_info: ?*const v8.C_PropertyCallbackInfo) callconv(.c) u8 { + const info = v8.PropertyCallbackInfo.initFromV8(raw_info); + const _isolate = info.getIsolate(); + const v8_context = _isolate.getCurrentContext(); + + const js_context: *JsContext = @ptrFromInt(v8_context.getEmbedderData(1).castTo(v8.BigInt).getUint64()); + + const property = valueToString(js_context.call_arena, .{ .handle = c_name.? }, _isolate, v8_context) catch "???"; + if (js_context.global_callback.?.missing(property, js_context)) { + return v8.Intercepted.Yes; + } + return v8.Intercepted.No; + } + }.callback, + .flags = v8.PropertyHandlerFlags.NonMasking | v8.PropertyHandlerFlags.OnlyInterceptStrings, + }; + global_template.setNamedProperty(configuration, null); + } + // All the FunctionTemplates that we created and setup in Env.init // are now going to get associated with our global instance. inline for (Types, 0..) |s, i| { @@ -456,6 +480,7 @@ pub fn Env(comptime State: type, comptime WebApis: type) type { .ptr = safe_module_loader, .func = ModuleLoader.fetchModuleSource, }, + .global_callback = global_callback, }; var js_context = &self.js_context.?; @@ -608,6 +633,9 @@ pub fn Env(comptime State: type, comptime WebApis: type) type { // necessary to lookup/store the dependent module in the module_cache. module_identifier: std.AutoHashMapUnmanaged(u32, []const u8) = .empty, + // Global callback is called on missing property. + global_callback: ?GlobalMissingCallback = null, + const ModuleLoader = struct { ptr: *anyopaque, func: *const fn (ptr: *anyopaque, specifier: []const u8) anyerror!?[]const u8, @@ -2546,6 +2574,35 @@ pub fn Env(comptime State: type, comptime WebApis: type) type { self.callScopeEndFn(self.ptr); } }; + + // Callback called on global's property mssing. + // Return true to intercept the exectution or false to let the call + // continue the chain. + pub const GlobalMissingCallback = struct { + ptr: *anyopaque, + missingFn: *const fn (ptr: *anyopaque, name: []const u8, ctx: *JsContext) bool, + + pub fn init(ptr: anytype) GlobalMissingCallback { + const T = @TypeOf(ptr); + const ptr_info = @typeInfo(T); + + const gen = struct { + pub fn missing(pointer: *anyopaque, name: []const u8, ctx: *JsContext) bool { + const self: T = @ptrCast(@alignCast(pointer)); + return ptr_info.pointer.child.missing(self, name, ctx); + } + }; + + return .{ + .ptr = ptr, + .missingFn = gen.missing, + }; + } + + pub fn missing(self: GlobalMissingCallback, name: []const u8, ctx: *JsContext) bool { + return self.missingFn(self.ptr, name, ctx); + } + }; }; } @@ -3431,7 +3488,7 @@ fn valueToDetailString(arena: Allocator, value: v8.Value, isolate: v8.Isolate, v if (debugValueToString(arena, value.castTo(v8.Object), isolate, v8_context)) |ds| { return ds; } else |err| { - log.err(.js, "debug serialize value", .{.err = err}); + log.err(.js, "debug serialize value", .{ .err = err }); } } } diff --git a/src/runtime/testing.zig b/src/runtime/testing.zig index 25695232..87246d8e 100644 --- a/src/runtime/testing.zig +++ b/src/runtime/testing.zig @@ -53,6 +53,7 @@ pub fn Runner(comptime State: type, comptime Global: type, comptime types: anyty state, {}, true, + null, ); return self; } From bdfceec520add0aeaf0e1d98ff588907d9c5a365 Mon Sep 17 00:00:00 2001 From: Pierre Tachoire Date: Mon, 7 Jul 2025 14:29:04 -0700 Subject: [PATCH 2/4] refacto a bit the missing callback into polyfill Add a debug global unknown property --- src/browser/polyfill/polyfill.zig | 39 ++++++++++++++++++++++++++++--- 1 file changed, 36 insertions(+), 3 deletions(-) diff --git a/src/browser/polyfill/polyfill.zig b/src/browser/polyfill/polyfill.zig index e7e69600..65721958 100644 --- a/src/browser/polyfill/polyfill.zig +++ b/src/browser/polyfill/polyfill.zig @@ -24,6 +24,8 @@ const Allocator = std.mem.Allocator; const Env = @import("../env.zig").Env; pub const Loader = struct { + state: enum { empty, loading } = .empty, + done: struct { fetch: bool = false, } = .{}, @@ -42,9 +44,14 @@ pub const Loader = struct { } pub fn missing(self: *Loader, name: []const u8, js_context: *Env.JsContext) bool { - if (!self.done.fetch and std.mem.eql(u8, name, "fetch")) { - // load the polyfill once. - self.done.fetch = true; + // Avoid recursive calls during polyfill loading. + if (self.state == .loading) { + return false; + } + + if (!self.done.fetch and isFetch(name)) { + self.state = .loading; + defer self.state = .empty; const _name = "fetch"; const source = @import("fetch.zig").source; @@ -52,7 +59,33 @@ pub const Loader = struct { load(_name, source, js_context) catch |err| { log.fatal(.app, "polyfill load", .{ .name = name, .err = err }); }; + + // load the polyfill once. + self.done.fetch = true; + + // We return false here: We want v8 to continue the calling chain + // to finally find the polyfill we just inserted. If we want to + // return false and stops the call chain, we have to use + // `info.GetReturnValue.Set()` function, or `undefined` will be + // returned immediately. + return false; } + + if (comptime builtin.mode == .Debug) { + log.debug(.unknown_prop, "unkown global property", .{ + .info = "but the property can exist in pure JS", + .property = name, + }); + } + + return false; + } + + fn isFetch(name: []const u8) bool { + if (std.mem.eql(u8, name, "fetch")) return true; + if (std.mem.eql(u8, name, "Request")) return true; + if (std.mem.eql(u8, name, "Response")) return true; + if (std.mem.eql(u8, name, "Headers")) return true; return false; } }; From 13c623755ce4fe4e13301739b03bc9ef4b969065 Mon Sep 17 00:00:00 2001 From: Pierre Tachoire Date: Mon, 7 Jul 2025 14:29:48 -0700 Subject: [PATCH 3/4] js: remove existing unknown property debug Because it will be displayed only if the property is non-native. So if your property is set in pureJS, you will still have the log... --- src/log.zig | 2 +- src/runtime/js.zig | 30 ------------------------------ 2 files changed, 1 insertion(+), 31 deletions(-) diff --git a/src/log.zig b/src/log.zig index b50cfc3c..19623600 100644 --- a/src/log.zig +++ b/src/log.zig @@ -45,7 +45,7 @@ pub const Scope = enum { const Opts = struct { format: Format = if (is_debug) .pretty else .logfmt, level: Level = if (is_debug) .info else .warn, - filter_scopes: []const Scope = &.{.unknown_prop}, + filter_scopes: []const Scope = &.{}, }; pub var opts = Opts{}; diff --git a/src/runtime/js.zig b/src/runtime/js.zig index d62a1714..70d7ef7e 100644 --- a/src/runtime/js.zig +++ b/src/runtime/js.zig @@ -2199,11 +2199,6 @@ pub fn Env(comptime State: type, comptime WebApis: type) type { fn generateNamedIndexer(comptime Struct: type, template_proto: v8.ObjectTemplate) void { if (@hasDecl(Struct, "named_get") == false) { - if (comptime builtin.mode == .Debug) { - if (log.enabled(.unknown_prop, .debug)) { - generateDebugNamedIndexer(Struct, template_proto); - } - } return; } @@ -2263,31 +2258,6 @@ pub fn Env(comptime State: type, comptime WebApis: type) type { template_proto.setNamedProperty(configuration, null); } - fn generateDebugNamedIndexer(comptime Struct: type, template_proto: v8.ObjectTemplate) void { - const configuration = v8.NamedPropertyHandlerConfiguration{ - .getter = struct { - fn callback(c_name: ?*const v8.C_Name, raw_info: ?*const v8.C_PropertyCallbackInfo) callconv(.c) u8 { - const info = v8.PropertyCallbackInfo.initFromV8(raw_info); - const isolate = info.getIsolate(); - const v8_context = isolate.getCurrentContext(); - - const js_context: *JsContext = @ptrFromInt(v8_context.getEmbedderData(1).castTo(v8.BigInt).getUint64()); - - const property = valueToString(js_context.call_arena, .{ .handle = c_name.? }, isolate, v8_context) catch "???"; - log.debug(.unknown_prop, "unkown property", .{ .@"struct" = @typeName(Struct), .property = property }); - return v8.Intercepted.No; - } - }.callback, - - // This is really cool. Without this, we'd intercept _all_ properties - // even those explicitly set. So, node.length for example would get routed - // to our `named_get`, rather than a `get_length`. This might be - // useful if we run into a type that we can't model properly in Zig. - .flags = v8.PropertyHandlerFlags.OnlyInterceptStrings | v8.PropertyHandlerFlags.NonMasking, - }; - template_proto.setNamedProperty(configuration, null); - } - fn generateUndetectable(comptime Struct: type, template: v8.ObjectTemplate) void { const has_js_call_as_function = @hasDecl(Struct, "jsCallAsFunction"); From 2cdc9e9f5fc32e05c80f1899098f8d29f509b1e6 Mon Sep 17 00:00:00 2001 From: Pierre Tachoire Date: Mon, 7 Jul 2025 16:27:37 -0700 Subject: [PATCH 4/4] cdp: use a polyfill loader per isolate --- src/cdp/cdp.zig | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/cdp/cdp.zig b/src/cdp/cdp.zig index a1d25e42..2472481a 100644 --- a/src/cdp/cdp.zig +++ b/src/cdp/cdp.zig @@ -30,6 +30,8 @@ const Inspector = @import("../browser/env.zig").Env.Inspector; const Incrementing = @import("../id.zig").Incrementing; const Notification = @import("../notification.zig").Notification; +const polyfill = @import("../browser/polyfill/polyfill.zig"); + pub const URL_BASE = "chrome://newtab/"; pub const LOADER_ID = "LOADERID24DD2FD56CF1EF33C965C79C"; @@ -554,6 +556,10 @@ const IsolatedWorld = struct { executor: Env.ExecutionWorld, grant_universal_access: bool, + // Polyfill loader for the isolated world. + // We want to load polyfill in the world's context. + polyfill_loader: polyfill.Loader = .{}, + pub fn deinit(self: *IsolatedWorld) void { self.executor.deinit(); } @@ -569,7 +575,13 @@ const IsolatedWorld = struct { // Currently we have only 1 page/frame and thus also only 1 state in the isolate world. pub fn createContext(self: *IsolatedWorld, page: *Page) !void { if (self.executor.js_context != null) return error.Only1IsolatedContextSupported; - _ = try self.executor.createJsContext(&page.window, page, {}, false, null); + _ = try self.executor.createJsContext( + &page.window, + page, + {}, + false, + Env.GlobalMissingCallback.init(&self.polyfill_loader), + ); } };