From a67f46b550e54ef4d10ae0dd15d040a1965f122b Mon Sep 17 00:00:00 2001 From: Muki Kiboigo Date: Fri, 2 Jan 2026 21:02:51 -0800 Subject: [PATCH 1/4] add named access on the Window object --- src/browser/js/ExecutionWorld.zig | 56 ++++++++++++++-------- src/browser/js/Object.zig | 2 +- src/browser/js/Snapshot.zig | 20 ++++---- src/browser/tests/window/named_access.html | 21 ++++++++ 4 files changed, 69 insertions(+), 30 deletions(-) create mode 100644 src/browser/tests/window/named_access.html diff --git a/src/browser/js/ExecutionWorld.zig b/src/browser/js/ExecutionWorld.zig index 372709cb..2c381b9a 100644 --- a/src/browser/js/ExecutionWorld.zig +++ b/src/browser/js/ExecutionWorld.zig @@ -74,26 +74,23 @@ pub fn createContext(self: *ExecutionWorld, page: *Page, enter: bool) !*Context const env = self.env; const isolate = env.isolate; + const arena = self.context_arena.allocator(); var v8_context: v8.Context = blk: { var temp_scope: v8.HandleScope = undefined; v8.HandleScope.init(&temp_scope, isolate); defer temp_scope.deinit(); - if (comptime IS_DEBUG) { - // Getting this into the snapshot is tricky (anything involving the - // global is tricky). Easier to do here, and in debug mode, we're - // fine with paying the small perf hit. - const js_global = v8.FunctionTemplate.initDefault(isolate); - const global_template = js_global.getInstanceTemplate(); + // Creates a global template that inherits from Window. + const global_template = @import("Snapshot.zig").createGlobalTemplate(isolate, env.templates); - global_template.setNamedProperty(v8.NamedPropertyHandlerConfiguration{ - .getter = unknownPropertyCallback, - .flags = v8.PropertyHandlerFlags.NonMasking | v8.PropertyHandlerFlags.OnlyInterceptStrings, - }, null); - } + // Add the named property handler + global_template.setNamedProperty(v8.NamedPropertyHandlerConfiguration{ + .getter = unknownPropertyCallback, + .flags = v8.PropertyHandlerFlags.NonMasking | v8.PropertyHandlerFlags.OnlyInterceptStrings, + }, v8.External.init(isolate, page)); - const context_local = v8.Context.init(isolate, null, null); + const context_local = v8.Context.init(isolate, global_template, null); const v8_context = v8.Persistent(v8.Context).init(isolate, context_local).castToContext(); break :blk v8_context; }; @@ -124,7 +121,7 @@ pub fn createContext(self: *ExecutionWorld, page: *Page, enter: bool) !*Context .handle_scope = handle_scope, .script_manager = &page._script_manager, .call_arena = page.call_arena, - .arena = self.context_arena.allocator(), + .arena = arena, }; var context = &self.context.?; @@ -159,9 +156,13 @@ pub fn resumeExecution(self: *const ExecutionWorld) void { pub fn unknownPropertyCallback(c_name: ?*const v8.C_Name, raw_info: ?*const v8.C_PropertyCallbackInfo) callconv(.c) u8 { const info = v8.PropertyCallbackInfo.initFromV8(raw_info); - const context = Context.fromIsolate(info.getIsolate()); - const property = context.valueToString(.{ .handle = c_name.? }, .{}) catch "???"; + const data_value = info.getData(); + const external = data_value.castTo(v8.External); + const page: *Page = @ptrCast(@alignCast(external.get())); + + const context = Context.fromIsolate(info.getIsolate()); + const maybe_property: ?[]u8 = context.valueToString(.{ .handle = c_name.? }, .{}) catch null; const ignored = std.StaticStringMap(void).initComptime(.{ .{ "process", {} }, @@ -185,12 +186,25 @@ pub fn unknownPropertyCallback(c_name: ?*const v8.C_Name, raw_info: ?*const v8.C .{ "CLOSURE_FLAGS", {} }, }); - if (!ignored.has(property)) { - log.debug(.unknown_prop, "unkown global property", .{ - .info = "but the property can exist in pure JS", - .stack = context.stackTrace() catch "???", - .property = property, - }); + if (maybe_property) |prop| { + if (!ignored.has(prop)) { + const document = page.document; + + if (document.getElementById(prop)) |el| { + const js_value = context.zigValueToJs(el, .{}) catch { + return v8.Intercepted.No; + }; + + info.getReturnValue().set(js_value); + return v8.Intercepted.Yes; + } + + log.debug(.unknown_prop, "unknown global property", .{ + .info = "but the property can exist in pure JS", + .stack = context.stackTrace() catch "???", + .property = prop, + }); + } } return v8.Intercepted.No; diff --git a/src/browser/js/Object.zig b/src/browser/js/Object.zig index f2bc3a4d..4520e396 100644 --- a/src/browser/js/Object.zig +++ b/src/browser/js/Object.zig @@ -54,7 +54,7 @@ pub fn set(self: Object, key: []const u8, value: anytype, opts: SetOpts) error{ const context = self.context; const js_key = v8.String.initUtf8(context.isolate, key); - const js_value = try context.zigValueToJs(value); + const js_value = try context.zigValueToJs(value, .{}); const res = self.js_obj.defineOwnProperty(context.v8_context, js_key.toName(), js_value, @bitCast(opts)) orelse false; if (!res) { diff --git a/src/browser/js/Snapshot.zig b/src/browser/js/Snapshot.zig index d93a0f97..6a89e5e5 100644 --- a/src/browser/js/Snapshot.zig +++ b/src/browser/js/Snapshot.zig @@ -113,6 +113,17 @@ fn isValid(self: Snapshot) bool { return v8.SnapshotCreator.startupDataIsValid(self.startup_data); } +pub fn createGlobalTemplate(isolate: v8.Isolate, templates: []const v8.FunctionTemplate) v8.ObjectTemplate { + // Set up the global template to inherit from Window's template + // This way the global object gets all Window properties through inheritance + const js_global = v8.FunctionTemplate.initDefault(isolate); + js_global.setClassName(v8.String.initUtf8(isolate, "Window")); + // Find Window in JsApis by name (avoids circular import) + const window_index = comptime bridge.JsApiLookup.getId(Window.JsApi); + js_global.inherit(templates[window_index]); + return js_global.getInstanceTemplate(); +} + pub fn create(allocator: Allocator) !Snapshot { var external_references = collectExternalReferences(); @@ -154,14 +165,7 @@ pub fn create(allocator: Allocator) !Snapshot { // Set up the global template to inherit from Window's template // This way the global object gets all Window properties through inheritance - const js_global = v8.FunctionTemplate.initDefault(isolate); - js_global.setClassName(v8.String.initUtf8(isolate, "Window")); - - // Find Window in JsApis by name (avoids circular import) - const window_index = comptime bridge.JsApiLookup.getId(Window.JsApi); - js_global.inherit(templates[window_index]); - - const global_template = js_global.getInstanceTemplate(); + const global_template = createGlobalTemplate(isolate, templates[0..]); const context = v8.Context.init(isolate, global_template, null); context.enter(); diff --git a/src/browser/tests/window/named_access.html b/src/browser/tests/window/named_access.html new file mode 100644 index 00000000..4663dbdf --- /dev/null +++ b/src/browser/tests/window/named_access.html @@ -0,0 +1,21 @@ + + + +
+
+ +

+ + + + From b911051842b5a6fa1818608f82a42f37e5b656c1 Mon Sep 17 00:00:00 2001 From: Muki Kiboigo Date: Mon, 5 Jan 2026 09:08:57 -0800 Subject: [PATCH 2/4] add named access shadowing test --- src/browser/tests/window/named_access.html | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/browser/tests/window/named_access.html b/src/browser/tests/window/named_access.html index 4663dbdf..7d43eb8c 100644 --- a/src/browser/tests/window/named_access.html +++ b/src/browser/tests/window/named_access.html @@ -19,3 +19,8 @@ testing.expectEqual('mySpan', window.mySpan.id); testing.expectEqual('paragraph', window.paragraph.id); + + From 05da040ce130b83a3a679d41e4535e7d7350c3d8 Mon Sep 17 00:00:00 2001 From: Muki Kiboigo Date: Mon, 5 Jan 2026 09:27:17 -0800 Subject: [PATCH 3/4] increment call_depth on callWithThis --- src/browser/js/Function.zig | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/browser/js/Function.zig b/src/browser/js/Function.zig index aa87f57a..47ddbdb1 100644 --- a/src/browser/js/Function.zig +++ b/src/browser/js/Function.zig @@ -115,6 +115,8 @@ pub fn tryCallWithThis(self: *const Function, comptime T: type, this: anytype, a pub fn callWithThis(self: *const Function, comptime T: type, this: anytype, args: anytype) !T { const context = self.context; + context.call_depth += 1; + defer context.call_depth -= 1; const js_this = try context.valueToExistingObject(this); From 645ec79fceec747d47c3ec9efd5337a2da7b84fa Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Tue, 6 Jan 2026 18:04:17 +0800 Subject: [PATCH 4/4] access page from context, document call_depth usage --- src/browser/js/ExecutionWorld.zig | 8 ++------ src/browser/js/Function.zig | 15 +++++++++++++-- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/src/browser/js/ExecutionWorld.zig b/src/browser/js/ExecutionWorld.zig index 2c381b9a..b4a45f78 100644 --- a/src/browser/js/ExecutionWorld.zig +++ b/src/browser/js/ExecutionWorld.zig @@ -88,7 +88,7 @@ pub fn createContext(self: *ExecutionWorld, page: *Page, enter: bool) !*Context global_template.setNamedProperty(v8.NamedPropertyHandlerConfiguration{ .getter = unknownPropertyCallback, .flags = v8.PropertyHandlerFlags.NonMasking | v8.PropertyHandlerFlags.OnlyInterceptStrings, - }, v8.External.init(isolate, page)); + }, null); const context_local = v8.Context.init(isolate, global_template, null); const v8_context = v8.Persistent(v8.Context).init(isolate, context_local).castToContext(); @@ -157,10 +157,6 @@ pub fn resumeExecution(self: *const ExecutionWorld) void { pub fn unknownPropertyCallback(c_name: ?*const v8.C_Name, raw_info: ?*const v8.C_PropertyCallbackInfo) callconv(.c) u8 { const info = v8.PropertyCallbackInfo.initFromV8(raw_info); - const data_value = info.getData(); - const external = data_value.castTo(v8.External); - const page: *Page = @ptrCast(@alignCast(external.get())); - const context = Context.fromIsolate(info.getIsolate()); const maybe_property: ?[]u8 = context.valueToString(.{ .handle = c_name.? }, .{}) catch null; @@ -188,7 +184,7 @@ pub fn unknownPropertyCallback(c_name: ?*const v8.C_Name, raw_info: ?*const v8.C if (maybe_property) |prop| { if (!ignored.has(prop)) { - const document = page.document; + const document = context.page.document; if (document.getElementById(prop)) |el| { const js_value = context.zigValueToJs(el, .{}) catch { diff --git a/src/browser/js/Function.zig b/src/browser/js/Function.zig index 47ddbdb1..7e88b926 100644 --- a/src/browser/js/Function.zig +++ b/src/browser/js/Function.zig @@ -115,8 +115,19 @@ pub fn tryCallWithThis(self: *const Function, comptime T: type, this: anytype, a pub fn callWithThis(self: *const Function, comptime T: type, this: anytype, args: anytype) !T { const context = self.context; - context.call_depth += 1; - defer context.call_depth -= 1; + + // When we're calling a function from within JavaScript itself, this isn't + // necessary. We're within a Caller instantiation, which will already have + // incremented the call_depth and it won't decrement it until the Caller is + // done. + // But some JS functions are initiated from Zig code, and not v8. For + // example, Observers, some event and window callbacks. In those cases, we + // need to increase the call_depth so that the call_arena remains valid for + // the duration of the function call. If we don't do this, the call_arena + // will be reset after each statement of the function which executes Zig code. + const call_depth = context.call_depth; + context.call_depth = call_depth + 1; + defer context.call_depth = call_depth; const js_this = try context.valueToExistingObject(this);