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