diff --git a/.gitignore b/.gitignore
index c0b52a0d1..dbc57ab93 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,3 +8,4 @@ lightpanda.id
/v8/
/build/
/src/html5ever/target/
+src/snapshot.bin
diff --git a/LICENSING.md b/LICENSING.md
index fc1314840..5a38be6db 100644
--- a/LICENSING.md
+++ b/LICENSING.md
@@ -5,14 +5,6 @@ List](https://spdx.org/licenses/).
The default license for this project is [AGPL-3.0-only](LICENSE).
-## MIT
-
-The following files are licensed under MIT:
-
-```
-src/polyfill/fetch.js
-```
-
The following directories and their subdirectories are licensed under their
original upstream licenses:
diff --git a/build.zig b/build.zig
index 308ec6860..4378ade1a 100644
--- a/build.zig
+++ b/build.zig
@@ -29,10 +29,12 @@ pub fn build(b: *Build) !void {
const git_commit = b.option([]const u8, "git_commit", "Current git commit");
const prebuilt_v8_path = b.option([]const u8, "prebuilt_v8_path", "Path to prebuilt libc_v8.a");
+ const snapshot_path = b.option([]const u8, "snapshot_path", "Path to v8 snapshot");
var opts = b.addOptions();
opts.addOption([]const u8, "version", manifest.version);
opts.addOption([]const u8, "git_commit", git_commit orelse "dev");
+ opts.addOption(?[]const u8, "snapshot_path", snapshot_path);
// Build step to install html5ever dependency.
const html5ever_argv = blk: {
@@ -112,6 +114,30 @@ pub fn build(b: *Build) !void {
run_step.dependOn(&run_cmd.step);
}
+ {
+ // snapshot creator
+ const exe = b.addExecutable(.{
+ .name = "lightpanda-snapshot-creator",
+ .use_llvm = true,
+ .root_module = b.createModule(.{
+ .root_source_file = b.path("src/main_snapshot_creator.zig"),
+ .target = target,
+ .optimize = optimize,
+ .imports = &.{
+ .{ .name = "lightpanda", .module = lightpanda_module },
+ },
+ }),
+ });
+ b.installArtifact(exe);
+
+ const run_cmd = b.addRunArtifact(exe);
+ if (b.args) |args| {
+ run_cmd.addArgs(args);
+ }
+ const run_step = b.step("snapshot_creator", "Generate a v8 snapshot");
+ run_step.dependOn(&run_cmd.step);
+ }
+
{
// test
const tests = b.addTest(.{
diff --git a/build.zig.zon b/build.zig.zon
index 7bef6f541..e2069f79e 100644
--- a/build.zig.zon
+++ b/build.zig.zon
@@ -6,10 +6,10 @@
.minimum_zig_version = "0.15.2",
.dependencies = .{
.v8 = .{
- .url = "https://github.com/lightpanda-io/zig-v8-fork/archive/e047d2a4d5af5783763f0f6a652fab8982a08603.tar.gz",
- .hash = "v8-0.0.0-xddH65gMBACRBQMM7EwmVgfi94FJyyX-0jpe5KhXYhfv",
+ .url = "https://github.com/lightpanda-io/zig-v8-fork/archive/0d64a3d5b36ac94067df3e13fddbf715caa6f391.tar.gz",
+ .hash = "v8-0.0.0-xddH65sfBAC8o3q41YxhOms5uY2fvMzBrsgN8IeCXZgE",
},
- //.v8 = .{ .path = "../zig-v8-fork" }
+ //.v8 = .{ .path = "../zig-v8-fork" },
.@"boringssl-zig" = .{
.url = "git+https://github.com/Syndica/boringssl-zig.git#c53df00d06b02b755ad88bbf4d1202ed9687b096",
.hash = "boringssl-0.1.0-VtJeWehMAAA4RNnwRnzEvKcS9rjsR1QVRw1uJrwXxmVK",
diff --git a/src/App.zig b/src/App.zig
index 24d015c01..f3bb0b07f 100644
--- a/src/App.zig
+++ b/src/App.zig
@@ -22,6 +22,7 @@ const Allocator = std.mem.Allocator;
const log = @import("log.zig");
const Http = @import("http/Http.zig");
+const Snapshot = @import("browser/js/Snapshot.zig");
const Platform = @import("browser/js/Platform.zig");
const Notification = @import("Notification.zig");
@@ -34,6 +35,7 @@ const App = @This();
http: Http,
config: Config,
platform: Platform,
+snapshot: Snapshot,
telemetry: Telemetry,
allocator: Allocator,
app_dir_path: ?[]const u8,
@@ -83,6 +85,9 @@ pub fn init(allocator: Allocator, config: Config) !*App {
app.platform = try Platform.init();
errdefer app.platform.deinit();
+ app.snapshot = try Snapshot.load(allocator);
+ errdefer app.snapshot.deinit(allocator);
+
app.app_dir_path = getAndMakeAppDir(allocator);
app.telemetry = try Telemetry.init(app, config.run_mode);
@@ -101,6 +106,7 @@ pub fn deinit(self: *App) void {
self.telemetry.deinit();
self.notification.deinit();
self.http.deinit();
+ self.snapshot.deinit(allocator);
self.platform.deinit();
allocator.destroy(self);
diff --git a/src/browser/Browser.zig b/src/browser/Browser.zig
index 1d3bcbfeb..e2317761a 100644
--- a/src/browser/Browser.zig
+++ b/src/browser/Browser.zig
@@ -34,7 +34,7 @@ const Session = @import("Session.zig");
// A browser contains only one session.
const Browser = @This();
-env: *js.Env,
+env: js.Env,
app: *App,
session: ?Session,
allocator: Allocator,
@@ -48,7 +48,7 @@ notification: *Notification,
pub fn init(app: *App) !Browser {
const allocator = app.allocator;
- const env = try js.Env.init(allocator, &app.platform, .{});
+ var env = try js.Env.init(allocator, &app.platform, &app.snapshot);
errdefer env.deinit();
const notification = try Notification.init(allocator, app.notification);
diff --git a/src/browser/Page.zig b/src/browser/Page.zig
index b4db2d37a..7be266847 100644
--- a/src/browser/Page.zig
+++ b/src/browser/Page.zig
@@ -37,8 +37,6 @@ const Scheduler = @import("Scheduler.zig");
const EventManager = @import("EventManager.zig");
const ScriptManager = @import("ScriptManager.zig");
-const polyfill = @import("polyfill/polyfill.zig");
-
const Parser = @import("parser/Parser.zig");
const URL = @import("webapi/URL.zig");
@@ -124,8 +122,6 @@ _upgrading_element: ?*Node = null,
// List of custom elements that were created before their definition was registered
_undefined_custom_elements: std.ArrayList(*Element.Html.Custom) = .{},
-_polyfill_loader: polyfill.Loader = .{},
-
// for heap allocations and managing WebAPI objects
_factory: Factory,
@@ -230,6 +226,7 @@ fn reset(self: *Page, comptime initializing: bool) !void {
self._parse_state = .pre;
self._load_state = .parsing;
+ self._parse_mode = .document;
self._attribute_lookup = .empty;
self._attribute_named_node_map_lookup = .empty;
self._event_manager = EventManager.init(self);
@@ -238,7 +235,7 @@ fn reset(self: *Page, comptime initializing: bool) !void {
errdefer self._script_manager.deinit();
if (comptime initializing == true) {
- self.js = try self._session.executor.createContext(self, true, JS.GlobalMissingCallback.init(&self._polyfill_loader));
+ self.js = try self._session.executor.createContext(self, true);
errdefer self.js.deinit();
}
diff --git a/src/browser/js/Context.zig b/src/browser/js/Context.zig
index 2d1a4cbd3..a11023a90 100644
--- a/src/browser/js/Context.zig
+++ b/src/browser/js/Context.zig
@@ -106,9 +106,6 @@ module_identifier: std.AutoHashMapUnmanaged(u32, [:0]const u8) = .empty,
// the page's script manager
script_manager: ?*ScriptManager,
-// Global callback is called on missing property.
-global_callback: ?js.GlobalMissingCallback = null,
-
const ModuleEntry = struct {
// Can be null if we're asynchrously loading the module, in
// which case resolver_promise cannot be null.
@@ -645,7 +642,12 @@ pub fn mapZigInstanceToJs(self: *Context, js_obj_: ?v8.Object, value: anytype) !
.prototype_len = @intCast(resolved.prototype_chain.len),
.subtype = if (@hasDecl(JsApi.Meta, "subtype")) JsApi.Meta.subype else .node,
};
- js_obj.setInternalField(0, v8.External.init(isolate, tao));
+
+ // Skip setting internal field for the global object (Window)
+ // Window accessors get the instance from context.page.window instead
+ if (resolved.class_id != @import("../webapi/Window.zig").JsApi.Meta.class_id) {
+ js_obj.setInternalField(0, v8.External.init(isolate, tao));
+ }
} else {
// If the struct is empty, we don't need to do all
// the TOA stuff and setting the internal data.
@@ -1028,7 +1030,7 @@ const valueToStringOpts = struct {
pub fn valueToString(self: *const Context, js_val: v8.Value, opts: valueToStringOpts) ![]u8 {
const allocator = opts.allocator orelse self.call_arena;
if (js_val.isSymbol()) {
- const js_sym = v8.Symbol{.handle = js_val.handle};
+ const js_sym = v8.Symbol{ .handle = js_val.handle };
const js_sym_desc = js_sym.getDescription(self.isolate);
return self.valueToString(js_sym_desc, .{});
}
@@ -1039,7 +1041,7 @@ pub fn valueToString(self: *const Context, js_val: v8.Value, opts: valueToString
pub fn valueToStringZ(self: *const Context, js_val: v8.Value, opts: valueToStringOpts) ![:0]u8 {
const allocator = opts.allocator orelse self.call_arena;
if (js_val.isSymbol()) {
- const js_sym = v8.Symbol{.handle = js_val.handle};
+ const js_sym = v8.Symbol{ .handle = js_val.handle };
const js_sym_desc = js_sym.getDescription(self.isolate);
return self.valueToStringZ(js_sym_desc, .{});
}
@@ -1094,7 +1096,7 @@ fn _debugValue(self: *const Context, js_val: v8.Value, seen: *std.AutoHashMapUnm
}
if (js_val.isSymbol()) {
- const js_sym = v8.Symbol{.handle = js_val.handle};
+ const js_sym = v8.Symbol{ .handle = js_val.handle };
const js_sym_desc = js_sym.getDescription(self.isolate);
const js_sym_str = try self.valueToString(js_sym_desc, .{});
return writer.print("{s} (symbol)", .{js_sym_str});
@@ -1596,6 +1598,33 @@ pub fn typeTaggedAnyOpaque(comptime R: type, js_obj: v8.Object) !R {
return @constCast(@as(*const T, &.{}));
}
+ // Special case for Window: the global object doesn't have internal fields
+ // Window instance is stored in context.page.window instead
+ if (js_obj.internalFieldCount() == 0) {
+ // Normally, this would be an error. All JsObject that map to a Zig type
+ // are either `empty_with_no_proto` (handled above) or have an
+ // interalFieldCount. The only exception to that is the Window...
+ const isolate = js_obj.getIsolate();
+ const context = fromIsolate(isolate);
+
+ const Window = @import("../webapi/Window.zig");
+ if (T == Window) {
+ return context.page.window;
+ }
+
+ // ... Or the window's prototype.
+ // We could make this all comptime-fancy, but it's easier to hard-code
+ // the EventTarget
+
+ const EventTarget = @import("../webapi/EventTarget.zig");
+ if (T == EventTarget) {
+ return context.page.window._proto;
+ }
+
+ // Type not found in Window's prototype chain
+ return error.InvalidArgument;
+ }
+
// if it isn't an empty struct, then the v8.Object should have an
// InternalFieldCount > 0, since our toa pointer should be embedded
// at index 0 of the internal field count.
diff --git a/src/browser/js/Env.zig b/src/browser/js/Env.zig
index 5bc1f7fcc..a5d205993 100644
--- a/src/browser/js/Env.zig
+++ b/src/browser/js/Env.zig
@@ -26,15 +26,14 @@ const bridge = @import("bridge.zig");
const Caller = @import("Caller.zig");
const Context = @import("Context.zig");
const Platform = @import("Platform.zig");
+const Snapshot = @import("Snapshot.zig");
const Inspector = @import("Inspector.zig");
const ExecutionWorld = @import("ExecutionWorld.zig");
-const NamedFunction = Caller.NamedFunction;
+const JsApis = bridge.JsApis;
const Allocator = std.mem.Allocator;
const ArenaAllocator = std.heap.ArenaAllocator;
-const JsApis = bridge.JsApis;
-
// The Env maps to a V8 isolate, which represents a isolated sandbox for
// executing JavaScript. The Env is where we'll define our V8 <-> Zig bindings,
// and it's where we'll start ExecutionWorlds, which actually execute JavaScript.
@@ -53,28 +52,22 @@ isolate: v8.Isolate,
// just kept around because we need to free it on deinit
isolate_params: *v8.CreateParams,
-// Given a type, we can lookup its index in JS_API_LOOKUP and then have
-// access to its TunctionTemplate (the thing we need to create an instance
-// of it)
-// I.e.:
-// const index = @field(JS_API_LOOKUP, @typeName(type_name))
-// const template = templates[index];
-templates: [JsApis.len]v8.FunctionTemplate,
-
context_id: usize,
-const Opts = struct {};
+// Dynamic slice to avoid circular dependency on JsApis.len at comptime
+templates: []v8.FunctionTemplate,
-pub fn init(allocator: Allocator, platform: *const Platform, _: Opts) !*Env {
- // var params = v8.initCreateParams();
+pub fn init(allocator: Allocator, platform: *const Platform, snapshot: *Snapshot) !Env {
var params = try allocator.create(v8.CreateParams);
errdefer allocator.destroy(params);
-
v8.c.v8__Isolate__CreateParams__CONSTRUCT(params);
+ params.snapshot_blob = @ptrCast(&snapshot.startup_data);
params.array_buffer_allocator = v8.createDefaultArrayBufferAllocator();
errdefer v8.destroyArrayBufferAllocator(params.array_buffer_allocator.?);
+ params.external_references = &snapshot.external_references;
+
var isolate = v8.Isolate.init(params);
errdefer isolate.deinit();
@@ -88,42 +81,35 @@ pub fn init(allocator: Allocator, platform: *const Platform, _: Opts) !*Env {
isolate.setHostInitializeImportMetaObjectCallback(Context.metaObjectCallback);
- var temp_scope: v8.HandleScope = undefined;
- v8.HandleScope.init(&temp_scope, isolate);
- defer temp_scope.deinit();
+ // // Allocate templates array dynamically to avoid comptime dependency on JsApis.len
+ const templates = try allocator.alloc(v8.FunctionTemplate, JsApis.len);
+ errdefer allocator.free(templates);
+
+ {
+ var temp_scope: v8.HandleScope = undefined;
+ v8.HandleScope.init(&temp_scope, isolate);
+ defer temp_scope.deinit();
+ const context = v8.Context.init(isolate, null, null);
+
+ context.enter();
+ defer context.exit();
- const env = try allocator.create(Env);
- errdefer allocator.destroy(env);
+ inline for (JsApis, 0..) |JsApi, i| {
+ JsApi.Meta.class_id = i;
+ const data = context.getDataFromSnapshotOnce(snapshot.data_start + i);
+ const function = v8.FunctionTemplate{ .handle = @ptrCast(data) };
+ templates[i] = v8.Persistent(v8.FunctionTemplate).init(isolate, function).castToFunctionTemplate();
+ }
+ }
- env.* = .{
+ return .{
.context_id = 0,
- .platform = platform,
.isolate = isolate,
- .templates = undefined,
+ .platform = platform,
.allocator = allocator,
+ .templates = templates,
.isolate_params = params,
};
-
- // Populate our templates lookup. generateClass creates the
- // v8.FunctionTemplate, which we store in our env.templates.
- // The ordering doesn't matter. What matters is that, given a type
- // we can get its index via: @field(types.LOOKUP, type_name)
- const templates = &env.templates;
- inline for (JsApis, 0..) |JsApi, i| {
- @setEvalBranchQuota(10_000);
- JsApi.Meta.class_id = i;
- templates[i] = v8.Persistent(v8.FunctionTemplate).init(isolate, generateClass(JsApi, isolate)).castToFunctionTemplate();
- }
-
- // Above, we've created all our our FunctionTemplates. Now that we
- // have them all, we can hook up the prototypes.
- inline for (JsApis, 0..) |JsApi, i| {
- if (comptime protoIndexLookup(JsApi)) |proto_index| {
- templates[i].inherit(templates[proto_index]);
- }
- }
-
- return env;
}
pub fn deinit(self: *Env) void {
@@ -131,7 +117,7 @@ pub fn deinit(self: *Env) void {
self.isolate.deinit();
v8.destroyArrayBufferAllocator(self.isolate_params.array_buffer_allocator.?);
self.allocator.destroy(self.isolate_params);
- self.allocator.destroy(self);
+ self.allocator.free(self.templates);
}
pub fn newInspector(self: *Env, arena: Allocator, ctx: anytype) !Inspector {
@@ -149,7 +135,6 @@ pub fn pumpMessageLoop(self: *const Env) bool {
pub fn runIdleTasks(self: *const Env) void {
return self.platform.inner.runIdleTasks(self.isolate, 1);
}
-
pub fn newExecutionWorld(self: *Env) !ExecutionWorld {
return .{
.env = self,
@@ -207,148 +192,3 @@ fn promiseRejectCallback(v8_msg: v8.C_PromiseRejectMessage) callconv(.c) void {
.note = "This should be updated to call window.unhandledrejection",
});
}
-
-// Give it a Zig struct, get back a v8.FunctionTemplate.
-// The FunctionTemplate is a bit like a struct container - it's where
-// we'll attach functions/getters/setters and where we'll "inherit" a
-// prototype type (if there is any)
-fn generateClass(comptime JsApi: type, isolate: v8.Isolate) v8.FunctionTemplate {
- const template = generateConstructor(JsApi, isolate);
- attachClass(JsApi, isolate, template);
- return template;
-}
-
-// Normally this is called from generateClass. Where generateClass creates
-// the constructor (hence, the FunctionTemplate), attachClass adds all
-// of its functions, getters, setters, ...
-// But it's extracted from generateClass because we also have 1 global
-// object (i.e. the Window), which gets attached not only to the Window
-// constructor/FunctionTemplate as normal, but also through the default
-// FunctionTemplate of the isolate (in createContext)
-pub fn attachClass(comptime JsApi: type, isolate: v8.Isolate, template: v8.FunctionTemplate) void {
- const template_proto = template.getPrototypeTemplate();
-
- const declarations = @typeInfo(JsApi).@"struct".decls;
- inline for (declarations) |d| {
- const name: [:0]const u8 = d.name;
- const value = @field(JsApi, name);
- const definition = @TypeOf(value);
-
- switch (definition) {
- bridge.Accessor => {
- const js_name = v8.String.initUtf8(isolate, name).toName();
- const getter_callback = v8.FunctionTemplate.initCallback(isolate, value.getter);
- if (value.setter == null) {
- if (value.static) {
- template.setAccessorGetter(js_name, getter_callback);
- } else {
- template_proto.setAccessorGetter(js_name, getter_callback);
- }
- } else {
- std.debug.assert(value.static == false);
- const setter_callback = v8.FunctionTemplate.initCallback(isolate, value.setter);
- template_proto.setAccessorGetterAndSetter(js_name, getter_callback, setter_callback);
- }
- },
- bridge.Function => {
- const function_template = v8.FunctionTemplate.initCallback(isolate, value.func);
- const js_name: v8.Name = v8.String.initUtf8(isolate, name).toName();
- if (value.static) {
- template.set(js_name, function_template, v8.PropertyAttribute.None);
- } else {
- template_proto.set(js_name, function_template, v8.PropertyAttribute.None);
- }
- },
- bridge.Indexed => {
- const configuration = v8.IndexedPropertyHandlerConfiguration{
- .getter = value.getter,
- };
- template_proto.setIndexedProperty(configuration, null);
- },
- bridge.NamedIndexed => template.getInstanceTemplate().setNamedProperty(.{
- .getter = value.getter,
- .setter = value.setter,
- .deleter = value.deleter,
- .flags = v8.PropertyHandlerFlags.OnlyInterceptStrings | v8.PropertyHandlerFlags.NonMasking,
- }, null),
- bridge.Iterator => {
- // Same as a function, but with a specific name
- const function_template = v8.FunctionTemplate.initCallback(isolate, value.func);
- const js_name = if (value.async)
- v8.Symbol.getAsyncIterator(isolate).toName()
- else
- v8.Symbol.getIterator(isolate).toName();
- template_proto.set(js_name, function_template, v8.PropertyAttribute.None);
- },
- bridge.Property => {
- const js_value = switch (value) {
- .int => |v| js.simpleZigValueToJs(isolate, v, true, false),
- };
-
- const js_name = v8.String.initUtf8(isolate, name).toName();
- // apply it both to the type itself
- template.set(js_name, js_value, v8.PropertyAttribute.ReadOnly + v8.PropertyAttribute.DontDelete);
-
- // and to instances of the type
- template_proto.set(js_name, js_value, v8.PropertyAttribute.ReadOnly + v8.PropertyAttribute.DontDelete);
- },
- bridge.Constructor => {}, // already handled in generateClasss
- else => {},
- }
- }
-
- if (@hasDecl(JsApi.Meta, "htmldda")) {
- const instance_template = template.getInstanceTemplate();
- instance_template.markAsUndetectable();
- instance_template.setCallAsFunctionHandler(JsApi.Meta.callable.func);
- }
-}
-
-// Even if a struct doesn't have a `constructor` function, we still
-// `generateConstructor`, because this is how we create our
-// FunctionTemplate. Such classes exist, but they can't be instantiated
-// via `new ClassName()` - but they could, for example, be created in
-// Zig and returned from a function call, which is why we need the
-// FunctionTemplate.
-fn generateConstructor(comptime JsApi: type, isolate: v8.Isolate) v8.FunctionTemplate {
- const callback = blk: {
- if (@hasDecl(JsApi, "constructor")) {
- break :blk JsApi.constructor.func;
- }
-
- break :blk struct {
- fn wrap(raw_info: ?*const v8.C_FunctionCallbackInfo) callconv(.c) void {
- const info = v8.FunctionCallbackInfo.initFromV8(raw_info);
- var caller = Caller.init(info);
- defer caller.deinit();
-
- const iso = caller.isolate;
- log.warn(.js, "Illegal constructor call", .{ .name = @typeName(JsApi) });
- const js_exception = iso.throwException(js._createException(iso, "Illegal Constructor"));
- info.getReturnValue().set(js_exception);
- return;
- }
- }.wrap;
- };
-
- const template = v8.FunctionTemplate.initCallback(isolate, callback);
- if (!@hasDecl(JsApi.Meta, "empty_with_no_proto")) {
- template.getInstanceTemplate().setInternalFieldCount(1);
- }
- const class_name = v8.String.initUtf8(isolate, if (@hasDecl(JsApi.Meta, "name")) JsApi.Meta.name else @typeName(JsApi));
- template.setClassName(class_name);
- return template;
-}
-
-pub fn protoIndexLookup(comptime JsApi: type) ?bridge.JsApiLookup.BackingInt {
- @setEvalBranchQuota(2000);
- comptime {
- const T = JsApi.bridge.type;
- if (!@hasField(T, "_proto")) {
- return null;
- }
- const Ptr = std.meta.fieldInfo(T, ._proto).type;
- const F = @typeInfo(Ptr).pointer.child;
- return bridge.JsApiLookup.getId(F.JsApi);
- }
-}
diff --git a/src/browser/js/ExecutionWorld.zig b/src/browser/js/ExecutionWorld.zig
index e9a8786dc..38e066ffd 100644
--- a/src/browser/js/ExecutionWorld.zig
+++ b/src/browser/js/ExecutionWorld.zig
@@ -17,13 +17,13 @@
// along with this program. If not, see .
const std = @import("std");
+const IS_DEBUG = @import("builtin").mode == .Debug;
const log = @import("../../log.zig");
const js = @import("js.zig");
const v8 = js.v8;
-const bridge = @import("bridge.zig");
const Env = @import("Env.zig");
const Context = @import("Context.zig");
@@ -34,8 +34,6 @@ const ArenaAllocator = std.heap.ArenaAllocator;
const CONTEXT_ARENA_RETAIN = 1024 * 64;
-const JsApis = bridge.JsApis;
-
// ExecutionWorld closely models a JS World.
// https://chromium.googlesource.com/chromium/src/+/master/third_party/blink/renderer/bindings/core/v8/V8BindingDesign.md#World
// https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/scripting/ExecutionWorld
@@ -72,82 +70,32 @@ pub fn deinit(self: *ExecutionWorld) void {
// 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 createContext(self: *ExecutionWorld, page: *Page, enter: bool, global_callback: ?js.GlobalMissingCallback) !*Context {
+pub fn createContext(self: *ExecutionWorld, page: *Page, enter: bool) !*Context {
std.debug.assert(self.context == null);
const env = self.env;
const isolate = env.isolate;
- const templates = &self.env.templates;
var v8_context: v8.Context = blk: {
var temp_scope: v8.HandleScope = undefined;
v8.HandleScope.init(&temp_scope, isolate);
defer temp_scope.deinit();
- const js_global = v8.FunctionTemplate.initDefault(isolate);
- js_global.setClassName(v8.String.initUtf8(isolate, "Window"));
- Env.attachClass(@TypeOf(page.window.*).JsApi, isolate, js_global);
-
- 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 context = Context.fromIsolate(info.getIsolate());
-
- const property = context.valueToString(.{ .handle = c_name.? }, .{}) catch "???";
- if (context.global_callback.?.missing(property, 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 (JsApis, 0..) |JsApi, i| {
- if (@hasDecl(JsApi.Meta, "name")) {
- const class_name = if (@hasDecl(JsApi.Meta, "constructor_alias")) JsApi.Meta.constructor_alias else JsApi.Meta.name;
- const v8_class_name = v8.String.initUtf8(isolate, class_name);
- global_template.set(v8_class_name.toName(), templates[i], v8.PropertyAttribute.None);
- }
- }
+ if (comptime IS_DEBUG) {
+ // Getting this into the snapshot is tricky (anything involving the
+ // global is tricky). Easier to do here, and in debug more, we're
+ // find with paying the small perf hit.
+ const js_global = v8.FunctionTemplate.initDefault(isolate);
+ const global_template = js_global.getInstanceTemplate();
- // The global object (Window) has already been hooked into the v8
- // engine when the Env was initialized - like every other type.
- // But the V8 global is its own FunctionTemplate instance so even
- // though it's also a Window, we need to set the prototype for this
- // specific instance of the the Window.
- {
- const proto_type = @typeInfo(@TypeOf(page.window._proto)).pointer.child;
- const proto_index = bridge.JsApiLookup.getId(proto_type.JsApi);
- js_global.inherit(templates[proto_index]);
+ global_template.setNamedProperty(v8.NamedPropertyHandlerConfiguration{
+ .getter = unknownPropertyCallback,
+ .flags = v8.PropertyHandlerFlags.NonMasking | v8.PropertyHandlerFlags.OnlyInterceptStrings,
+ }, null);
}
- const context_local = v8.Context.init(isolate, global_template, null);
+ const context_local = v8.Context.init(isolate, null, null);
const v8_context = v8.Persistent(v8.Context).init(isolate, context_local).castToContext();
- v8_context.enter();
- errdefer if (enter) v8_context.exit();
- defer if (!enter) v8_context.exit();
-
- // This shouldn't be necessary, but it is:
- // https://groups.google.com/g/v8-users/c/qAQQBmbi--8
- // TODO: see if newer V8 engines have a way around this.
- inline for (JsApis, 0..) |JsApi, i| {
- if (comptime Env.protoIndexLookup(JsApi)) |proto_index| {
- const proto_obj = templates[proto_index].getFunction(v8_context).toObject();
- const self_obj = templates[i].getFunction(v8_context).toObject();
- _ = self_obj.setPrototype(v8_context, proto_obj);
- }
- }
break :blk v8_context;
};
@@ -158,19 +106,12 @@ pub fn createContext(self: *ExecutionWorld, page: *Page, enter: bool, global_cal
if (enter) {
handle_scope = @as(v8.HandleScope, undefined);
v8.HandleScope.init(&handle_scope.?, isolate);
+ v8_context.enter();
}
- errdefer if (enter) handle_scope.?.deinit();
-
- const js_global = v8_context.getGlobal();
- {
- // If we want to overwrite the built-in console, we have to
- // delete the built-in one.
-
- const console_key = v8.String.initUtf8(isolate, "console");
- if (js_global.deleteValue(v8_context, console_key) == false) {
- return error.ConsoleDeleteError;
- }
- }
+ errdefer if (enter) {
+ v8_context.exit();
+ handle_scope.?.deinit();
+ };
const context_id = env.context_id;
env.context_id = context_id + 1;
@@ -180,27 +121,18 @@ pub fn createContext(self: *ExecutionWorld, page: *Page, enter: bool, global_cal
.id = context_id,
.isolate = isolate,
.v8_context = v8_context,
- .templates = &env.templates,
+ .templates = env.templates,
.handle_scope = handle_scope,
.script_manager = &page._script_manager,
.call_arena = page.call_arena,
.arena = self.context_arena.allocator(),
- .global_callback = global_callback,
};
var context = &self.context.?;
- {
- // Store a pointer to our context inside the v8 context so that, given
- // a v8 context, we can get our context out
- const data = isolate.initBigIntU64(@intCast(@intFromPtr(context)));
- v8_context.setEmbedderData(1, data);
- }
-
- // Custom exception
- // TODO: this is an horrible hack, I can't figure out how to do this cleanly.
- {
- _ = try context.exec("DOMException.prototype.__proto__ = Error.prototype", "errorSubclass");
- }
+ // Store a pointer to our context inside the v8 context so that, given
+ // a v8 context, we can get our context out
+ const data = isolate.initBigIntU64(@intCast(@intFromPtr(context)));
+ v8_context.setEmbedderData(1, data);
try context.setupGlobal();
return context;
@@ -225,3 +157,42 @@ pub fn terminateExecution(self: *const ExecutionWorld) void {
pub fn resumeExecution(self: *const ExecutionWorld) void {
self.env.isolate.cancelTerminateExecution();
}
+
+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 ignored = std.StaticStringMap(void).initComptime(.{
+ .{ "process", {} },
+ .{ "ShadyDOM", {} },
+ .{ "ShadyCSS", {} },
+
+ .{ "litNonce", {} },
+ .{ "litHtmlVersions", {} },
+ .{ "litElementVersions", {} },
+ .{ "litHtmlPolyfillSupport", {} },
+ .{ "litElementHydrateSupport", {} },
+ .{ "litElementPolyfillSupport", {} },
+ .{ "reactiveElementVersions", {} },
+
+ .{ "recaptcha", {} },
+ .{ "grecaptcha", {} },
+ .{ "___grecaptcha_cfg", {} },
+ .{ "__recaptcha_api", {} },
+ .{ "__google_recaptcha_client", {} },
+
+ .{ "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,
+ });
+ }
+
+ return v8.Intercepted.No;
+}
diff --git a/src/browser/js/Snapshot.zig b/src/browser/js/Snapshot.zig
new file mode 100644
index 000000000..0618cbfa2
--- /dev/null
+++ b/src/browser/js/Snapshot.zig
@@ -0,0 +1,471 @@
+// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
+//
+// Francis Bouvier
+// Pierre Tachoire
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+const std = @import("std");
+const js = @import("js.zig");
+const bridge = @import("bridge.zig");
+const log = @import("../../log.zig");
+
+const IS_DEBUG = @import("builtin").mode == .Debug;
+const Window = @import("../webapi/Window.zig");
+
+const v8 = js.v8;
+const JsApis = bridge.JsApis;
+const Allocator = std.mem.Allocator;
+
+const Snapshot = @This();
+
+const embedded_snapshot_blob = if (@import("build_config").snapshot_path) |path| @embedFile(path) else "";
+
+// When creating our Snapshot, we use local function templates for every Zig type.
+// You cannot, from what I can tell, create persisted FunctoinTemplates at
+// snapshot creation time. But you can embedd those templates (or any other v8
+// Data) so that it's available to contexts created from the snapshot. This is
+// the starting index of those function templtes, which we can extract. At
+// creation time, in debug, we assert that this is actually a consecutive integer
+// sequence
+data_start: usize,
+
+// The snapshot data (v8.StartupData is a ptr to the data and len).
+startup_data: v8.StartupData,
+
+// V8 doesn't know how to serialize external references, and pretty much any hook
+// into Zig is an external reference (e.g. every accessor and function callback).
+// When we create the snapshot, we give it an array with the address of every
+// external reference. When we load the snapshot, we need to give it the same
+// array with the exact same number of entries in the same order (but, of course
+// cross-process, the value (address) might be different).
+external_references: [countExternalReferences()]isize,
+
+// Track whether this snapshot owns its data (was created in-process)
+// If false, the data points into embedded_snapshot_blob and should not be freed
+owns_data: bool = false,
+
+pub fn load(allocator: Allocator) !Snapshot {
+ if (loadEmbedded()) |snapshot| {
+ return snapshot;
+ }
+ return create(allocator);
+}
+
+fn loadEmbedded() ?Snapshot {
+ // Binary format: [data_start: usize][blob data]
+ const min_size = @sizeOf(usize) + 1000;
+ if (embedded_snapshot_blob.len < min_size) {
+ // our blob should be in the MB, this is just a quick sanity check
+ return null;
+ }
+
+ const data_start = std.mem.readInt(usize, embedded_snapshot_blob[0..@sizeOf(usize)], .little);
+ const blob = embedded_snapshot_blob[@sizeOf(usize)..];
+
+ const startup_data = v8.StartupData{ .data = blob.ptr, .raw_size = @intCast(blob.len) };
+ if (!v8.SnapshotCreator.startupDataIsValid(startup_data)) {
+ return null;
+ }
+
+ return .{
+ .owns_data = false,
+ .data_start = data_start,
+ .startup_data = startup_data,
+ .external_references = collectExternalReferences(),
+ };
+}
+
+pub fn deinit(self: Snapshot, allocator: Allocator) void {
+ // Only free if we own the data (was created in-process)
+ if (self.owns_data) {
+ allocator.free(self.startup_data.data[0..@intCast(self.startup_data.raw_size)]);
+ }
+}
+
+pub fn write(self: Snapshot, writer: *std.Io.Writer) !void {
+ if (!self.isValid()) {
+ return error.InvalidSnapshot;
+ }
+
+ try writer.writeInt(usize, self.data_start, .little);
+ try writer.writeAll(self.startup_data.data[0..@intCast(self.startup_data.raw_size)]);
+}
+
+pub fn fromEmbedded(self: Snapshot) bool {
+ // if the snapshot comes from the embedFile, then it'll be flagged as not
+ // owneing (aka, not needing to free) the data.
+ return self.owns_data == false;
+}
+
+fn isValid(self: Snapshot) bool {
+ return v8.SnapshotCreator.startupDataIsValid(self.startup_data);
+}
+
+pub fn create(allocator: Allocator) !Snapshot {
+ var external_references = collectExternalReferences();
+
+ var params = v8.initCreateParams();
+ params.array_buffer_allocator = v8.createDefaultArrayBufferAllocator();
+ defer v8.destroyArrayBufferAllocator(params.array_buffer_allocator.?);
+ params.external_references = @ptrCast(&external_references);
+
+ var snapshot_creator: v8.SnapshotCreator = undefined;
+ v8.SnapshotCreator.init(&snapshot_creator, ¶ms);
+ defer snapshot_creator.deinit();
+
+ var data_start: usize = 0;
+ const isolate = snapshot_creator.getIsolate();
+
+ {
+ // CreateBlob, which we'll call once everything is setup, MUST NOT
+ // be called from an active HandleScope. Hence we have this scope to
+ // clean it up before we call CreateBlob
+ var handle_scope: v8.HandleScope = undefined;
+ v8.HandleScope.init(&handle_scope, isolate);
+ defer handle_scope.deinit();
+
+ // Create templates (constructors only) FIRST
+ var templates: [JsApis.len]v8.FunctionTemplate = undefined;
+ inline for (JsApis, 0..) |JsApi, i| {
+ @setEvalBranchQuota(10_000);
+ templates[i] = generateConstructor(JsApi, isolate);
+ attachClass(JsApi, isolate, templates[i]);
+ }
+
+ // Set up prototype chains BEFORE attaching properties
+ // This must come before attachClass so inheritance is set up first
+ inline for (JsApis, 0..) |JsApi, i| {
+ if (comptime protoIndexLookup(JsApi)) |proto_index| {
+ templates[i].inherit(templates[proto_index]);
+ }
+ }
+
+ // 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 context = v8.Context.init(isolate, global_template, null);
+ context.enter();
+ defer context.exit();
+
+ // Add templates to context snapshot
+ var last_data_index: usize = 0;
+ inline for (JsApis, 0..) |_, i| {
+ @setEvalBranchQuota(10_000);
+ const data_index = snapshot_creator.addDataWithContext(context, @ptrCast(templates[i].handle));
+ if (i == 0) {
+ data_start = data_index;
+ last_data_index = data_index;
+ } else {
+ // This isn't strictly required, but it means we only need to keep
+ // the first data_index. This is based on the assumption that
+ // addDataWithContext always increases by 1. If we ever hit this
+ // error, then that assumption is wrong and we should capture
+ // all the indexes explicitly in an array.
+ if (data_index != last_data_index + 1) {
+ return error.InvalidDataIndex;
+ }
+ last_data_index = data_index;
+ }
+ }
+
+ // Realize all templates by getting their functions and attaching to global
+ const global_obj = context.getGlobal();
+
+ inline for (JsApis, 0..) |JsApi, i| {
+ const func = templates[i].getFunction(context);
+
+ // Attach to global if it has a name
+ if (@hasDecl(JsApi.Meta, "name")) {
+ const class_name = if (@hasDecl(JsApi.Meta, "constructor_alias"))
+ JsApi.Meta.constructor_alias
+ else
+ JsApi.Meta.name;
+ const v8_class_name = v8.String.initUtf8(isolate, class_name);
+ _ = global_obj.setValue(context, v8_class_name, func);
+ }
+ }
+
+ {
+ // If we want to overwrite the built-in console, we have to
+ // delete the built-in one.
+ const console_key = v8.String.initUtf8(isolate, "console");
+ if (global_obj.deleteValue(context, console_key) == false) {
+ return error.ConsoleDeleteError;
+ }
+ }
+
+ // This shouldn't be necessary, but it is:
+ // https://groups.google.com/g/v8-users/c/qAQQBmbi--8
+ // TODO: see if newer V8 engines have a way around this.
+ inline for (JsApis, 0..) |JsApi, i| {
+ if (comptime protoIndexLookup(JsApi)) |proto_index| {
+ const proto_obj = templates[proto_index].getFunction(context).toObject();
+ const self_obj = templates[i].getFunction(context).toObject();
+ _ = self_obj.setPrototype(context, proto_obj);
+ }
+ }
+
+ {
+ // Custom exception
+ // TODO: this is an horrible hack, I can't figure out how to do this cleanly.
+ const code = v8.String.initUtf8(isolate, "DOMException.prototype.__proto__ = Error.prototype");
+ _ = try (try v8.Script.compile(context, code, null)).run(context);
+ }
+
+ snapshot_creator.setDefaultContext(context);
+ }
+
+ const blob = snapshot_creator.createBlob(v8.FunctionCodeHandling.kKeep);
+ const owned = try allocator.dupe(u8, blob.data[0..@intCast(blob.raw_size)]);
+
+ return .{
+ .owns_data = true,
+ .data_start = data_start,
+ .external_references = external_references,
+ .startup_data = .{ .data = owned.ptr, .raw_size = @intCast(owned.len) },
+ };
+}
+
+// Count total callbacks needed for external_references array
+fn countExternalReferences() comptime_int {
+ @setEvalBranchQuota(100_000);
+
+ // +1 for the illegal constructor callback
+ var count: comptime_int = 1;
+
+ inline for (JsApis) |JsApi| {
+ // Constructor (only if explicit)
+ if (@hasDecl(JsApi, "constructor")) {
+ count += 1;
+ }
+
+ // Callable (htmldda)
+ if (@hasDecl(JsApi, "callable")) {
+ count += 1;
+ }
+
+ // All other callbacks
+ const declarations = @typeInfo(JsApi).@"struct".decls;
+ inline for (declarations) |d| {
+ const value = @field(JsApi, d.name);
+ const T = @TypeOf(value);
+ if (T == bridge.Accessor) {
+ count += 1; // getter
+ if (value.setter != null) count += 1; // setter
+ } else if (T == bridge.Function) {
+ count += 1;
+ } else if (T == bridge.Iterator) {
+ count += 1;
+ } else if (T == bridge.Indexed) {
+ count += 1;
+ } else if (T == bridge.NamedIndexed) {
+ count += 1; // getter
+ if (value.setter != null) count += 1;
+ if (value.deleter != null) count += 1;
+ }
+ }
+ }
+
+ return count + 1; // +1 for null terminator
+}
+
+fn collectExternalReferences() [countExternalReferences()]isize {
+ var idx: usize = 0;
+ var references = std.mem.zeroes([countExternalReferences()]isize);
+
+ references[idx] = @bitCast(@intFromPtr(&illegalConstructorCallback));
+ idx += 1;
+
+ inline for (JsApis) |JsApi| {
+ if (@hasDecl(JsApi, "constructor")) {
+ references[idx] = @bitCast(@intFromPtr(JsApi.constructor.func));
+ idx += 1;
+ }
+
+ if (@hasDecl(JsApi, "callable")) {
+ references[idx] = @bitCast(@intFromPtr(JsApi.callable.func));
+ idx += 1;
+ }
+
+ const declarations = @typeInfo(JsApi).@"struct".decls;
+ inline for (declarations) |d| {
+ const value = @field(JsApi, d.name);
+ const T = @TypeOf(value);
+ if (T == bridge.Accessor) {
+ references[idx] = @bitCast(@intFromPtr(value.getter));
+ idx += 1;
+ if (value.setter) |setter| {
+ references[idx] = @bitCast(@intFromPtr(setter));
+ idx += 1;
+ }
+ } else if (T == bridge.Function) {
+ references[idx] = @bitCast(@intFromPtr(value.func));
+ idx += 1;
+ } else if (T == bridge.Iterator) {
+ references[idx] = @bitCast(@intFromPtr(value.func));
+ idx += 1;
+ } else if (T == bridge.Indexed) {
+ references[idx] = @bitCast(@intFromPtr(value.getter));
+ idx += 1;
+ } else if (T == bridge.NamedIndexed) {
+ references[idx] = @bitCast(@intFromPtr(value.getter));
+ idx += 1;
+ if (value.setter) |setter| {
+ references[idx] = @bitCast(@intFromPtr(setter));
+ idx += 1;
+ }
+ if (value.deleter) |deleter| {
+ references[idx] = @bitCast(@intFromPtr(deleter));
+ idx += 1;
+ }
+ }
+ }
+ }
+
+ return references;
+}
+
+// Even if a struct doesn't have a `constructor` function, we still
+// `generateConstructor`, because this is how we create our
+// FunctionTemplate. Such classes exist, but they can't be instantiated
+// via `new ClassName()` - but they could, for example, be created in
+// Zig and returned from a function call, which is why we need the
+// FunctionTemplate.
+fn generateConstructor(comptime JsApi: type, isolate: v8.Isolate) v8.FunctionTemplate {
+ const callback = blk: {
+ if (@hasDecl(JsApi, "constructor")) {
+ break :blk JsApi.constructor.func;
+ }
+
+ // Use shared illegal constructor callback
+ break :blk illegalConstructorCallback;
+ };
+
+ const template = v8.FunctionTemplate.initCallback(isolate, callback);
+ if (!@hasDecl(JsApi.Meta, "empty_with_no_proto")) {
+ template.getInstanceTemplate().setInternalFieldCount(1);
+ }
+ const class_name = v8.String.initUtf8(isolate, if (@hasDecl(JsApi.Meta, "name")) JsApi.Meta.name else @typeName(JsApi));
+ template.setClassName(class_name);
+ return template;
+}
+
+// Attaches JsApi members to the prototype template (normal case)
+fn attachClass(comptime JsApi: type, isolate: v8.Isolate, template: v8.FunctionTemplate) void {
+ const target = template.getPrototypeTemplate();
+ const declarations = @typeInfo(JsApi).@"struct".decls;
+ inline for (declarations) |d| {
+ const name: [:0]const u8 = d.name;
+ const value = @field(JsApi, name);
+ const definition = @TypeOf(value);
+
+ switch (definition) {
+ bridge.Accessor => {
+ const js_name = v8.String.initUtf8(isolate, name).toName();
+ const getter_callback = v8.FunctionTemplate.initCallback(isolate, value.getter);
+ if (value.setter == null) {
+ if (value.static) {
+ template.setAccessorGetter(js_name, getter_callback);
+ } else {
+ target.setAccessorGetter(js_name, getter_callback);
+ }
+ } else {
+ std.debug.assert(value.static == false);
+ const setter_callback = v8.FunctionTemplate.initCallback(isolate, value.setter);
+ target.setAccessorGetterAndSetter(js_name, getter_callback, setter_callback);
+ }
+ },
+ bridge.Function => {
+ const function_template = v8.FunctionTemplate.initCallback(isolate, value.func);
+ const js_name: v8.Name = v8.String.initUtf8(isolate, name).toName();
+ if (value.static) {
+ template.set(js_name, function_template, v8.PropertyAttribute.None);
+ } else {
+ target.set(js_name, function_template, v8.PropertyAttribute.None);
+ }
+ },
+ bridge.Indexed => {
+ const configuration = v8.IndexedPropertyHandlerConfiguration{
+ .getter = value.getter,
+ };
+ target.setIndexedProperty(configuration, null);
+ },
+ bridge.NamedIndexed => template.getInstanceTemplate().setNamedProperty(.{
+ .getter = value.getter,
+ .setter = value.setter,
+ .deleter = value.deleter,
+ .flags = v8.PropertyHandlerFlags.OnlyInterceptStrings | v8.PropertyHandlerFlags.NonMasking,
+ }, null),
+ bridge.Iterator => {
+ const function_template = v8.FunctionTemplate.initCallback(isolate, value.func);
+ const js_name = if (value.async)
+ v8.Symbol.getAsyncIterator(isolate).toName()
+ else
+ v8.Symbol.getIterator(isolate).toName();
+ target.set(js_name, function_template, v8.PropertyAttribute.None);
+ },
+ bridge.Property => {
+ const js_value = switch (value) {
+ .int => |v| js.simpleZigValueToJs(isolate, v, true, false),
+ };
+
+ const js_name = v8.String.initUtf8(isolate, name).toName();
+ // apply it both to the type itself
+ template.set(js_name, js_value, v8.PropertyAttribute.ReadOnly + v8.PropertyAttribute.DontDelete);
+
+ // and to instances of the type
+ target.set(js_name, js_value, v8.PropertyAttribute.ReadOnly + v8.PropertyAttribute.DontDelete);
+ },
+ bridge.Constructor => {}, // already handled in generateConstructor
+ else => {},
+ }
+ }
+
+ if (@hasDecl(JsApi.Meta, "htmldda")) {
+ const instance_template = template.getInstanceTemplate();
+ instance_template.markAsUndetectable();
+ instance_template.setCallAsFunctionHandler(JsApi.Meta.callable.func);
+ }
+}
+
+fn protoIndexLookup(comptime JsApi: type) ?bridge.JsApiLookup.BackingInt {
+ @setEvalBranchQuota(2000);
+ comptime {
+ const T = JsApi.bridge.type;
+ if (!@hasField(T, "_proto")) {
+ return null;
+ }
+ const Ptr = std.meta.fieldInfo(T, ._proto).type;
+ const F = @typeInfo(Ptr).pointer.child;
+ return bridge.JsApiLookup.getId(F.JsApi);
+ }
+}
+
+// Shared illegal constructor callback for types without explicit constructors
+fn illegalConstructorCallback(raw_info: ?*const v8.C_FunctionCallbackInfo) callconv(.c) void {
+ const info = v8.FunctionCallbackInfo.initFromV8(raw_info);
+ const iso = info.getIsolate();
+ log.warn(.js, "Illegal constructor call", .{});
+ const js_exception = iso.throwException(js._createException(iso, "Illegal Constructor"));
+ info.getReturnValue().set(js_exception);
+}
diff --git a/src/browser/js/bridge.zig b/src/browser/js/bridge.zig
index a37097e17..b984cdca0 100644
--- a/src/browser/js/bridge.zig
+++ b/src/browser/js/bridge.zig
@@ -26,8 +26,8 @@ const Caller = @import("Caller.zig");
pub fn Builder(comptime T: type) type {
return struct {
- pub const ClassId = u16;
pub const @"type" = T;
+ pub const ClassId = u16;
pub fn constructor(comptime func: anytype, comptime opts: Constructor.Opts) Constructor {
return Constructor.init(T, func, opts);
diff --git a/src/browser/js/js.zig b/src/browser/js/js.zig
index 9530990be..882d00d72 100644
--- a/src/browser/js/js.zig
+++ b/src/browser/js/js.zig
@@ -26,6 +26,8 @@ pub const bridge = @import("bridge.zig");
pub const ExecutionWorld = @import("ExecutionWorld.zig");
pub const Context = @import("Context.zig");
pub const Inspector = @import("Inspector.zig");
+pub const Snapshot = @import("Snapshot.zig");
+pub const Platform = @import("Platform.zig");
// TODO: Is "This" really necessary?
pub const This = @import("This.zig");
@@ -38,7 +40,6 @@ pub const Function = @import("Function.zig");
const Caller = @import("Caller.zig");
const Page = @import("../Page.zig");
const Allocator = std.mem.Allocator;
-const NamedFunction = Context.NamedFunction;
pub fn Bridge(comptime T: type) type {
return bridge.Builder(T);
diff --git a/src/browser/polyfill/polyfill.zig b/src/browser/polyfill/polyfill.zig
deleted file mode 100644
index bf6f92274..000000000
--- a/src/browser/polyfill/polyfill.zig
+++ /dev/null
@@ -1,91 +0,0 @@
-// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
-//
-// Francis Bouvier
-// Pierre Tachoire
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU Affero General Public License as
-// published by the Free Software Foundation, either version 3 of the
-// License, or (at your option) any later version.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU Affero General Public License for more details.
-//
-// You should have received a copy of the GNU Affero General Public License
-// along with this program. If not, see .
-
-const std = @import("std");
-const builtin = @import("builtin");
-
-const js = @import("../js/js.zig");
-const log = @import("../../log.zig");
-const Allocator = std.mem.Allocator;
-
-pub const Loader = struct {
- state: enum { empty, loading } = .empty,
-
- done: struct {} = .{},
-
- fn load(self: *Loader, comptime name: []const u8, source: []const u8, js_context: *js.Context) void {
- var try_catch: js.TryCatch = undefined;
- try_catch.init(js_context);
- defer try_catch.deinit();
-
- self.state = .loading;
- defer self.state = .empty;
-
- log.debug(.js, "polyfill load", .{ .name = name });
- _ = js_context.exec(source, name) catch |err| {
- log.fatal(.app, "polyfill error", .{
- .name = name,
- .err = try_catch.err(js_context.call_arena) catch @errorName(err) orelse @errorName(err),
- });
- };
-
- @field(self.done, name) = true;
- }
-
- pub fn missing(self: *Loader, name: []const u8, js_context: *js.Context) bool {
- // Avoid recursive calls during polyfill loading.
- if (self.state == .loading) {
- return false;
- }
-
- if (comptime builtin.mode == .Debug) {
- const ignored = std.StaticStringMap(void).initComptime(.{
- .{ "process", {} },
- .{ "ShadyDOM", {} },
- .{ "ShadyCSS", {} },
-
- .{ "litNonce", {} },
- .{ "litHtmlVersions", {} },
- .{ "litElementVersions", {} },
- .{ "litHtmlPolyfillSupport", {} },
- .{ "litElementHydrateSupport", {} },
- .{ "litElementPolyfillSupport", {} },
- .{ "reactiveElementVersions", {} },
-
- .{ "recaptcha", {} },
- .{ "grecaptcha", {} },
- .{ "___grecaptcha_cfg", {} },
- .{ "__recaptcha_api", {} },
- .{ "__google_recaptcha_client", {} },
-
- .{ "CLOSURE_FLAGS", {} },
- });
- if (ignored.has(name)) {
- return false;
- }
-
- log.debug(.unknown_prop, "unkown global property", .{
- .info = "but the property can exist in pure JS",
- .stack = js_context.stackTrace() catch "???",
- .property = name,
- });
- }
-
- return false;
- }
-};
diff --git a/src/cdp/cdp.zig b/src/cdp/cdp.zig
index a2ce7159e..37c42fff0 100644
--- a/src/cdp/cdp.zig
+++ b/src/cdp/cdp.zig
@@ -22,7 +22,6 @@ const json = std.json;
const log = @import("../log.zig");
const js = @import("../browser/js/js.zig");
-const polyfill = @import("../browser/polyfill/polyfill.zig");
const App = @import("../App.zig");
const Browser = @import("../browser/Browser.zig");
@@ -700,10 +699,6 @@ const IsolatedWorld = struct {
executor: js.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();
}
@@ -729,7 +724,6 @@ const IsolatedWorld = struct {
_ = try self.executor.createContext(
page,
false,
- js.GlobalMissingCallback.init(&self.polyfill_loader),
);
}
diff --git a/src/main.zig b/src/main.zig
index b43ea92cf..c73ec79aa 100644
--- a/src/main.zig
+++ b/src/main.zig
@@ -100,7 +100,7 @@ fn run(allocator: Allocator, main_arena: Allocator) !void {
switch (args.mode) {
.serve => |opts| {
- log.debug(.app, "startup", .{ .mode = "serve" });
+ log.debug(.app, "startup", .{ .mode = "serve", .snapshot = app.snapshot.fromEmbedded() });
const address = std.net.Address.parseIp(opts.host, opts.port) catch |err| {
log.fatal(.app, "invalid server address", .{ .err = err, .host = opts.host, .port = opts.port });
return args.printUsageAndExit(false);
@@ -120,7 +120,7 @@ fn run(allocator: Allocator, main_arena: Allocator) !void {
},
.fetch => |opts| {
const url = opts.url;
- log.debug(.app, "startup", .{ .mode = "fetch", .dump = opts.dump, .url = url });
+ log.debug(.app, "startup", .{ .mode = "fetch", .dump = opts.dump, .url = url, .snapshot = app.snapshot.fromEmbedded() });
var fetch_opts = lp.FetchOpts{
.wait_ms = 5000,
diff --git a/src/main_snapshot_creator.zig b/src/main_snapshot_creator.zig
new file mode 100644
index 000000000..0a7fec595
--- /dev/null
+++ b/src/main_snapshot_creator.zig
@@ -0,0 +1,47 @@
+// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
+//
+// Francis Bouvier
+// Pierre Tachoire
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+const std = @import("std");
+const lp = @import("lightpanda");
+
+pub fn main() !void {
+ const allocator = std.heap.c_allocator;
+
+ var platform = try lp.js.Platform.init();
+ defer platform.deinit();
+
+ const snapshot = try lp.js.Snapshot.create(allocator);
+ defer snapshot.deinit(allocator);
+
+ var is_stdout = true;
+ var file = std.fs.File.stdout();
+ var args = try std.process.argsWithAllocator(allocator);
+ _ = args.next(); // executable name
+ if (args.next()) |n| {
+ is_stdout = false;
+ file = try std.fs.cwd().createFile(n, .{});
+ }
+ defer if (!is_stdout) {
+ file.close();
+ };
+
+ var buffer: [4096]u8 = undefined;
+ var writer = file.writer(&buffer);
+ try snapshot.write(&writer.interface);
+ try writer.end();
+}