mirror of
https://github.com/lightpanda-io/browser.git
synced 2025-12-23 22:58:00 -05:00
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:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -8,3 +8,4 @@ lightpanda.id
|
||||
/v8/
|
||||
/build/
|
||||
/src/html5ever/target/
|
||||
src/snapshot.bin
|
||||
|
||||
@@ -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:
|
||||
|
||||
|
||||
26
build.zig
26
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(.{
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
471
src/browser/js/Snapshot.zig
Normal 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, ¶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);
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
@@ -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),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
47
src/main_snapshot_creator.zig
Normal file
47
src/main_snapshot_creator.zig
Normal 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();
|
||||
}
|
||||
Reference in New Issue
Block a user