Enable v8 snapshots

There are two layers here. The first is that, on startup, a v8 SnapshotCreator
is created, and a snapshot-specific isolate/context is setup with our browser
environment. This contains most of what was in Env.init and a good chunk of
what was in ExecutionWorld.createContext. From this, we create a v8.StartupData
which is used for the creation of all subsequent contexts. The snapshot sits
at the application level, above the Env - it's re-used for all envs/isolates, so
this gives a nice performance boost for both 1 connection opening multiple pages
or multiple connections opening 1 page.

The second layer is that the Snapshot data can be embedded into the binary, so
that it doesn't have to be created on startup, but rather created at build-time.
This improves the startup time (though, I'm not really sure how to measure that
accurately...).

The first layer is the big win (and just works as-is without any build / usage
changes).

with snapshot
total runs 1000
total duration (ms) 7527
avg run duration (ms) 7
min run duration (ms) 5
max run duration (ms) 41

without snapshot
total runs 1000
total duration (ms) 9350
avg run duration (ms) 9
min run duration (ms) 8
max run duration (ms) 42

To embed a snapshot into the binary, we first need to create the snapshot file:

zig build -Doptimize=ReleaseFast snapshot_creator -- src/snapshot.bin

And then build using the new snapshot_path argument:

zig build -Dsnapshot_path=../../snapshot.bin -Doptimize=ReleaseFast

The paths are weird, I know...since it's embedded, it needs to be inside the
project path, hence we put it in src/snapshot.bin. And since it's embedded
relative to the embedder (src/browser/js/Snapshot.zig) the path has to be
relative to that, hence ../../snapshot.bin. I'm open to suggestions on
improving this.
This commit is contained in:
Karl Seguin
2025-12-18 20:10:38 +08:00
parent aa5e71112e
commit b3a0aaaeea
17 changed files with 693 additions and 409 deletions

1
.gitignore vendored
View File

@@ -8,3 +8,4 @@ lightpanda.id
/v8/
/build/
/src/html5ever/target/
src/snapshot.bin

View File

@@ -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:

View File

@@ -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(.{

View File

@@ -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",

View File

@@ -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);

View File

@@ -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);

View File

@@ -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();
}

View File

@@ -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.

View File

@@ -26,14 +26,13 @@ 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 Allocator = std.mem.Allocator;
const ArenaAllocator = std.heap.ArenaAllocator;
const JsApis = bridge.JsApis;
const Allocator = std.mem.Allocator;
const ArenaAllocator = std.heap.ArenaAllocator;
// 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,
@@ -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);
const env = try allocator.create(Env);
errdefer allocator.destroy(env);
{
var temp_scope: v8.HandleScope = undefined;
v8.HandleScope.init(&temp_scope, isolate);
defer temp_scope.deinit();
const context = v8.Context.init(isolate, null, null);
env.* = .{
.context_id = 0,
.platform = platform,
.isolate = isolate,
.templates = undefined,
.allocator = allocator,
.isolate_params = params,
};
context.enter();
defer context.exit();
// 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]);
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();
}
}
return env;
return .{
.context_id = 0,
.isolate = isolate,
.platform = platform,
.allocator = allocator,
.templates = templates,
.isolate_params = params,
};
}
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);
}
}

View File

@@ -17,13 +17,13 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
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);
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();
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,
global_template.setNamedProperty(v8.NamedPropertyHandlerConfiguration{
.getter = unknownPropertyCallback,
.flags = v8.PropertyHandlerFlags.NonMasking | v8.PropertyHandlerFlags.OnlyInterceptStrings,
};
global_template.setNamedProperty(configuration, null);
}, 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);
}
}
// 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]);
}
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;
}

471
src/browser/js/Snapshot.zig Normal file
View File

@@ -0,0 +1,471 @@
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// 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 <https://www.gnu.org/licenses/>.
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, &params);
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);
}

View File

@@ -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);

View File

@@ -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);

View File

@@ -1,91 +0,0 @@
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// 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 <https://www.gnu.org/licenses/>.
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;
}
};

View File

@@ -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),
);
}

View File

@@ -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,

View File

@@ -0,0 +1,47 @@
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// 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 <https://www.gnu.org/licenses/>.
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();
}