Create Worker context in snapshot

Turns out you can embed multiple contexts within a snapshot. So our snapshot
now contains the Page context (as before) but also the Worker context. This
gives us the performance benefit of snapshots and makes context creation for
pages and workers much more similar.
This commit is contained in:
Karl Seguin
2026-04-03 19:47:07 +08:00
parent 4dd014de41
commit 3864aa4a6b
11 changed files with 329 additions and 292 deletions

View File

@@ -5,8 +5,8 @@
.minimum_zig_version = "0.15.2", .minimum_zig_version = "0.15.2",
.dependencies = .{ .dependencies = .{
.v8 = .{ .v8 = .{
.url = "https://github.com/lightpanda-io/zig-v8-fork/archive/refs/tags/v0.3.7.tar.gz", .url = "https://github.com/lightpanda-io/zig-v8-fork/archive/715ccbae21d7528eba951f78af4dfd48835fc172.tar.gz",
.hash = "v8-0.0.0-xddH67uBBAD95hWsPQz3Ni1PlZjdywtPXrGUAp8rSKco", .hash = "v8-0.0.0-xddH65-HBADXFCII9ucZE3NgbkWmwsbTbsx8qevYVki5",
}, },
// .v8 = .{ .path = "../zig-v8-fork" }, // .v8 = .{ .path = "../zig-v8-fork" },
.brotli = .{ .brotli = .{

View File

@@ -55,15 +55,9 @@ fn initWithContext(self: *Caller, ctx: *Context, v8_context: *const v8.Context)
.isolate = ctx.isolate, .isolate = ctx.isolate,
}, },
.prev_local = ctx.local, .prev_local = ctx.local,
.prev_context = switch (ctx.global) { .prev_context = ctx.global.getJs(),
.page => |page| page.js,
.worker => |worker| worker.js,
},
}; };
switch (ctx.global) { ctx.global.setJs(ctx);
.page => |page| page.js = ctx,
.worker => |worker| worker.js = ctx,
}
ctx.local = &self.local; ctx.local = &self.local;
} }
@@ -94,10 +88,7 @@ pub fn deinit(self: *Caller) void {
ctx.call_depth = call_depth; ctx.call_depth = call_depth;
ctx.local = self.prev_local; ctx.local = self.prev_local;
switch (ctx.global) { ctx.global.setJs(self.prev_context);
.page => |page| page.js = self.prev_context,
.worker => |worker| worker.js = self.prev_context,
}
} }
pub const CallOpts = struct { pub const CallOpts = struct {
@@ -444,10 +435,6 @@ fn isPage(comptime T: type) bool {
return T == *Page or T == *const Page; 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 { fn isExecution(comptime T: type) bool {
return T == *js.Execution or T == *const js.Execution; 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)) { if (comptime isPage(T)) {
return switch (ctx.global) { return switch (ctx.global) {
.page => |page| page, .page => |page| page,
.worker => { .worker => unreachable,
if (comptime IS_DEBUG) std.debug.assert(false);
unreachable;
},
}; };
} }
if (comptime isWorker(T)) { if (comptime isExecution(T)) {
return switch (ctx.global) { return &ctx.execution;
.page => {
if (comptime IS_DEBUG) std.debug.assert(false);
unreachable;
},
.worker => |worker| worker,
};
} }
@compileError("Unsupported global arg type: " ++ @typeName(T)); @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 // from our params slice, because we don't want to bind it to
// a JS argument // a JS argument
const LastParamType = params[params.len - 1].type.?; 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); @field(args, tupleFieldName(params.len - 1 + offset)) = getGlobalArg(LastParamType, local.ctx);
break :blk params[0 .. params.len - 1]; 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 // we have neither a Page, Execution, nor a JsObject. All params must be
// bound to a JavaScript value. // bound to a JavaScript value.
break :blk params; break :blk params;

View File

@@ -45,6 +45,27 @@ const Context = @This();
pub const GlobalScope = union(enum) { pub const GlobalScope = union(enum) {
page: *Page, page: *Page,
worker: *WorkerGlobalScope, 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, id: usize,
@@ -547,10 +568,7 @@ pub fn dynamicModuleCallback(
if (resource_value.isNullOrUndefined()) { if (resource_value.isNullOrUndefined()) {
// will only be null / undefined in extreme cases (e.g. WPT tests) // will only be null / undefined in extreme cases (e.g. WPT tests)
// where you're // where you're
break :blk switch (self.global) { break :blk self.global.base();
.page => |page| page.base(),
.worker => |worker| worker.base(),
};
} }
break :blk js.String.toSliceZ(.{ .local = &local, .handle = resource_name.? }) catch |err| { 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; const isolate = self.isolate;
js.HandleScope.init(hs, isolate); js.HandleScope.init(hs, isolate);
const original = switch (self.global) { const original = self.global.getJs();
.page => |page| blk: { self.global.setJs(self);
const orig = page.js;
page.js = self;
break :blk orig;
},
.worker => |worker| blk: {
const orig = worker.js;
worker.js = self;
break :blk orig;
},
};
const handle: *const v8.Context = @ptrCast(v8.v8__Global__Get(&self.handle, isolate.handle)); const handle: *const v8.Context = @ptrCast(v8.v8__Global__Get(&self.handle, isolate.handle));
v8.v8__Context__Enter(handle); v8.v8__Context__Enter(handle);
@@ -920,10 +928,7 @@ const Entered = struct {
global: GlobalScope, global: GlobalScope,
pub fn exit(self: Entered) void { pub fn exit(self: Entered) void {
switch (self.global) { self.global.setJs(self.original);
.page => |page| page.js = self.original,
.worker => |worker| worker.js = self.original,
}
v8.v8__Context__Exit(self.handle); v8.v8__Context__Exit(self.handle);
self.handle_scope.deinit(); self.handle_scope.deinit();
} }
@@ -934,12 +939,7 @@ pub fn queueMutationDelivery(self: *Context) !void {
fn run(ctx: *Context) void { fn run(ctx: *Context) void {
switch (ctx.global) { switch (ctx.global) {
.page => |page| page.deliverMutations(), .page => |page| page.deliverMutations(),
.worker => { .worker => unreachable,
if (comptime IS_DEBUG) {
std.debug.assert(false);
}
unreachable;
},
} }
} }
}.run); }.run);
@@ -950,12 +950,7 @@ pub fn queueIntersectionChecks(self: *Context) !void {
fn run(ctx: *Context) void { fn run(ctx: *Context) void {
switch (ctx.global) { switch (ctx.global) {
.page => |page| page.performScheduledIntersectionChecks(), .page => |page| page.performScheduledIntersectionChecks(),
.worker => { .worker => unreachable,
if (comptime IS_DEBUG) {
std.debug.assert(false);
}
unreachable;
},
} }
} }
}.run); }.run);
@@ -966,12 +961,7 @@ pub fn queueIntersectionDelivery(self: *Context) !void {
fn run(ctx: *Context) void { fn run(ctx: *Context) void {
switch (ctx.global) { switch (ctx.global) {
.page => |page| page.deliverIntersections(), .page => |page| page.deliverIntersections(),
.worker => { .worker => unreachable,
if (comptime IS_DEBUG) {
std.debug.assert(false);
}
unreachable;
},
} }
} }
}.run); }.run);
@@ -982,12 +972,7 @@ pub fn queueSlotchangeDelivery(self: *Context) !void {
fn run(ctx: *Context) void { fn run(ctx: *Context) void {
switch (ctx.global) { switch (ctx.global) {
.page => |page| page.deliverSlotchangeEvents(), .page => |page| page.deliverSlotchangeEvents(),
.worker => { .worker => unreachable,
if (comptime IS_DEBUG) {
std.debug.assert(false);
}
unreachable;
},
} }
} }
}.run); }.run);

View File

@@ -34,6 +34,7 @@ const Inspector = @import("Inspector.zig");
const Page = @import("../Page.zig"); const Page = @import("../Page.zig");
const Window = @import("../webapi/Window.zig"); const Window = @import("../webapi/Window.zig");
const WorkerGlobalScope = @import("../webapi/WorkerGlobalScope.zig");
const JsApis = bridge.JsApis; const JsApis = bridge.JsApis;
const Allocator = std.mem.Allocator; 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 // Dynamic slice to avoid circular dependency on JsApis.len at comptime
templates: []*const v8.FunctionTemplate, 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 associated with the Isolate. Exists when CDP is being used.
inspector: ?*Inspector, inspector: ?*Inspector,
@@ -146,7 +144,6 @@ pub fn init(app: *App, opts: InitOpts) !Env {
const templates = try allocator.alloc(*const v8.FunctionTemplate, JsApis.len); const templates = try allocator.alloc(*const v8.FunctionTemplate, JsApis.len);
errdefer allocator.free(templates); errdefer allocator.free(templates);
var global_eternal: v8.Eternal = undefined;
var private_symbols: PrivateSymbols = undefined; var private_symbols: PrivateSymbols = undefined;
{ {
var temp_scope: js.HandleScope = 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.?)); 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); private_symbols = PrivateSymbols.init(isolate_handle);
} }
@@ -221,7 +180,6 @@ pub fn init(app: *App, opts: InitOpts) !Env {
.templates = templates, .templates = templates,
.isolate_params = params, .isolate_params = params,
.inspector = inspector, .inspector = inspector,
.global_template = global_eternal,
.private_symbols = private_symbols, .private_symbols = private_symbols,
.microtask_queues_are_running = false, .microtask_queues_are_running = false,
.eternal_function_templates = eternal_function_templates, .eternal_function_templates = eternal_function_templates,
@@ -261,6 +219,17 @@ pub const ContextParams = struct {
}; };
pub fn createContext(self: *Env, page: *Page, params: ContextParams) !*Context { 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 }); const context_arena = try self.app.arena_pool.acquire(.{ .debug = params.debug_name });
errdefer self.app.arena_pool.release(context_arena); 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).?; const microtask_queue = v8.v8__MicrotaskQueue__New(isolate.handle, v8.kExplicit).?;
errdefer v8.v8__MicrotaskQueue__DELETE(microtask_queue); errdefer v8.v8__MicrotaskQueue__DELETE(microtask_queue);
// Get the global template that was created once per isolate // Restore the context from the snapshot (0 = Page, 1 = Worker)
const global_template: *const v8.ObjectTemplate = @ptrCast(@alignCast(v8.v8__Eternal__Get(&self.global_template, isolate.handle).?)); const snapshot_index: u32 = if (comptime is_page) 0 else 1;
v8.v8__ObjectTemplate__SetInternalFieldCount(global_template, comptime Snapshot.countInternalFields(Window.JsApi)); const v8_context = v8.v8__Context__FromSnapshot__Config(isolate.handle, snapshot_index, &.{
.global_template = null,
const v8_context = v8.v8__Context__New__Config(isolate.handle, &.{
.global_template = global_template,
.global_object = null, .global_object = null,
.microtask_queue = microtask_queue, .microtask_queue = microtask_queue,
}).?; }).?;
@@ -287,36 +254,36 @@ pub fn createContext(self: *Env, page: *Page, params: ContextParams) !*Context {
var context_global: v8.Global = undefined; var context_global: v8.Global = undefined;
v8.v8__Global__New(isolate.handle, v8_context, &context_global); 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).?; const global_obj = v8.v8__Context__Global(v8_context).?;
{
// Store our TAO inside the internal field of the global object. This // Store our TAO inside the internal field of the global object. This
// maps the v8::Object -> Zig instance. Almost all objects have this, and // maps the v8::Object -> Zig instance.
// 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")); const tao = try params.identity_arena.create(@import("TaggedOpaque.zig"));
tao.* = .{ tao.* = if (comptime is_page) .{
.value = @ptrCast(page.window), .value = @ptrCast(global.window),
.prototype_chain = (&Window.JsApi.Meta.prototype_chain).ptr, .prototype_chain = (&Window.JsApi.Meta.prototype_chain).ptr,
.prototype_len = @intCast(Window.JsApi.Meta.prototype_chain.len), .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 .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); v8.v8__Object__SetAlignedPointerInInternalField(global_obj, 0, tao);
}
const context_id = self.context_id; const context_id = self.context_id;
self.context_id = context_id + 1; self.context_id = context_id + 1;
const session = page._session; const session = global._session;
const origin = try session.getOrCreateOrigin(null); const origin = try session.getOrCreateOrigin(null);
errdefer session.releaseOrigin(origin); errdefer session.releaseOrigin(origin);
const context = try context_arena.create(Context); const context = try context_arena.create(Context);
context.* = .{ context.* = .{
.env = self, .env = self,
.global = .{ .page = page }, .global = if (comptime is_page) .{ .page = global } else .{ .worker = global },
.origin = origin, .origin = origin,
.id = context_id, .id = context_id,
.session = session, .session = session,
@@ -326,7 +293,7 @@ pub fn createContext(self: *Env, page: *Page, params: ContextParams) !*Context {
.templates = self.templates, .templates = self.templates,
.call_arena = params.call_arena, .call_arena = params.call_arena,
.microtask_queue = microtask_queue, .microtask_queue = microtask_queue,
.script_manager = &page._script_manager, .script_manager = if (comptime is_page) &global._script_manager else null,
.scheduler = .init(context_arena), .scheduler = .init(context_arena),
.identity = params.identity, .identity = params.identity,
.identity_arena = params.identity_arena, .identity_arena = params.identity_arena,
@@ -334,26 +301,24 @@ pub fn createContext(self: *Env, page: *Page, params: ContextParams) !*Context {
}; };
context.execution = .{ context.execution = .{
.buf = &page.buf, .url = &global.url,
.buf = &global.buf,
.context = context, .context = context,
.arena = page.arena, .arena = global.arena,
.call_arena = params.call_arena, .call_arena = params.call_arena,
._factory = page._factory, ._factory = global._factory,
._scheduler = &context.scheduler, ._scheduler = &context.scheduler,
.url = &page.url,
}; };
{ // Register in the identity map. Multiple contexts can be created for the
// Multiple contexts can be created for the same Window (via CDP). We only // same global (via CDP), so we only register the first one.
// need to 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, @intFromPtr(page.window)); const gop = try params.identity.identity_map.getOrPut(params.identity_arena, identity_ptr);
if (gop.found_existing == false) { if (gop.found_existing == false) {
// our window wrapped in a v8::Global
var global_global: v8.Global = undefined; var global_global: v8.Global = undefined;
v8.v8__Global__New(isolate.handle, global_obj, &global_global); v8.v8__Global__New(isolate.handle, global_obj, &global_global);
gop.value_ptr.* = global_global; gop.value_ptr.* = global_global;
} }
}
// Store a pointer to our context inside the v8 context so that, given // Store a pointer to our context inside the v8 context so that, given
// a v8 context, we can get our context out // a v8 context, we can get our context out
@@ -583,3 +548,50 @@ const PrivateSymbols = struct {
self.child_nodes.deinit(); 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());
}

View File

@@ -37,11 +37,11 @@ const Execution = @This();
context: *Context, context: *Context,
// Fields named to match Page for generic code (executor._factory works for both) // Fields named to match Page for generic code (executor._factory works for both)
_factory: *Factory, buf: []u8,
arena: Allocator, arena: Allocator,
call_arena: Allocator, call_arena: Allocator,
_factory: *Factory,
_scheduler: *Scheduler, _scheduler: *Scheduler,
buf: []u8,
// Pointer to the url field (Page or WorkerGlobalScope) - allows access to current url even after navigation // Pointer to the url field (Page or WorkerGlobalScope) - allows access to current url even after navigation
url: *[:0]const u8, url: *[:0]const u8,

View File

@@ -337,9 +337,6 @@ pub fn zigValueToJs(self: *const Local, value: anytype, comptime opts: CallOpts)
.worker => { .worker => {
// No Worker-related API currently uses this, so haven't // No Worker-related API currently uses this, so haven't
// added support for it // added support for it
if (comptime IS_DEBUG) {
std.debug.assert(false);
}
unreachable; unreachable;
}, },
}; };
@@ -425,9 +422,6 @@ pub fn zigValueToJs(self: *const Local, value: anytype, comptime opts: CallOpts)
.worker => { .worker => {
// No Worker-related API currently uses this, so haven't // No Worker-related API currently uses this, so haven't
// added support for it // added support for it
if (comptime IS_DEBUG) {
std.debug.assert(false);
}
unreachable; unreachable;
}, },
}; };

View File

@@ -17,6 +17,7 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std"); const std = @import("std");
const lp = @import("lightpanda");
const js = @import("js.zig"); const js = @import("js.zig");
const bridge = @import("bridge.zig"); const bridge = @import("bridge.zig");
const log = @import("../../log.zig"); const log = @import("../../log.zig");
@@ -26,6 +27,7 @@ const IS_DEBUG = @import("builtin").mode == .Debug;
const v8 = js.v8; const v8 = js.v8;
const JsApis = bridge.JsApis; const JsApis = bridge.JsApis;
const PageJsApis = bridge.PageJsApis; const PageJsApis = bridge.PageJsApis;
const WorkerJsApis = bridge.WorkerJsApis;
const Snapshot = @This(); const Snapshot = @This();
@@ -114,8 +116,6 @@ fn isValid(self: Snapshot) bool {
} }
pub fn create() !Snapshot { pub fn create() !Snapshot {
comptime validatePrototypeChains(&JsApis);
var external_references = collectExternalReferences(); var external_references = collectExternalReferences();
var params: v8.CreateParams = undefined; var params: v8.CreateParams = undefined;
@@ -153,11 +153,13 @@ pub fn create() !Snapshot {
} }
} }
const context = v8.v8__Context__New(isolate, null, null); // Add ALL templates to snapshot (done once, in any context)
v8.v8__Context__Enter(context); // We need a context to call AddData, so create a temporary one
defer v8.v8__Context__Exit(context); {
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; var last_data_index: usize = 0;
inline for (JsApis, 0..) |_, i| { inline for (JsApis, 0..) |_, i| {
@setEvalBranchQuota(10_000); @setEvalBranchQuota(10_000);
@@ -173,12 +175,91 @@ pub fn create() !Snapshot {
} }
} }
// 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);
}
{
const Window = @import("../webapi/Window.zig");
const index = try createSnapshotContext(&PageJsApis, Window.JsApi, isolate, snapshot_creator.?, &templates);
std.debug.assert(index == 0);
}
{
const WorkerGlobalScope = @import("../webapi/WorkerGlobalScope.zig");
const index = try createSnapshotContext(&WorkerJsApis, WorkerGlobalScope.JsApi, isolate, snapshot_creator.?, &templates);
std.debug.assert(index == 1);
}
}
const blob = v8.v8__SnapshotCreator__createBlob(snapshot_creator, v8.kKeep);
return .{
.owns_data = true,
.data_start = data_start,
.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); const global_obj = v8.v8__Context__Global(context);
// Attach only PAGE types to the default context's global // Attach constructors for this context's APIs to the global
inline for (PageJsApis, 0..) |JsApi, i| { inline for (ContextApis) |JsApi| {
// PageJsApis[i] == JsApis[i] because the PageJsApis are position at the start of the list const template_index = comptime bridge.JsApiLookup.getId(JsApi);
const func = v8.v8__FunctionTemplate__GetFunction(templates[i], context); const func = v8.v8__FunctionTemplate__GetFunction(templates[template_index], context);
if (@hasDecl(JsApi.Meta, "name")) { if (@hasDecl(JsApi.Meta, "name")) {
if (@hasDecl(JsApi.Meta, "constructor_alias")) { if (@hasDecl(JsApi.Meta, "constructor_alias")) {
const alias = JsApi.Meta.constructor_alias; const alias = JsApi.Meta.constructor_alias;
@@ -236,17 +317,7 @@ pub fn create() !Snapshot {
_ = v8.v8__Script__Run(script, context) orelse return error.ScriptRunFailed; _ = v8.v8__Script__Run(script, context) orelse return error.ScriptRunFailed;
} }
v8.v8__SnapshotCreator__setDefaultContext(snapshot_creator, context); return v8.v8__SnapshotCreator__AddContext(snapshot_creator, context);
}
const blob = v8.v8__SnapshotCreator__createBlob(snapshot_creator, v8.kKeep);
return .{
.owns_data = true,
.data_start = data_start,
.external_references = external_references,
.startup_data = blob,
};
} }
fn countExternalReferences() comptime_int { fn countExternalReferences() comptime_int {
@@ -260,6 +331,9 @@ fn countExternalReferences() comptime_int {
// +1 for the noop function shared by various types // +1 for the noop function shared by various types
count += 1; count += 1;
// +1 for unknownWindowPropertyCallback used on Window's global template
count += 1;
inline for (JsApis) |JsApi| { inline for (JsApis) |JsApi| {
if (@hasDecl(JsApi, "constructor")) { if (@hasDecl(JsApi, "constructor")) {
count += 1; count += 1;
@@ -316,6 +390,9 @@ fn collectExternalReferences() [countExternalReferences()]isize {
references[idx] = @bitCast(@intFromPtr(&bridge.Function.noopFunction)); references[idx] = @bitCast(@intFromPtr(&bridge.Function.noopFunction));
idx += 1; idx += 1;
references[idx] = @bitCast(@intFromPtr(&bridge.unknownWindowPropertyCallback));
idx += 1;
inline for (JsApis) |JsApi| { inline for (JsApis) |JsApi| {
if (@hasDecl(JsApi, "constructor")) { if (@hasDecl(JsApi, "constructor")) {
references[idx] = @bitCast(@intFromPtr(JsApi.constructor.func)); references[idx] = @bitCast(@intFromPtr(JsApi.constructor.func));
@@ -382,7 +459,7 @@ fn protoIndexLookup(comptime JsApi: type) ?u16 {
return protoIndexLookupFor(&JsApis, JsApi); return protoIndexLookupFor(&JsApis, JsApi);
} }
pub fn countInternalFields(comptime JsApi: type) u8 { fn countInternalFields(comptime JsApi: type) u8 {
var last_used_id = 0; var last_used_id = 0;
var cache_count: u8 = 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) // Generate a constructor template for a JsApi type (public for reuse)
pub fn generateConstructor(comptime JsApi: type, isolate: *v8.Isolate) *const v8.FunctionTemplate { pub fn generateConstructor(comptime JsApi: type, isolate: *v8.Isolate) *const v8.FunctionTemplate {
const callback = blk: { const callback = blk: {

View File

@@ -24,6 +24,7 @@ const Session = @import("../Session.zig");
const v8 = js.v8; const v8 = js.v8;
const Caller = @import("Caller.zig"); const Caller = @import("Caller.zig");
const Context = @import("Context.zig");
const IS_DEBUG = @import("builtin").mode == .Debug; 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 { pub fn unknownWindowPropertyCallback(c_name: ?*const v8.Name, handle: ?*const v8.PropertyCallbackInfo) callconv(.c) u8 {
const v8_isolate = v8.v8__PropertyCallbackInfo__GetIsolate(handle).?; 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; var caller: Caller = undefined;
caller.init(v8_isolate); caller.init(v8_isolate);
defer caller.deinit(); defer caller.deinit();
@@ -679,7 +685,7 @@ pub const SubType = enum {
webassemblymemory, 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(&.{ pub const PageJsApis = flattenTypes(&.{
@import("../webapi/AbortController.zig"), @import("../webapi/AbortController.zig"),
@import("../webapi/AbortSignal.zig"), @import("../webapi/AbortSignal.zig"),
@@ -874,13 +880,32 @@ pub const PageJsApis = flattenTypes(&.{
@import("../webapi/ImageData.zig"), @import("../webapi/ImageData.zig"),
}); });
/// APIs that exist only in Worker contexts (not in Page/Window). // APIs available on Worker context globals (constructors like URL, Headers, etc.)
const WorkerOnlyApis = flattenTypes(&.{ // 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/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. // Master list of ALL JS APIs across all contexts.
/// Used by Env (class IDs, templates), JsApiLookup, and anywhere that needs // Used by Env (class IDs, templates), JsApiLookup, and anywhere that needs
/// to know about all possible types. Individual snapshots use their own // to know about all possible types. Individual snapshots use their own
/// subsets (PageJsApis, WorkerSnapshot.JsApis). // subsets (PageJsApis, WorkerSnapshot.JsApis).
pub const JsApis = PageJsApis ++ WorkerOnlyApis; pub const JsApis = PageJsApis ++ [_]type{@import("../webapi/WorkerGlobalScope.zig").JsApi};

View File

@@ -53,12 +53,12 @@ pub fn getPort(self: *const Location) []const u8 {
return self._url.getPort(); return self._url.getPort();
} }
pub fn getOrigin(self: *const Location, page: *const Page) ![]const u8 { pub fn getOrigin(self: *const Location, exec: *const js.Execution) ![]const u8 {
return self._url.getOrigin(&page.js.execution); return self._url.getOrigin(exec);
} }
pub fn getSearch(self: *const Location, page: *const Page) ![]const u8 { pub fn getSearch(self: *const Location, exec: *const js.Execution) ![]const u8 {
return self._url.getSearch(&page.js.execution); return self._url.getSearch(exec);
} }
pub fn getHash(self: *const Location) []const u8 { 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 }); return page.scheduleNavigation(page.url, .{ .reason = .script, .kind = .reload }, .{ .script = page });
} }
pub fn toString(self: *const Location, page: *const Page) ![:0]const u8 { pub fn toString(self: *const Location, exec: *const js.Execution) ![:0]const u8 {
return self._url.toString(&page.js.execution); return self._url.toString(exec);
} }
pub const JsApi = struct { pub const JsApi = struct {

View File

@@ -411,7 +411,7 @@ pub fn postMessage(self: *Window, message: js.Value.Temp, target_origin: ?[]cons
errdefer target_page.releaseArena(arena); errdefer target_page.releaseArena(arena);
// Origin should be the source window's origin (where the message came from) // 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); const callback = try arena.create(PostMessageCallback);
callback.* = .{ callback.* = .{
.arena = arena, .arena = arena,

View File

@@ -19,7 +19,6 @@
const std = @import("std"); const std = @import("std");
const JS = @import("../js/js.zig"); const JS = @import("../js/js.zig");
const base64 = @import("encoding/base64.zig");
const Console = @import("Console.zig"); const Console = @import("Console.zig");
const Crypto = @import("Crypto.zig"); const Crypto = @import("Crypto.zig");
const EventTarget = @import("EventTarget.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 { 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); return base64.encode(exec.call_arena, input);
} }
pub fn atob(_: *const WorkerGlobalScope, input: []const u8, exec: *JS.Execution) ![]const u8 { 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); return base64.decode(exec.call_arena, input);
} }