diff --git a/build.zig.zon b/build.zig.zon index f6c231bb..5ac488bd 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -5,8 +5,8 @@ .minimum_zig_version = "0.15.2", .dependencies = .{ .v8 = .{ - .url = "https://github.com/lightpanda-io/zig-v8-fork/archive/refs/tags/v0.3.7.tar.gz", - .hash = "v8-0.0.0-xddH67uBBAD95hWsPQz3Ni1PlZjdywtPXrGUAp8rSKco", + .url = "https://github.com/lightpanda-io/zig-v8-fork/archive/715ccbae21d7528eba951f78af4dfd48835fc172.tar.gz", + .hash = "v8-0.0.0-xddH65-HBADXFCII9ucZE3NgbkWmwsbTbsx8qevYVki5", }, // .v8 = .{ .path = "../zig-v8-fork" }, .brotli = .{ diff --git a/src/browser/js/Caller.zig b/src/browser/js/Caller.zig index e3962313..daacbd17 100644 --- a/src/browser/js/Caller.zig +++ b/src/browser/js/Caller.zig @@ -55,15 +55,9 @@ fn initWithContext(self: *Caller, ctx: *Context, v8_context: *const v8.Context) .isolate = ctx.isolate, }, .prev_local = ctx.local, - .prev_context = switch (ctx.global) { - .page => |page| page.js, - .worker => |worker| worker.js, - }, + .prev_context = ctx.global.getJs(), }; - switch (ctx.global) { - .page => |page| page.js = ctx, - .worker => |worker| worker.js = ctx, - } + ctx.global.setJs(ctx); ctx.local = &self.local; } @@ -94,10 +88,7 @@ pub fn deinit(self: *Caller) void { ctx.call_depth = call_depth; ctx.local = self.prev_local; - switch (ctx.global) { - .page => |page| page.js = self.prev_context, - .worker => |worker| worker.js = self.prev_context, - } + ctx.global.setJs(self.prev_context); } pub const CallOpts = struct { @@ -444,10 +435,6 @@ fn isPage(comptime T: type) bool { return T == *Page or T == *const Page; } -fn isWorker(comptime T: type) bool { - return T == *WorkerGlobalScope or T == *const WorkerGlobalScope; -} - fn isExecution(comptime T: type) bool { return T == *js.Execution or T == *const js.Execution; } @@ -456,21 +443,12 @@ fn getGlobalArg(comptime T: type, ctx: *Context) T { if (comptime isPage(T)) { return switch (ctx.global) { .page => |page| page, - .worker => { - if (comptime IS_DEBUG) std.debug.assert(false); - unreachable; - }, + .worker => unreachable, }; } - if (comptime isWorker(T)) { - return switch (ctx.global) { - .page => { - if (comptime IS_DEBUG) std.debug.assert(false); - unreachable; - }, - .worker => |worker| worker, - }; + if (comptime isExecution(T)) { + return &ctx.execution; } @compileError("Unsupported global arg type: " ++ @typeName(T)); @@ -748,17 +726,11 @@ fn getArgs(comptime F: type, comptime offset: usize, local: *const Local, info: // from our params slice, because we don't want to bind it to // a JS argument const LastParamType = params[params.len - 1].type.?; - if (comptime isPage(LastParamType) or isWorker(LastParamType)) { + if (comptime isPage(LastParamType) or isExecution(LastParamType)) { @field(args, tupleFieldName(params.len - 1 + offset)) = getGlobalArg(LastParamType, local.ctx); break :blk params[0 .. params.len - 1]; } - // If the last parameter is Execution, set it from the context - if (comptime isExecution(LastParamType)) { - @field(args, tupleFieldName(params.len - 1 + offset)) = &local.ctx.execution; - break :blk params[0 .. params.len - 1]; - } - // we have neither a Page, Execution, nor a JsObject. All params must be // bound to a JavaScript value. break :blk params; diff --git a/src/browser/js/Context.zig b/src/browser/js/Context.zig index 3f41053e..44102418 100644 --- a/src/browser/js/Context.zig +++ b/src/browser/js/Context.zig @@ -45,6 +45,27 @@ const Context = @This(); pub const GlobalScope = union(enum) { page: *Page, worker: *WorkerGlobalScope, + + pub fn base(self: GlobalScope) [:0]const u8 { + return switch (self) { + .page => |page| page.base(), + .worker => |worker| worker.base(), + }; + } + + pub fn getJs(self: GlobalScope) *Context { + return switch (self) { + .page => |page| page.js, + .worker => |worker| worker.js, + }; + } + + pub fn setJs(self: GlobalScope, ctx: *Context) void { + switch (self) { + .page => |page| page.js = ctx, + .worker => |worker| worker.js = ctx, + } + } }; id: usize, @@ -547,10 +568,7 @@ pub fn dynamicModuleCallback( if (resource_value.isNullOrUndefined()) { // will only be null / undefined in extreme cases (e.g. WPT tests) // where you're - break :blk switch (self.global) { - .page => |page| page.base(), - .worker => |worker| worker.base(), - }; + break :blk self.global.base(); } break :blk js.String.toSliceZ(.{ .local = &local, .handle = resource_name.? }) catch |err| { @@ -890,18 +908,8 @@ pub fn enter(self: *Context, hs: *js.HandleScope) Entered { const isolate = self.isolate; js.HandleScope.init(hs, isolate); - const original = switch (self.global) { - .page => |page| blk: { - const orig = page.js; - page.js = self; - break :blk orig; - }, - .worker => |worker| blk: { - const orig = worker.js; - worker.js = self; - break :blk orig; - }, - }; + const original = self.global.getJs(); + self.global.setJs(self); const handle: *const v8.Context = @ptrCast(v8.v8__Global__Get(&self.handle, isolate.handle)); v8.v8__Context__Enter(handle); @@ -920,10 +928,7 @@ const Entered = struct { global: GlobalScope, pub fn exit(self: Entered) void { - switch (self.global) { - .page => |page| page.js = self.original, - .worker => |worker| worker.js = self.original, - } + self.global.setJs(self.original); v8.v8__Context__Exit(self.handle); self.handle_scope.deinit(); } @@ -934,12 +939,7 @@ pub fn queueMutationDelivery(self: *Context) !void { fn run(ctx: *Context) void { switch (ctx.global) { .page => |page| page.deliverMutations(), - .worker => { - if (comptime IS_DEBUG) { - std.debug.assert(false); - } - unreachable; - }, + .worker => unreachable, } } }.run); @@ -950,12 +950,7 @@ pub fn queueIntersectionChecks(self: *Context) !void { fn run(ctx: *Context) void { switch (ctx.global) { .page => |page| page.performScheduledIntersectionChecks(), - .worker => { - if (comptime IS_DEBUG) { - std.debug.assert(false); - } - unreachable; - }, + .worker => unreachable, } } }.run); @@ -966,12 +961,7 @@ pub fn queueIntersectionDelivery(self: *Context) !void { fn run(ctx: *Context) void { switch (ctx.global) { .page => |page| page.deliverIntersections(), - .worker => { - if (comptime IS_DEBUG) { - std.debug.assert(false); - } - unreachable; - }, + .worker => unreachable, } } }.run); @@ -982,12 +972,7 @@ pub fn queueSlotchangeDelivery(self: *Context) !void { fn run(ctx: *Context) void { switch (ctx.global) { .page => |page| page.deliverSlotchangeEvents(), - .worker => { - if (comptime IS_DEBUG) { - std.debug.assert(false); - } - unreachable; - }, + .worker => unreachable, } } }.run); diff --git a/src/browser/js/Env.zig b/src/browser/js/Env.zig index 4b4369ea..a0bbcb2c 100644 --- a/src/browser/js/Env.zig +++ b/src/browser/js/Env.zig @@ -34,6 +34,7 @@ const Inspector = @import("Inspector.zig"); const Page = @import("../Page.zig"); const Window = @import("../webapi/Window.zig"); +const WorkerGlobalScope = @import("../webapi/WorkerGlobalScope.zig"); const JsApis = bridge.JsApis; const Allocator = std.mem.Allocator; @@ -83,9 +84,6 @@ eternal_function_templates: []v8.Eternal, // Dynamic slice to avoid circular dependency on JsApis.len at comptime templates: []*const v8.FunctionTemplate, -// Global template created once per isolate and reused across all contexts -global_template: v8.Eternal, - // Inspector associated with the Isolate. Exists when CDP is being used. inspector: ?*Inspector, @@ -146,7 +144,6 @@ pub fn init(app: *App, opts: InitOpts) !Env { const templates = try allocator.alloc(*const v8.FunctionTemplate, JsApis.len); errdefer allocator.free(templates); - var global_eternal: v8.Eternal = undefined; var private_symbols: PrivateSymbols = undefined; { var temp_scope: js.HandleScope = undefined; @@ -164,44 +161,6 @@ pub fn init(app: *App, opts: InitOpts) !Env { templates[i] = @ptrCast(@alignCast(eternal_ptr.?)); } - // Create global template once per isolate - const js_global = v8.v8__FunctionTemplate__New__DEFAULT(isolate_handle); - const window_name = v8.v8__String__NewFromUtf8(isolate_handle, "Window", v8.kNormal, 6); - v8.v8__FunctionTemplate__SetClassName(js_global, window_name); - - // Find Window in JsApis by name (avoids circular import) - const window_index = comptime bridge.JsApiLookup.getId(Window.JsApi); - v8.v8__FunctionTemplate__Inherit(js_global, templates[window_index]); - - const global_template_local = v8.v8__FunctionTemplate__InstanceTemplate(js_global).?; - v8.v8__ObjectTemplate__SetNamedHandler(global_template_local, &.{ - .getter = bridge.unknownWindowPropertyCallback, - .setter = null, - .query = null, - .deleter = null, - .enumerator = null, - .definer = null, - .descriptor = null, - .data = null, - .flags = v8.kOnlyInterceptStrings | v8.kNonMasking, - }); - // I don't 100% understand this. We actually set this up in the snapshot, - // but for the global instance, it doesn't work. SetIndexedHandler and - // SetNamedHandler are set on the Instance template, and that's the key - // difference. The context has its own global instance, so we need to set - // these back up directly on it. There might be a better way to do this. - v8.v8__ObjectTemplate__SetIndexedHandler(global_template_local, &.{ - .getter = Window.JsApi.index.getter, - .setter = null, - .query = null, - .deleter = null, - .enumerator = null, - .definer = null, - .descriptor = null, - .data = null, - .flags = 0, - }); - v8.v8__Eternal__New(isolate_handle, @ptrCast(global_template_local), &global_eternal); private_symbols = PrivateSymbols.init(isolate_handle); } @@ -221,7 +180,6 @@ pub fn init(app: *App, opts: InitOpts) !Env { .templates = templates, .isolate_params = params, .inspector = inspector, - .global_template = global_eternal, .private_symbols = private_symbols, .microtask_queues_are_running = false, .eternal_function_templates = eternal_function_templates, @@ -261,6 +219,17 @@ pub const ContextParams = struct { }; pub fn createContext(self: *Env, page: *Page, params: ContextParams) !*Context { + return self._createContext(page, params); +} + +pub fn createWorkerContext(self: *Env, worker: *WorkerGlobalScope, params: ContextParams) !*Context { + return self._createContext(worker, params); +} + +fn _createContext(self: *Env, global: anytype, params: ContextParams) !*Context { + const T = @TypeOf(global); + const is_page = T == *Page; + const context_arena = try self.app.arena_pool.acquire(.{ .debug = params.debug_name }); errdefer self.app.arena_pool.release(context_arena); @@ -273,12 +242,10 @@ pub fn createContext(self: *Env, page: *Page, params: ContextParams) !*Context { const microtask_queue = v8.v8__MicrotaskQueue__New(isolate.handle, v8.kExplicit).?; errdefer v8.v8__MicrotaskQueue__DELETE(microtask_queue); - // Get the global template that was created once per isolate - const global_template: *const v8.ObjectTemplate = @ptrCast(@alignCast(v8.v8__Eternal__Get(&self.global_template, isolate.handle).?)); - v8.v8__ObjectTemplate__SetInternalFieldCount(global_template, comptime Snapshot.countInternalFields(Window.JsApi)); - - const v8_context = v8.v8__Context__New__Config(isolate.handle, &.{ - .global_template = global_template, + // Restore the context from the snapshot (0 = Page, 1 = Worker) + const snapshot_index: u32 = if (comptime is_page) 0 else 1; + const v8_context = v8.v8__Context__FromSnapshot__Config(isolate.handle, snapshot_index, &.{ + .global_template = null, .global_object = null, .microtask_queue = microtask_queue, }).?; @@ -287,36 +254,36 @@ pub fn createContext(self: *Env, page: *Page, params: ContextParams) !*Context { var context_global: v8.Global = undefined; v8.v8__Global__New(isolate.handle, v8_context, &context_global); - // get the global object for the context, this maps to our Window + // Get the global object for the context const global_obj = v8.v8__Context__Global(v8_context).?; - { - // Store our TAO inside the internal field of the global object. This - // maps the v8::Object -> Zig instance. Almost all objects have this, and - // it gets setup automatically as objects are created, but the Window - // object already exists in v8 (it's the global) so we manually create - // the mapping here. - const tao = try params.identity_arena.create(@import("TaggedOpaque.zig")); - tao.* = .{ - .value = @ptrCast(page.window), - .prototype_chain = (&Window.JsApi.Meta.prototype_chain).ptr, - .prototype_len = @intCast(Window.JsApi.Meta.prototype_chain.len), - .subtype = .node, // this probably isn't right, but it's what we've been doing all along - }; - v8.v8__Object__SetAlignedPointerInInternalField(global_obj, 0, tao); - } + // Store our TAO inside the internal field of the global object. This + // maps the v8::Object -> Zig instance. + const tao = try params.identity_arena.create(@import("TaggedOpaque.zig")); + tao.* = if (comptime is_page) .{ + .value = @ptrCast(global.window), + .prototype_chain = (&Window.JsApi.Meta.prototype_chain).ptr, + .prototype_len = @intCast(Window.JsApi.Meta.prototype_chain.len), + .subtype = .node, + } else .{ + .value = @ptrCast(global), + .prototype_chain = (&WorkerGlobalScope.JsApi.Meta.prototype_chain).ptr, + .prototype_len = @intCast(WorkerGlobalScope.JsApi.Meta.prototype_chain.len), + .subtype = null, + }; + v8.v8__Object__SetAlignedPointerInInternalField(global_obj, 0, tao); const context_id = self.context_id; self.context_id = context_id + 1; - const session = page._session; + const session = global._session; const origin = try session.getOrCreateOrigin(null); errdefer session.releaseOrigin(origin); const context = try context_arena.create(Context); context.* = .{ .env = self, - .global = .{ .page = page }, + .global = if (comptime is_page) .{ .page = global } else .{ .worker = global }, .origin = origin, .id = context_id, .session = session, @@ -326,7 +293,7 @@ pub fn createContext(self: *Env, page: *Page, params: ContextParams) !*Context { .templates = self.templates, .call_arena = params.call_arena, .microtask_queue = microtask_queue, - .script_manager = &page._script_manager, + .script_manager = if (comptime is_page) &global._script_manager else null, .scheduler = .init(context_arena), .identity = params.identity, .identity_arena = params.identity_arena, @@ -334,25 +301,23 @@ pub fn createContext(self: *Env, page: *Page, params: ContextParams) !*Context { }; context.execution = .{ - .buf = &page.buf, + .url = &global.url, + .buf = &global.buf, .context = context, - .arena = page.arena, + .arena = global.arena, .call_arena = params.call_arena, - ._factory = page._factory, + ._factory = global._factory, ._scheduler = &context.scheduler, - .url = &page.url, }; - { - // Multiple contexts can be created for the same Window (via CDP). We only - // need to register the first one. - const gop = try params.identity.identity_map.getOrPut(params.identity_arena, @intFromPtr(page.window)); - if (gop.found_existing == false) { - // our window wrapped in a v8::Global - var global_global: v8.Global = undefined; - v8.v8__Global__New(isolate.handle, global_obj, &global_global); - gop.value_ptr.* = global_global; - } + // Register in the identity map. Multiple contexts can be created for the + // same global (via CDP), so we only register the first one. + const identity_ptr = if (comptime is_page) @intFromPtr(global.window) else @intFromPtr(global); + const gop = try params.identity.identity_map.getOrPut(params.identity_arena, identity_ptr); + if (gop.found_existing == false) { + var global_global: v8.Global = undefined; + v8.v8__Global__New(isolate.handle, global_obj, &global_global); + gop.value_ptr.* = global_global; } // Store a pointer to our context inside the v8 context so that, given @@ -583,3 +548,50 @@ const PrivateSymbols = struct { self.child_nodes.deinit(); } }; + +const testing = @import("../../testing.zig"); +const EventTarget = @import("../webapi/EventTarget.zig"); + +test "Env: Worker context " { + const session = testing.test_session; + + // Create a dummy WorkerGlobalScope using page's resources (hackish until session.createWorker exists) + const worker = try session.factory.eventTarget(WorkerGlobalScope{ + ._session = session, + ._factory = &session.factory, + .arena = session.arena, + .url = "about:blank", + ._proto = undefined, + ._performance = .init(), + }); + + const ctx = try testing.test_browser.env.createWorkerContext(worker, .{ + .identity = &session.identity, + .identity_arena = session.arena, + .call_arena = session.arena, + }); + defer testing.test_browser.env.destroyContext(ctx); + + var ls: js.Local.Scope = undefined; + ctx.localScope(&ls); + defer ls.deinit(); + + try testing.expectEqual(true, (try ls.local.exec("typeof Node === 'undefined'", null)).isTrue()); + try testing.expectEqual(true, (try ls.local.exec("typeof WorkerGlobalScope !== 'undefined'", null)).isTrue()); +} + +test "Env: Page context" { + const session = testing.test_session; + const page = try session.createPage(); + defer session.removePage(); + + // Page already has a context created, use it directly + const ctx = page.js; + + var ls: js.Local.Scope = undefined; + ctx.localScope(&ls); + defer ls.deinit(); + + try testing.expectEqual(true, (try ls.local.exec("typeof Node !== 'undefined'", null)).isTrue()); + try testing.expectEqual(true, (try ls.local.exec("typeof WorkerGlobalScope === 'undefined'", null)).isTrue()); +} diff --git a/src/browser/js/Execution.zig b/src/browser/js/Execution.zig index 7d6ad6fd..877cd9e3 100644 --- a/src/browser/js/Execution.zig +++ b/src/browser/js/Execution.zig @@ -37,11 +37,11 @@ const Execution = @This(); context: *Context, // Fields named to match Page for generic code (executor._factory works for both) -_factory: *Factory, +buf: []u8, arena: Allocator, call_arena: Allocator, +_factory: *Factory, _scheduler: *Scheduler, -buf: []u8, // Pointer to the url field (Page or WorkerGlobalScope) - allows access to current url even after navigation url: *[:0]const u8, diff --git a/src/browser/js/Local.zig b/src/browser/js/Local.zig index 42b430b6..e359c1ec 100644 --- a/src/browser/js/Local.zig +++ b/src/browser/js/Local.zig @@ -337,9 +337,6 @@ pub fn zigValueToJs(self: *const Local, value: anytype, comptime opts: CallOpts) .worker => { // No Worker-related API currently uses this, so haven't // added support for it - if (comptime IS_DEBUG) { - std.debug.assert(false); - } unreachable; }, }; @@ -425,9 +422,6 @@ pub fn zigValueToJs(self: *const Local, value: anytype, comptime opts: CallOpts) .worker => { // No Worker-related API currently uses this, so haven't // added support for it - if (comptime IS_DEBUG) { - std.debug.assert(false); - } unreachable; }, }; diff --git a/src/browser/js/Snapshot.zig b/src/browser/js/Snapshot.zig index e1b4905c..785cfd78 100644 --- a/src/browser/js/Snapshot.zig +++ b/src/browser/js/Snapshot.zig @@ -17,6 +17,7 @@ // along with this program. If not, see . const std = @import("std"); +const lp = @import("lightpanda"); const js = @import("js.zig"); const bridge = @import("bridge.zig"); const log = @import("../../log.zig"); @@ -26,6 +27,7 @@ const IS_DEBUG = @import("builtin").mode == .Debug; const v8 = js.v8; const JsApis = bridge.JsApis; const PageJsApis = bridge.PageJsApis; +const WorkerJsApis = bridge.WorkerJsApis; const Snapshot = @This(); @@ -114,8 +116,6 @@ fn isValid(self: Snapshot) bool { } pub fn create() !Snapshot { - comptime validatePrototypeChains(&JsApis); - var external_references = collectExternalReferences(); var params: v8.CreateParams = undefined; @@ -153,90 +153,45 @@ pub fn create() !Snapshot { } } - const context = v8.v8__Context__New(isolate, null, null); - v8.v8__Context__Enter(context); - defer v8.v8__Context__Exit(context); + // Add ALL templates to snapshot (done once, in any context) + // We need a context to call AddData, so create a temporary one + { + const temp_context = v8.v8__Context__New(isolate, null, null); + v8.v8__Context__Enter(temp_context); + defer v8.v8__Context__Exit(temp_context); - // Add ALL templates to context snapshot - var last_data_index: usize = 0; - inline for (JsApis, 0..) |_, i| { - @setEvalBranchQuota(10_000); - const data_index = v8.v8__SnapshotCreator__AddData(snapshot_creator, @ptrCast(templates[i])); - if (i == 0) { - data_start = data_index; - last_data_index = data_index; - } else { - if (data_index != last_data_index + 1) { - return error.InvalidDataIndex; - } - last_data_index = data_index; - } - } - - const global_obj = v8.v8__Context__Global(context); - - // Attach only PAGE types to the default context's global - inline for (PageJsApis, 0..) |JsApi, i| { - // PageJsApis[i] == JsApis[i] because the PageJsApis are position at the start of the list - const func = v8.v8__FunctionTemplate__GetFunction(templates[i], context); - if (@hasDecl(JsApi.Meta, "name")) { - if (@hasDecl(JsApi.Meta, "constructor_alias")) { - const alias = JsApi.Meta.constructor_alias; - const v8_class_name = v8.v8__String__NewFromUtf8(isolate, alias.ptr, v8.kNormal, @intCast(alias.len)); - var maybe_result: v8.MaybeBool = undefined; - v8.v8__Object__Set(global_obj, context, v8_class_name, func, &maybe_result); - - const name = JsApi.Meta.name; - const illegal_class_name = v8.v8__String__NewFromUtf8(isolate, name.ptr, v8.kNormal, @intCast(name.len)); - var maybe_result2: v8.MaybeBool = undefined; - v8.v8__Object__DefineOwnProperty(global_obj, context, illegal_class_name, func, 0, &maybe_result2); + var last_data_index: usize = 0; + inline for (JsApis, 0..) |_, i| { + @setEvalBranchQuota(10_000); + const data_index = v8.v8__SnapshotCreator__AddData(snapshot_creator, @ptrCast(templates[i])); + if (i == 0) { + data_start = data_index; + last_data_index = data_index; } else { - const name = JsApi.Meta.name; - const v8_class_name = v8.v8__String__NewFromUtf8(isolate, name.ptr, v8.kNormal, @intCast(name.len)); - var maybe_result: v8.MaybeBool = undefined; - var properties: v8.PropertyAttribute = v8.None; - if (@hasDecl(JsApi.Meta, "enumerable") and JsApi.Meta.enumerable == false) { - properties |= v8.DontEnum; + if (data_index != last_data_index + 1) { + return error.InvalidDataIndex; } - v8.v8__Object__DefineOwnProperty(global_obj, context, v8_class_name, func, properties, &maybe_result); + last_data_index = data_index; } } + + // V8 requires a default context. We could probably make this our + // Page context, but having both the Page and Worker context be + // indexed via addContext makes things a little more consistent. + v8.v8__SnapshotCreator__setDefaultContext(snapshot_creator, temp_context); } { - // Delete built-in console so we can inject our own - const console_key = v8.v8__String__NewFromUtf8(isolate, "console", v8.kNormal, 7); - var maybe_deleted: v8.MaybeBool = undefined; - v8.v8__Object__Delete(global_obj, context, console_key, &maybe_deleted); - if (maybe_deleted.value == false) { - return error.ConsoleDeleteError; - } - } - - // Set prototype chains on function objects - // https://groups.google.com/g/v8-users/c/qAQQBmbi--8 - inline for (JsApis, 0..) |JsApi, i| { - if (comptime protoIndexLookup(JsApi)) |proto_index| { - const proto_func = v8.v8__FunctionTemplate__GetFunction(templates[proto_index], context); - const proto_obj: *const v8.Object = @ptrCast(proto_func); - - const self_func = v8.v8__FunctionTemplate__GetFunction(templates[i], context); - const self_obj: *const v8.Object = @ptrCast(self_func); - - var maybe_result: v8.MaybeBool = undefined; - v8.v8__Object__SetPrototype(self_obj, context, proto_obj, &maybe_result); - } + const Window = @import("../webapi/Window.zig"); + const index = try createSnapshotContext(&PageJsApis, Window.JsApi, isolate, snapshot_creator.?, &templates); + std.debug.assert(index == 0); } { - // DOMException prototype setup - const code_str = "DOMException.prototype.__proto__ = Error.prototype"; - const code = v8.v8__String__NewFromUtf8(isolate, code_str.ptr, v8.kNormal, @intCast(code_str.len)); - const script = v8.v8__Script__Compile(context, code, null) orelse return error.ScriptCompileFailed; - _ = v8.v8__Script__Run(script, context) orelse return error.ScriptRunFailed; + const WorkerGlobalScope = @import("../webapi/WorkerGlobalScope.zig"); + const index = try createSnapshotContext(&WorkerJsApis, WorkerGlobalScope.JsApi, isolate, snapshot_creator.?, &templates); + std.debug.assert(index == 1); } - - v8.v8__SnapshotCreator__setDefaultContext(snapshot_creator, context); } const blob = v8.v8__SnapshotCreator__createBlob(snapshot_creator, v8.kKeep); @@ -244,11 +199,127 @@ pub fn create() !Snapshot { return .{ .owns_data = true, .data_start = data_start, - .external_references = external_references, .startup_data = blob, + .external_references = external_references, }; } +fn createSnapshotContext( + comptime ContextApis: []const type, + comptime GlobalScopeApi: type, + isolate: *v8.Isolate, + snapshot_creator: *v8.SnapshotCreator, + templates: []*const v8.FunctionTemplate, +) !usize { + // Create a global template that inherits from the GlobalScopeApi (Window or WorkerGlobalScope) + const global_scope_index = comptime bridge.JsApiLookup.getId(GlobalScopeApi); + const js_global = v8.v8__FunctionTemplate__New__DEFAULT(isolate); + const class_name = v8.v8__String__NewFromUtf8(isolate, GlobalScopeApi.Meta.name.ptr, v8.kNormal, @intCast(GlobalScopeApi.Meta.name.len)); + v8.v8__FunctionTemplate__SetClassName(js_global, class_name); + v8.v8__FunctionTemplate__Inherit(js_global, templates[global_scope_index]); + + const global_template = v8.v8__FunctionTemplate__InstanceTemplate(js_global).?; + v8.v8__ObjectTemplate__SetInternalFieldCount(global_template, comptime countInternalFields(GlobalScopeApi)); + + // Set up named/indexed handlers for Window's global object (for named element access like window.myDiv) + if (comptime std.mem.eql(u8, GlobalScopeApi.Meta.name, "Window")) { + v8.v8__ObjectTemplate__SetNamedHandler(global_template, &.{ + .getter = bridge.unknownWindowPropertyCallback, + .setter = null, + .query = null, + .deleter = null, + .enumerator = null, + .definer = null, + .descriptor = null, + .data = null, + .flags = v8.kOnlyInterceptStrings | v8.kNonMasking, + }); + v8.v8__ObjectTemplate__SetIndexedHandler(global_template, &.{ + .getter = @import("../webapi/Window.zig").JsApi.index.getter, + .setter = null, + .query = null, + .deleter = null, + .enumerator = null, + .definer = null, + .descriptor = null, + .data = null, + .flags = 0, + }); + } + + const context = v8.v8__Context__New(isolate, global_template, null); + v8.v8__Context__Enter(context); + defer v8.v8__Context__Exit(context); + + // Initialize embedder data to null so callbacks can detect snapshot creation + v8.v8__Context__SetAlignedPointerInEmbedderData(context, 1, null); + + const global_obj = v8.v8__Context__Global(context); + + // Attach constructors for this context's APIs to the global + inline for (ContextApis) |JsApi| { + const template_index = comptime bridge.JsApiLookup.getId(JsApi); + const func = v8.v8__FunctionTemplate__GetFunction(templates[template_index], context); + if (@hasDecl(JsApi.Meta, "name")) { + if (@hasDecl(JsApi.Meta, "constructor_alias")) { + const alias = JsApi.Meta.constructor_alias; + const v8_class_name = v8.v8__String__NewFromUtf8(isolate, alias.ptr, v8.kNormal, @intCast(alias.len)); + var maybe_result: v8.MaybeBool = undefined; + v8.v8__Object__Set(global_obj, context, v8_class_name, func, &maybe_result); + + const name = JsApi.Meta.name; + const illegal_class_name = v8.v8__String__NewFromUtf8(isolate, name.ptr, v8.kNormal, @intCast(name.len)); + var maybe_result2: v8.MaybeBool = undefined; + v8.v8__Object__DefineOwnProperty(global_obj, context, illegal_class_name, func, 0, &maybe_result2); + } else { + const name = JsApi.Meta.name; + const v8_class_name = v8.v8__String__NewFromUtf8(isolate, name.ptr, v8.kNormal, @intCast(name.len)); + var maybe_result: v8.MaybeBool = undefined; + var properties: v8.PropertyAttribute = v8.None; + if (@hasDecl(JsApi.Meta, "enumerable") and JsApi.Meta.enumerable == false) { + properties |= v8.DontEnum; + } + v8.v8__Object__DefineOwnProperty(global_obj, context, v8_class_name, func, properties, &maybe_result); + } + } + } + + { + // Delete built-in console so we can inject our own + const console_key = v8.v8__String__NewFromUtf8(isolate, "console", v8.kNormal, 7); + var maybe_deleted: v8.MaybeBool = undefined; + v8.v8__Object__Delete(global_obj, context, console_key, &maybe_deleted); + if (maybe_deleted.value == false) { + return error.ConsoleDeleteError; + } + } + + // Set prototype chains on function objects + // https://groups.google.com/g/v8-users/c/qAQQBmbi--8 + inline for (JsApis, 0..) |JsApi, i| { + if (comptime protoIndexLookup(JsApi)) |proto_index| { + const proto_func = v8.v8__FunctionTemplate__GetFunction(templates[proto_index], context); + const proto_obj: *const v8.Object = @ptrCast(proto_func); + + const self_func = v8.v8__FunctionTemplate__GetFunction(templates[i], context); + const self_obj: *const v8.Object = @ptrCast(self_func); + + var maybe_result: v8.MaybeBool = undefined; + v8.v8__Object__SetPrototype(self_obj, context, proto_obj, &maybe_result); + } + } + + { + // DOMException prototype setup + const code_str = "DOMException.prototype.__proto__ = Error.prototype"; + const code = v8.v8__String__NewFromUtf8(isolate, code_str.ptr, v8.kNormal, @intCast(code_str.len)); + const script = v8.v8__Script__Compile(context, code, null) orelse return error.ScriptCompileFailed; + _ = v8.v8__Script__Run(script, context) orelse return error.ScriptRunFailed; + } + + return v8.v8__SnapshotCreator__AddContext(snapshot_creator, context); +} + fn countExternalReferences() comptime_int { @setEvalBranchQuota(100_000); @@ -260,6 +331,9 @@ fn countExternalReferences() comptime_int { // +1 for the noop function shared by various types count += 1; + // +1 for unknownWindowPropertyCallback used on Window's global template + count += 1; + inline for (JsApis) |JsApi| { if (@hasDecl(JsApi, "constructor")) { count += 1; @@ -316,6 +390,9 @@ fn collectExternalReferences() [countExternalReferences()]isize { references[idx] = @bitCast(@intFromPtr(&bridge.Function.noopFunction)); idx += 1; + references[idx] = @bitCast(@intFromPtr(&bridge.unknownWindowPropertyCallback)); + idx += 1; + inline for (JsApis) |JsApi| { if (@hasDecl(JsApi, "constructor")) { references[idx] = @bitCast(@intFromPtr(JsApi.constructor.func)); @@ -382,7 +459,7 @@ fn protoIndexLookup(comptime JsApi: type) ?u16 { return protoIndexLookupFor(&JsApis, JsApi); } -pub fn countInternalFields(comptime JsApi: type) u8 { +fn countInternalFields(comptime JsApi: type) u8 { var last_used_id = 0; var cache_count: u8 = 0; @@ -467,35 +544,6 @@ fn protoIndexLookupFor(comptime ApiList: []const type, comptime JsApi: type) ?u1 } } -// Validates that every type in the API list has its full prototype chain -// contained within that same list. This catches errors where a type is added -// to a snapshot but its prototype dependencies are missing. -// See bridge.AllJsApis for more information. -fn validatePrototypeChains(comptime ApiList: []const type) void { - @setEvalBranchQuota(100_000); - inline for (ApiList) |JsApi| { - const T = JsApi.bridge.type; - if (@hasField(T, "_proto")) { - const Ptr = std.meta.fieldInfo(T, ._proto).type; - const ProtoType = @typeInfo(Ptr).pointer.child; - // Verify the prototype's JsApi is in our list - var found = false; - inline for (ApiList) |Api| { - if (Api == ProtoType.JsApi) { - found = true; - break; - } - } - if (!found) { - @compileError( - @typeName(JsApi) ++ " has prototype " ++ - @typeName(ProtoType.JsApi) ++ " which is not in the API list", - ); - } - } - } -} - // Generate a constructor template for a JsApi type (public for reuse) pub fn generateConstructor(comptime JsApi: type, isolate: *v8.Isolate) *const v8.FunctionTemplate { const callback = blk: { diff --git a/src/browser/js/bridge.zig b/src/browser/js/bridge.zig index dc5e6910..625ead23 100644 --- a/src/browser/js/bridge.zig +++ b/src/browser/js/bridge.zig @@ -24,6 +24,7 @@ const Session = @import("../Session.zig"); const v8 = js.v8; const Caller = @import("Caller.zig"); +const Context = @import("Context.zig"); const IS_DEBUG = @import("builtin").mode == .Debug; @@ -386,6 +387,11 @@ pub const Property = struct { pub fn unknownWindowPropertyCallback(c_name: ?*const v8.Name, handle: ?*const v8.PropertyCallbackInfo) callconv(.c) u8 { const v8_isolate = v8.v8__PropertyCallbackInfo__GetIsolate(handle).?; + + // During snapshot creation, there's no Context in embedder data yet + const v8_context = v8.v8__Isolate__GetCurrentContext(v8_isolate) orelse return 0; + if (v8.v8__Context__GetAlignedPointerFromEmbedderData(v8_context, 1) == null) return 0; + var caller: Caller = undefined; caller.init(v8_isolate); defer caller.deinit(); @@ -679,7 +685,7 @@ pub const SubType = enum { webassemblymemory, }; -/// APIs for Page/Window contexts. Used by Snapshot.zig for Page snapshot creation. +// APIs for Page/Window contexts. Used by Snapshot.zig for Page snapshot creation. pub const PageJsApis = flattenTypes(&.{ @import("../webapi/AbortController.zig"), @import("../webapi/AbortSignal.zig"), @@ -874,13 +880,32 @@ pub const PageJsApis = flattenTypes(&.{ @import("../webapi/ImageData.zig"), }); -/// APIs that exist only in Worker contexts (not in Page/Window). -const WorkerOnlyApis = flattenTypes(&.{ +// APIs available on Worker context globals (constructors like URL, Headers, etc.) +// This is a subset of PageJsApis plus WorkerGlobalScope. +// TODO: Expand this list to include all worker-appropriate APIs. +pub const WorkerJsApis = flattenTypes(&.{ @import("../webapi/WorkerGlobalScope.zig"), + @import("../webapi/EventTarget.zig"), + @import("../webapi/DOMException.zig"), + @import("../webapi/AbortController.zig"), + @import("../webapi/AbortSignal.zig"), + @import("../webapi/URL.zig"), + @import("../webapi/net/URLSearchParams.zig"), + @import("../webapi/net/Headers.zig"), + @import("../webapi/net/Request.zig"), + @import("../webapi/net/Response.zig"), + @import("../webapi/encoding/TextEncoder.zig"), + @import("../webapi/encoding/TextDecoder.zig"), + @import("../webapi/Blob.zig"), + @import("../webapi/File.zig"), + @import("../webapi/net/FormData.zig"), + @import("../webapi/Console.zig"), + @import("../webapi/Crypto.zig"), + @import("../webapi/Performance.zig"), }); -/// Master list of ALL JS APIs across all contexts. -/// Used by Env (class IDs, templates), JsApiLookup, and anywhere that needs -/// to know about all possible types. Individual snapshots use their own -/// subsets (PageJsApis, WorkerSnapshot.JsApis). -pub const JsApis = PageJsApis ++ WorkerOnlyApis; +// Master list of ALL JS APIs across all contexts. +// Used by Env (class IDs, templates), JsApiLookup, and anywhere that needs +// to know about all possible types. Individual snapshots use their own +// subsets (PageJsApis, WorkerSnapshot.JsApis). +pub const JsApis = PageJsApis ++ [_]type{@import("../webapi/WorkerGlobalScope.zig").JsApi}; diff --git a/src/browser/webapi/Location.zig b/src/browser/webapi/Location.zig index cb8806ff..caf1c4ae 100644 --- a/src/browser/webapi/Location.zig +++ b/src/browser/webapi/Location.zig @@ -53,12 +53,12 @@ pub fn getPort(self: *const Location) []const u8 { return self._url.getPort(); } -pub fn getOrigin(self: *const Location, page: *const Page) ![]const u8 { - return self._url.getOrigin(&page.js.execution); +pub fn getOrigin(self: *const Location, exec: *const js.Execution) ![]const u8 { + return self._url.getOrigin(exec); } -pub fn getSearch(self: *const Location, page: *const Page) ![]const u8 { - return self._url.getSearch(&page.js.execution); +pub fn getSearch(self: *const Location, exec: *const js.Execution) ![]const u8 { + return self._url.getSearch(exec); } pub fn getHash(self: *const Location) []const u8 { @@ -98,8 +98,8 @@ pub fn reload(_: *const Location, page: *Page) !void { return page.scheduleNavigation(page.url, .{ .reason = .script, .kind = .reload }, .{ .script = page }); } -pub fn toString(self: *const Location, page: *const Page) ![:0]const u8 { - return self._url.toString(&page.js.execution); +pub fn toString(self: *const Location, exec: *const js.Execution) ![:0]const u8 { + return self._url.toString(exec); } pub const JsApi = struct { diff --git a/src/browser/webapi/Window.zig b/src/browser/webapi/Window.zig index 5feabb44..d359af3e 100644 --- a/src/browser/webapi/Window.zig +++ b/src/browser/webapi/Window.zig @@ -411,7 +411,7 @@ pub fn postMessage(self: *Window, message: js.Value.Temp, target_origin: ?[]cons errdefer target_page.releaseArena(arena); // Origin should be the source window's origin (where the message came from) - const origin = try source_window._location.getOrigin(page); + const origin = try source_window._location.getOrigin(&page.js.execution); const callback = try arena.create(PostMessageCallback); callback.* = .{ .arena = arena, diff --git a/src/browser/webapi/WorkerGlobalScope.zig b/src/browser/webapi/WorkerGlobalScope.zig index 6ea9bd90..604176e4 100644 --- a/src/browser/webapi/WorkerGlobalScope.zig +++ b/src/browser/webapi/WorkerGlobalScope.zig @@ -19,7 +19,6 @@ const std = @import("std"); const JS = @import("../js/js.zig"); -const base64 = @import("encoding/base64.zig"); const Console = @import("Console.zig"); const Crypto = @import("Crypto.zig"); const EventTarget = @import("EventTarget.zig"); @@ -97,10 +96,12 @@ pub fn setOnUnhandledRejection(self: *WorkerGlobalScope, setter: ?FunctionSetter } pub fn btoa(_: *const WorkerGlobalScope, input: []const u8, exec: *JS.Execution) ![]const u8 { + const base64 = @import("encoding/base64.zig"); return base64.encode(exec.call_arena, input); } pub fn atob(_: *const WorkerGlobalScope, input: []const u8, exec: *JS.Execution) ![]const u8 { + const base64 = @import("encoding/base64.zig"); return base64.decode(exec.call_arena, input); }