mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-04-04 00:20:32 +00:00
Tweak snapshot for workers
We'll have two types of contexts: one for pages and one for workers. They'll [probably] both be js.Context, but they'll have distinct FunctionTemplates attached to their global. The Worker template will only contain a small subset of the main Page's types, along with 1 or 2 of its own specific ones. The Snapshot now creates the templates for both, so that the Env contains the function templates for both contexts. Furthermore, having a "merged" view like this ensures that the env.template[N] indices are consistent between the two. However, the snapshot only attaches the Page-specific types to the snapshot context. This allows the Page-context to be created as-is (e.g. efficiently). The worker context will be created lazily, on demand, but from the templates loaded into the env (since, again, the env contains templates for both).
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
|
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
|
||||||
//
|
//
|
||||||
// Francis Bouvier <francis@lightpanda.io>
|
// Francis Bouvier <francis@lightpanda.io>
|
||||||
// Pierre Tachoire <pierre@lightpanda.io>
|
// Pierre Tachoire <pierre@lightpanda.io>
|
||||||
@@ -25,6 +25,7 @@ const IS_DEBUG = @import("builtin").mode == .Debug;
|
|||||||
|
|
||||||
const v8 = js.v8;
|
const v8 = js.v8;
|
||||||
const JsApis = bridge.JsApis;
|
const JsApis = bridge.JsApis;
|
||||||
|
const PageJsApis = bridge.PageJsApis;
|
||||||
|
|
||||||
const Snapshot = @This();
|
const Snapshot = @This();
|
||||||
|
|
||||||
@@ -113,6 +114,8 @@ fn isValid(self: Snapshot) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn create() !Snapshot {
|
pub fn create() !Snapshot {
|
||||||
|
comptime validatePrototypeChains(&JsApis);
|
||||||
|
|
||||||
var external_references = collectExternalReferences();
|
var external_references = collectExternalReferences();
|
||||||
|
|
||||||
var params: v8.CreateParams = undefined;
|
var params: v8.CreateParams = undefined;
|
||||||
@@ -135,7 +138,7 @@ pub fn create() !Snapshot {
|
|||||||
v8.v8__HandleScope__CONSTRUCT(&handle_scope, isolate);
|
v8.v8__HandleScope__CONSTRUCT(&handle_scope, isolate);
|
||||||
defer v8.v8__HandleScope__DESTRUCT(&handle_scope);
|
defer v8.v8__HandleScope__DESTRUCT(&handle_scope);
|
||||||
|
|
||||||
// Create templates (constructors only) FIRST
|
// Create templates for ALL types (JsApis)
|
||||||
var templates: [JsApis.len]*const v8.FunctionTemplate = undefined;
|
var templates: [JsApis.len]*const v8.FunctionTemplate = undefined;
|
||||||
inline for (JsApis, 0..) |JsApi, i| {
|
inline for (JsApis, 0..) |JsApi, i| {
|
||||||
@setEvalBranchQuota(10_000);
|
@setEvalBranchQuota(10_000);
|
||||||
@@ -144,20 +147,17 @@ pub fn create() !Snapshot {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Set up prototype chains BEFORE attaching properties
|
// Set up prototype chains BEFORE attaching properties
|
||||||
// This must come before attachClass so inheritance is set up first
|
|
||||||
inline for (JsApis, 0..) |JsApi, i| {
|
inline for (JsApis, 0..) |JsApi, i| {
|
||||||
if (comptime protoIndexLookup(JsApi)) |proto_index| {
|
if (comptime protoIndexLookup(JsApi)) |proto_index| {
|
||||||
v8.v8__FunctionTemplate__Inherit(templates[i], templates[proto_index]);
|
v8.v8__FunctionTemplate__Inherit(templates[i], 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 context = v8.v8__Context__New(isolate, null, null);
|
const context = v8.v8__Context__New(isolate, null, null);
|
||||||
v8.v8__Context__Enter(context);
|
v8.v8__Context__Enter(context);
|
||||||
defer v8.v8__Context__Exit(context);
|
defer v8.v8__Context__Exit(context);
|
||||||
|
|
||||||
// Add templates to context snapshot
|
// Add ALL templates to context snapshot
|
||||||
var last_data_index: usize = 0;
|
var last_data_index: usize = 0;
|
||||||
inline for (JsApis, 0..) |_, i| {
|
inline for (JsApis, 0..) |_, i| {
|
||||||
@setEvalBranchQuota(10_000);
|
@setEvalBranchQuota(10_000);
|
||||||
@@ -166,11 +166,6 @@ pub fn create() !Snapshot {
|
|||||||
data_start = data_index;
|
data_start = data_index;
|
||||||
last_data_index = data_index;
|
last_data_index = data_index;
|
||||||
} else {
|
} 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) {
|
if (data_index != last_data_index + 1) {
|
||||||
return error.InvalidDataIndex;
|
return error.InvalidDataIndex;
|
||||||
}
|
}
|
||||||
@@ -178,13 +173,12 @@ pub fn create() !Snapshot {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Realize all templates by getting their functions and attaching to global
|
|
||||||
const global_obj = v8.v8__Context__Global(context);
|
const global_obj = v8.v8__Context__Global(context);
|
||||||
|
|
||||||
inline for (JsApis, 0..) |JsApi, i| {
|
// Attach only PAGE types to the default context's global
|
||||||
|
inline for (PageJsApis, 0..) |JsApi, i| {
|
||||||
|
// PageJsApis[i] == JsApis[i] because the PageJsApis are position at the start of the list
|
||||||
const func = v8.v8__FunctionTemplate__GetFunction(templates[i], context);
|
const func = v8.v8__FunctionTemplate__GetFunction(templates[i], context);
|
||||||
|
|
||||||
// Attach to global if it has a name
|
|
||||||
if (@hasDecl(JsApi.Meta, "name")) {
|
if (@hasDecl(JsApi.Meta, "name")) {
|
||||||
if (@hasDecl(JsApi.Meta, "constructor_alias")) {
|
if (@hasDecl(JsApi.Meta, "constructor_alias")) {
|
||||||
const alias = JsApi.Meta.constructor_alias;
|
const alias = JsApi.Meta.constructor_alias;
|
||||||
@@ -192,12 +186,6 @@ pub fn create() !Snapshot {
|
|||||||
var maybe_result: v8.MaybeBool = undefined;
|
var maybe_result: v8.MaybeBool = undefined;
|
||||||
v8.v8__Object__Set(global_obj, context, v8_class_name, func, &maybe_result);
|
v8.v8__Object__Set(global_obj, context, v8_class_name, func, &maybe_result);
|
||||||
|
|
||||||
// @TODO: This is wrong. This name should be registered with the
|
|
||||||
// illegalConstructorCallback. I.e. new Image() is OK, but
|
|
||||||
// new HTMLImageElement() isn't.
|
|
||||||
// But we _have_ to register the name, i.e. HTMLImageElement
|
|
||||||
// has to be registered so, for now, instead of creating another
|
|
||||||
// template, we just hook it into the constructor.
|
|
||||||
const name = JsApi.Meta.name;
|
const name = JsApi.Meta.name;
|
||||||
const illegal_class_name = v8.v8__String__NewFromUtf8(isolate, name.ptr, v8.kNormal, @intCast(name.len));
|
const illegal_class_name = v8.v8__String__NewFromUtf8(isolate, name.ptr, v8.kNormal, @intCast(name.len));
|
||||||
var maybe_result2: v8.MaybeBool = undefined;
|
var maybe_result2: v8.MaybeBool = undefined;
|
||||||
@@ -216,8 +204,7 @@ pub fn create() !Snapshot {
|
|||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
// If we want to overwrite the built-in console, we have to
|
// Delete built-in console so we can inject our own
|
||||||
// delete the built-in one.
|
|
||||||
const console_key = v8.v8__String__NewFromUtf8(isolate, "console", v8.kNormal, 7);
|
const console_key = v8.v8__String__NewFromUtf8(isolate, "console", v8.kNormal, 7);
|
||||||
var maybe_deleted: v8.MaybeBool = undefined;
|
var maybe_deleted: v8.MaybeBool = undefined;
|
||||||
v8.v8__Object__Delete(global_obj, context, console_key, &maybe_deleted);
|
v8.v8__Object__Delete(global_obj, context, console_key, &maybe_deleted);
|
||||||
@@ -226,9 +213,8 @@ pub fn create() !Snapshot {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// This shouldn't be necessary, but it is:
|
// Set prototype chains on function objects
|
||||||
// https://groups.google.com/g/v8-users/c/qAQQBmbi--8
|
// 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| {
|
inline for (JsApis, 0..) |JsApi, i| {
|
||||||
if (comptime protoIndexLookup(JsApi)) |proto_index| {
|
if (comptime protoIndexLookup(JsApi)) |proto_index| {
|
||||||
const proto_func = v8.v8__FunctionTemplate__GetFunction(templates[proto_index], context);
|
const proto_func = v8.v8__FunctionTemplate__GetFunction(templates[proto_index], context);
|
||||||
@@ -243,8 +229,7 @@ pub fn create() !Snapshot {
|
|||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
// Custom exception
|
// DOMException prototype setup
|
||||||
// TODO: this is an horrible hack, I can't figure out how to do this cleanly.
|
|
||||||
const code_str = "DOMException.prototype.__proto__ = Error.prototype";
|
const code_str = "DOMException.prototype.__proto__ = Error.prototype";
|
||||||
const code = v8.v8__String__NewFromUtf8(isolate, code_str.ptr, v8.kNormal, @intCast(code_str.len));
|
const code = v8.v8__String__NewFromUtf8(isolate, code_str.ptr, v8.kNormal, @intCast(code_str.len));
|
||||||
const script = v8.v8__Script__Compile(context, code, null) orelse return error.ScriptCompileFailed;
|
const script = v8.v8__Script__Compile(context, code, null) orelse return error.ScriptCompileFailed;
|
||||||
@@ -264,20 +249,6 @@ pub fn create() !Snapshot {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper to check if a JsApi has a NamedIndexed handler
|
|
||||||
fn hasNamedIndexedGetter(comptime JsApi: type) bool {
|
|
||||||
const declarations = @typeInfo(JsApi).@"struct".decls;
|
|
||||||
inline for (declarations) |d| {
|
|
||||||
const value = @field(JsApi, d.name);
|
|
||||||
const T = @TypeOf(value);
|
|
||||||
if (T == bridge.NamedIndexed) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Count total callbacks needed for external_references array
|
|
||||||
fn countExternalReferences() comptime_int {
|
fn countExternalReferences() comptime_int {
|
||||||
@setEvalBranchQuota(100_000);
|
@setEvalBranchQuota(100_000);
|
||||||
|
|
||||||
@@ -290,23 +261,20 @@ fn countExternalReferences() comptime_int {
|
|||||||
count += 1;
|
count += 1;
|
||||||
|
|
||||||
inline for (JsApis) |JsApi| {
|
inline for (JsApis) |JsApi| {
|
||||||
// Constructor (only if explicit)
|
|
||||||
if (@hasDecl(JsApi, "constructor")) {
|
if (@hasDecl(JsApi, "constructor")) {
|
||||||
count += 1;
|
count += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Callable (htmldda)
|
|
||||||
if (@hasDecl(JsApi, "callable")) {
|
if (@hasDecl(JsApi, "callable")) {
|
||||||
count += 1;
|
count += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// All other callbacks
|
|
||||||
const declarations = @typeInfo(JsApi).@"struct".decls;
|
const declarations = @typeInfo(JsApi).@"struct".decls;
|
||||||
inline for (declarations) |d| {
|
inline for (declarations) |d| {
|
||||||
const value = @field(JsApi, d.name);
|
const value = @field(JsApi, d.name);
|
||||||
const T = @TypeOf(value);
|
const T = @TypeOf(value);
|
||||||
if (T == bridge.Accessor) {
|
if (T == bridge.Accessor) {
|
||||||
count += 1; // getter
|
count += 1;
|
||||||
if (value.setter != null) {
|
if (value.setter != null) {
|
||||||
count += 1;
|
count += 1;
|
||||||
}
|
}
|
||||||
@@ -320,14 +288,13 @@ fn countExternalReferences() comptime_int {
|
|||||||
count += 1;
|
count += 1;
|
||||||
}
|
}
|
||||||
} else if (T == bridge.NamedIndexed) {
|
} else if (T == bridge.NamedIndexed) {
|
||||||
count += 1; // getter
|
count += 1;
|
||||||
if (value.setter != null) count += 1;
|
if (value.setter != null) count += 1;
|
||||||
if (value.deleter != null) count += 1;
|
if (value.deleter != null) count += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// In debug mode, add unknown property callbacks for types without NamedIndexed
|
|
||||||
if (comptime IS_DEBUG) {
|
if (comptime IS_DEBUG) {
|
||||||
inline for (JsApis) |JsApi| {
|
inline for (JsApis) |JsApi| {
|
||||||
if (!hasNamedIndexedGetter(JsApi)) {
|
if (!hasNamedIndexedGetter(JsApi)) {
|
||||||
@@ -399,7 +366,6 @@ fn collectExternalReferences() [countExternalReferences()]isize {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// In debug mode, collect unknown property callbacks for types without NamedIndexed
|
|
||||||
if (comptime IS_DEBUG) {
|
if (comptime IS_DEBUG) {
|
||||||
inline for (JsApis) |JsApi| {
|
inline for (JsApis) |JsApi| {
|
||||||
if (!hasNamedIndexedGetter(JsApi)) {
|
if (!hasNamedIndexedGetter(JsApi)) {
|
||||||
@@ -412,34 +378,8 @@ fn collectExternalReferences() [countExternalReferences()]isize {
|
|||||||
return references;
|
return references;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Even if a struct doesn't have a `constructor` function, we still
|
fn protoIndexLookup(comptime JsApi: type) ?u16 {
|
||||||
// `generateConstructor`, because this is how we create our
|
return protoIndexLookupFor(&JsApis, JsApi);
|
||||||
// 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) *const 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.v8__FunctionTemplate__New__DEFAULT2(isolate, callback).?;
|
|
||||||
{
|
|
||||||
const internal_field_count = comptime countInternalFields(JsApi);
|
|
||||||
if (internal_field_count > 0) {
|
|
||||||
const instance_template = v8.v8__FunctionTemplate__InstanceTemplate(template);
|
|
||||||
v8.v8__ObjectTemplate__SetInternalFieldCount(instance_template, internal_field_count);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const name_str = if (@hasDecl(JsApi.Meta, "name")) JsApi.Meta.name else @typeName(JsApi);
|
|
||||||
const class_name = v8.v8__String__NewFromUtf8(isolate, name_str.ptr, v8.kNormal, @intCast(name_str.len));
|
|
||||||
v8.v8__FunctionTemplate__SetClassName(template, class_name);
|
|
||||||
return template;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn countInternalFields(comptime JsApi: type) u8 {
|
pub fn countInternalFields(comptime JsApi: type) u8 {
|
||||||
@@ -480,14 +420,109 @@ pub fn countInternalFields(comptime JsApi: type) u8 {
|
|||||||
return cache_count + 1;
|
return cache_count + 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Attaches JsApi members to the prototype template (normal case)
|
// Shared illegal constructor callback for types without explicit constructors
|
||||||
|
fn illegalConstructorCallback(raw_info: ?*const v8.FunctionCallbackInfo) callconv(.c) void {
|
||||||
|
const isolate = v8.v8__FunctionCallbackInfo__GetIsolate(raw_info);
|
||||||
|
log.warn(.js, "Illegal constructor call", .{});
|
||||||
|
|
||||||
|
const message = v8.v8__String__NewFromUtf8(isolate, "Illegal Constructor", v8.kNormal, 19);
|
||||||
|
const js_exception = v8.v8__Exception__TypeError(message);
|
||||||
|
|
||||||
|
_ = v8.v8__Isolate__ThrowException(isolate, js_exception);
|
||||||
|
var return_value: v8.ReturnValue = undefined;
|
||||||
|
v8.v8__FunctionCallbackInfo__GetReturnValue(raw_info, &return_value);
|
||||||
|
v8.v8__ReturnValue__Set(return_value, js_exception);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper to check if a JsApi has a NamedIndexed handler (public for reuse)
|
||||||
|
fn hasNamedIndexedGetter(comptime JsApi: type) bool {
|
||||||
|
const declarations = @typeInfo(JsApi).@"struct".decls;
|
||||||
|
inline for (declarations) |d| {
|
||||||
|
const value = @field(JsApi, d.name);
|
||||||
|
const T = @TypeOf(value);
|
||||||
|
if (T == bridge.NamedIndexed) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generic prototype index lookup for a given API list
|
||||||
|
fn protoIndexLookupFor(comptime ApiList: []const type, comptime JsApi: type) ?u16 {
|
||||||
|
@setEvalBranchQuota(100_000);
|
||||||
|
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;
|
||||||
|
// Look up in the provided API list
|
||||||
|
for (ApiList, 0..) |Api, i| {
|
||||||
|
if (Api == F.JsApi) {
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@compileError("Prototype " ++ @typeName(F.JsApi) ++ " not found in API list");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validates that every type in the API list has its full prototype chain
|
||||||
|
// contained within that same list. This catches errors where a type is added
|
||||||
|
// to a snapshot but its prototype dependencies are missing.
|
||||||
|
// See bridge.AllJsApis for more information.
|
||||||
|
fn validatePrototypeChains(comptime ApiList: []const type) void {
|
||||||
|
@setEvalBranchQuota(100_000);
|
||||||
|
inline for (ApiList) |JsApi| {
|
||||||
|
const T = JsApi.bridge.type;
|
||||||
|
if (@hasField(T, "_proto")) {
|
||||||
|
const Ptr = std.meta.fieldInfo(T, ._proto).type;
|
||||||
|
const ProtoType = @typeInfo(Ptr).pointer.child;
|
||||||
|
// Verify the prototype's JsApi is in our list
|
||||||
|
var found = false;
|
||||||
|
inline for (ApiList) |Api| {
|
||||||
|
if (Api == ProtoType.JsApi) {
|
||||||
|
found = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!found) {
|
||||||
|
@compileError(
|
||||||
|
@typeName(JsApi) ++ " has prototype " ++
|
||||||
|
@typeName(ProtoType.JsApi) ++ " which is not in the API list",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate a constructor template for a JsApi type (public for reuse)
|
||||||
|
pub fn generateConstructor(comptime JsApi: type, isolate: *v8.Isolate) *const v8.FunctionTemplate {
|
||||||
|
const callback = blk: {
|
||||||
|
if (@hasDecl(JsApi, "constructor")) {
|
||||||
|
break :blk JsApi.constructor.func;
|
||||||
|
}
|
||||||
|
break :blk illegalConstructorCallback;
|
||||||
|
};
|
||||||
|
|
||||||
|
const template = v8.v8__FunctionTemplate__New__DEFAULT2(isolate, callback).?;
|
||||||
|
{
|
||||||
|
const internal_field_count = comptime countInternalFields(JsApi);
|
||||||
|
if (internal_field_count > 0) {
|
||||||
|
const instance_template = v8.v8__FunctionTemplate__InstanceTemplate(template);
|
||||||
|
v8.v8__ObjectTemplate__SetInternalFieldCount(instance_template, internal_field_count);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const name_str = if (@hasDecl(JsApi.Meta, "name")) JsApi.Meta.name else @typeName(JsApi);
|
||||||
|
const class_name = v8.v8__String__NewFromUtf8(isolate, name_str.ptr, v8.kNormal, @intCast(name_str.len));
|
||||||
|
v8.v8__FunctionTemplate__SetClassName(template, class_name);
|
||||||
|
return template;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attach JsApi members to a template (public for reuse)
|
||||||
fn attachClass(comptime JsApi: type, isolate: *v8.Isolate, template: *const v8.FunctionTemplate) void {
|
fn attachClass(comptime JsApi: type, isolate: *v8.Isolate, template: *const v8.FunctionTemplate) void {
|
||||||
const instance = v8.v8__FunctionTemplate__InstanceTemplate(template);
|
const instance = v8.v8__FunctionTemplate__InstanceTemplate(template);
|
||||||
const prototype = v8.v8__FunctionTemplate__PrototypeTemplate(template);
|
const prototype = v8.v8__FunctionTemplate__PrototypeTemplate(template);
|
||||||
|
|
||||||
// Create a signature that validates the receiver is an instance of this template.
|
|
||||||
// This prevents crashes when JavaScript extracts a getter/method and calls it
|
|
||||||
// with the wrong `this` (e.g., documentGetter.call(null)).
|
|
||||||
const signature = v8.v8__Signature__New(isolate, template);
|
const signature = v8.v8__Signature__New(isolate, template);
|
||||||
|
|
||||||
const declarations = @typeInfo(JsApi).@"struct".decls;
|
const declarations = @typeInfo(JsApi).@"struct".decls;
|
||||||
@@ -523,7 +558,6 @@ fn attachClass(comptime JsApi: type, isolate: *v8.Isolate, template: *const v8.F
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (value.static) {
|
if (value.static) {
|
||||||
// Static accessors: use Template's SetAccessorProperty
|
|
||||||
v8.v8__Template__SetAccessorProperty(@ptrCast(template), js_name, getter_callback, setter_callback, attribute);
|
v8.v8__Template__SetAccessorProperty(@ptrCast(template), js_name, getter_callback, setter_callback, attribute);
|
||||||
} else {
|
} else {
|
||||||
v8.v8__ObjectTemplate__SetAccessorProperty__Config(prototype, &.{
|
v8.v8__ObjectTemplate__SetAccessorProperty__Config(prototype, &.{
|
||||||
@@ -535,7 +569,6 @@ fn attachClass(comptime JsApi: type, isolate: *v8.Isolate, template: *const v8.F
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
bridge.Function => {
|
bridge.Function => {
|
||||||
// For non-static functions, use the signature to validate the receiver
|
|
||||||
const func_signature = if (value.static) null else signature;
|
const func_signature = if (value.static) null else signature;
|
||||||
const function_template = v8.v8__FunctionTemplate__New__Config(isolate, &.{
|
const function_template = v8.v8__FunctionTemplate__New__Config(isolate, &.{
|
||||||
.callback = value.func,
|
.callback = value.func,
|
||||||
@@ -589,7 +622,7 @@ fn attachClass(comptime JsApi: type, isolate: *v8.Isolate, template: *const v8.F
|
|||||||
bridge.Property => {
|
bridge.Property => {
|
||||||
const js_value = switch (value.value) {
|
const js_value = switch (value.value) {
|
||||||
.null => js.simpleZigValueToJs(.{ .handle = isolate }, null, true, false),
|
.null => js.simpleZigValueToJs(.{ .handle = isolate }, null, true, false),
|
||||||
inline .bool, .int, .float, .string => |v| js.simpleZigValueToJs(.{ .handle = isolate }, v, true, false),
|
inline .bool, .int, .float, .string => |pv| js.simpleZigValueToJs(.{ .handle = isolate }, pv, true, false),
|
||||||
};
|
};
|
||||||
const js_name = v8.v8__String__NewFromUtf8(isolate, name.ptr, v8.kNormal, @intCast(name.len));
|
const js_name = v8.v8__String__NewFromUtf8(isolate, name.ptr, v8.kNormal, @intCast(name.len));
|
||||||
|
|
||||||
@@ -599,11 +632,10 @@ fn attachClass(comptime JsApi: type, isolate: *v8.Isolate, template: *const v8.F
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (value.template) {
|
if (value.template) {
|
||||||
// apply it both to the type itself (e.g. Node.Elem)
|
|
||||||
v8.v8__Template__Set(@ptrCast(template), js_name, js_value, v8.ReadOnly + v8.DontDelete);
|
v8.v8__Template__Set(@ptrCast(template), js_name, js_value, v8.ReadOnly + v8.DontDelete);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
bridge.Constructor => {}, // already handled in generateConstructor
|
bridge.Constructor => {},
|
||||||
else => {},
|
else => {},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -636,30 +668,3 @@ fn attachClass(comptime JsApi: type, isolate: *v8.Isolate, template: *const v8.F
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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.FunctionCallbackInfo) callconv(.c) void {
|
|
||||||
const isolate = v8.v8__FunctionCallbackInfo__GetIsolate(raw_info);
|
|
||||||
log.warn(.js, "Illegal constructor call", .{});
|
|
||||||
|
|
||||||
const message = v8.v8__String__NewFromUtf8(isolate, "Illegal Constructor", v8.kNormal, 19);
|
|
||||||
const js_exception = v8.v8__Exception__TypeError(message);
|
|
||||||
|
|
||||||
_ = v8.v8__Isolate__ThrowException(isolate, js_exception);
|
|
||||||
var return_value: v8.ReturnValue = undefined;
|
|
||||||
v8.v8__FunctionCallbackInfo__GetReturnValue(raw_info, &return_value);
|
|
||||||
v8.v8__ReturnValue__Set(return_value, js_exception);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -550,7 +550,7 @@ fn PrototypeType(comptime T: type) ?type {
|
|||||||
return Struct(std.meta.fieldInfo(T, ._proto).type);
|
return Struct(std.meta.fieldInfo(T, ._proto).type);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn flattenTypes(comptime Types: []const type) [countFlattenedTypes(Types)]type {
|
pub fn flattenTypes(comptime Types: []const type) [countFlattenedTypes(Types)]type {
|
||||||
var index: usize = 0;
|
var index: usize = 0;
|
||||||
var flat: [countFlattenedTypes(Types)]type = undefined;
|
var flat: [countFlattenedTypes(Types)]type = undefined;
|
||||||
for (Types) |T| {
|
for (Types) |T| {
|
||||||
@@ -673,7 +673,8 @@ pub const SubType = enum {
|
|||||||
webassemblymemory,
|
webassemblymemory,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const JsApis = flattenTypes(&.{
|
/// APIs for Page/Window contexts. Used by Snapshot.zig for Page snapshot creation.
|
||||||
|
pub const PageJsApis = flattenTypes(&.{
|
||||||
@import("../webapi/AbortController.zig"),
|
@import("../webapi/AbortController.zig"),
|
||||||
@import("../webapi/AbortSignal.zig"),
|
@import("../webapi/AbortSignal.zig"),
|
||||||
@import("../webapi/CData.zig"),
|
@import("../webapi/CData.zig"),
|
||||||
@@ -866,3 +867,14 @@ pub const JsApis = flattenTypes(&.{
|
|||||||
@import("../webapi/Selection.zig"),
|
@import("../webapi/Selection.zig"),
|
||||||
@import("../webapi/ImageData.zig"),
|
@import("../webapi/ImageData.zig"),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/// APIs that exist only in Worker contexts (not in Page/Window).
|
||||||
|
const WorkerOnlyApis = flattenTypes(&.{
|
||||||
|
@import("../webapi/WorkerGlobalScope.zig"),
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Master list of ALL JS APIs across all contexts.
|
||||||
|
/// Used by Env (class IDs, templates), JsApiLookup, and anywhere that needs
|
||||||
|
/// to know about all possible types. Individual snapshots use their own
|
||||||
|
/// subsets (PageJsApis, WorkerSnapshot.JsApis).
|
||||||
|
pub const JsApis = PageJsApis ++ WorkerOnlyApis;
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ pub const Type = union(enum) {
|
|||||||
generic: void,
|
generic: void,
|
||||||
node: *@import("Node.zig"),
|
node: *@import("Node.zig"),
|
||||||
window: *@import("Window.zig"),
|
window: *@import("Window.zig"),
|
||||||
|
worker_global_scope: *@import("WorkerGlobalScope.zig"),
|
||||||
xhr: *@import("net/XMLHttpRequestEventTarget.zig"),
|
xhr: *@import("net/XMLHttpRequestEventTarget.zig"),
|
||||||
abort_signal: *@import("AbortSignal.zig"),
|
abort_signal: *@import("AbortSignal.zig"),
|
||||||
media_query_list: *@import("css/MediaQueryList.zig"),
|
media_query_list: *@import("css/MediaQueryList.zig"),
|
||||||
@@ -130,6 +131,7 @@ pub fn format(self: *EventTarget, writer: *std.Io.Writer) !void {
|
|||||||
.node => |n| n.format(writer),
|
.node => |n| n.format(writer),
|
||||||
.generic => writer.writeAll("<EventTarget>"),
|
.generic => writer.writeAll("<EventTarget>"),
|
||||||
.window => writer.writeAll("<Window>"),
|
.window => writer.writeAll("<Window>"),
|
||||||
|
.worker_global_scope => writer.writeAll("<WorkerGlobalScope>"),
|
||||||
.xhr => writer.writeAll("<XMLHttpRequestEventTarget>"),
|
.xhr => writer.writeAll("<XMLHttpRequestEventTarget>"),
|
||||||
.abort_signal => writer.writeAll("<AbortSignal>"),
|
.abort_signal => writer.writeAll("<AbortSignal>"),
|
||||||
.media_query_list => writer.writeAll("<MediaQueryList>"),
|
.media_query_list => writer.writeAll("<MediaQueryList>"),
|
||||||
@@ -149,6 +151,7 @@ pub fn toString(self: *EventTarget) []const u8 {
|
|||||||
.node => return "[object Node]",
|
.node => return "[object Node]",
|
||||||
.generic => return "[object EventTarget]",
|
.generic => return "[object EventTarget]",
|
||||||
.window => return "[object Window]",
|
.window => return "[object Window]",
|
||||||
|
.worker_global_scope => return "[object WorkerGlobalScope]",
|
||||||
.xhr => return "[object XMLHttpRequestEventTarget]",
|
.xhr => return "[object XMLHttpRequestEventTarget]",
|
||||||
.abort_signal => return "[object AbortSignal]",
|
.abort_signal => return "[object AbortSignal]",
|
||||||
.media_query_list => return "[object MediaQueryList]",
|
.media_query_list => return "[object MediaQueryList]",
|
||||||
|
|||||||
@@ -429,27 +429,11 @@ pub fn postMessage(self: *Window, message: js.Value.Temp, target_origin: ?[]cons
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn btoa(_: *const Window, input: []const u8, page: *Page) ![]const u8 {
|
pub fn btoa(_: *const Window, input: []const u8, page: *Page) ![]const u8 {
|
||||||
const encoded_len = std.base64.standard.Encoder.calcSize(input.len);
|
return @import("encoding/base64.zig").encode(page.call_arena, input);
|
||||||
const encoded = try page.call_arena.alloc(u8, encoded_len);
|
|
||||||
return std.base64.standard.Encoder.encode(encoded, input);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn atob(_: *const Window, input: []const u8, page: *Page) ![]const u8 {
|
pub fn atob(_: *const Window, input: []const u8, page: *Page) ![]const u8 {
|
||||||
const trimmed = std.mem.trim(u8, input, &std.ascii.whitespace);
|
return @import("encoding/base64.zig").decode(page.call_arena, input);
|
||||||
// Forgiving base64 decode per WHATWG spec:
|
|
||||||
// https://infra.spec.whatwg.org/#forgiving-base64-decode
|
|
||||||
// Remove trailing padding to use standard_no_pad decoder
|
|
||||||
const unpadded = std.mem.trimRight(u8, trimmed, "=");
|
|
||||||
|
|
||||||
// Length % 4 == 1 is invalid (can't represent valid base64)
|
|
||||||
if (unpadded.len % 4 == 1) {
|
|
||||||
return error.InvalidCharacterError;
|
|
||||||
}
|
|
||||||
|
|
||||||
const decoded_len = std.base64.standard_no_pad.Decoder.calcSizeForSlice(unpadded) catch return error.InvalidCharacterError;
|
|
||||||
const decoded = try page.call_arena.alloc(u8, decoded_len);
|
|
||||||
std.base64.standard_no_pad.Decoder.decode(decoded, unpadded) catch return error.InvalidCharacterError;
|
|
||||||
return decoded;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn structuredClone(_: *const Window, value: js.Value) !js.Value {
|
pub fn structuredClone(_: *const Window, value: js.Value) !js.Value {
|
||||||
|
|||||||
135
src/browser/webapi/WorkerGlobalScope.zig
Normal file
135
src/browser/webapi/WorkerGlobalScope.zig
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
// Copyright (C) 2023-2026 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 js = @import("../js/js.zig");
|
||||||
|
|
||||||
|
const base64 = @import("encoding/base64.zig");
|
||||||
|
const Console = @import("Console.zig");
|
||||||
|
const Crypto = @import("Crypto.zig");
|
||||||
|
const EventTarget = @import("EventTarget.zig");
|
||||||
|
const Performance = @import("Performance.zig");
|
||||||
|
|
||||||
|
const WorkerGlobalScope = @This();
|
||||||
|
|
||||||
|
_proto: *EventTarget,
|
||||||
|
_console: Console = .init,
|
||||||
|
_crypto: Crypto = .init,
|
||||||
|
_performance: Performance,
|
||||||
|
_on_error: ?js.Function.Global = null,
|
||||||
|
_on_rejection_handled: ?js.Function.Global = null,
|
||||||
|
_on_unhandled_rejection: ?js.Function.Global = null,
|
||||||
|
|
||||||
|
pub fn asEventTarget(self: *WorkerGlobalScope) *EventTarget {
|
||||||
|
return self._proto;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getSelf(self: *WorkerGlobalScope) *WorkerGlobalScope {
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getConsole(self: *WorkerGlobalScope) *Console {
|
||||||
|
return &self._console;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getCrypto(self: *WorkerGlobalScope) *Crypto {
|
||||||
|
return &self._crypto;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getPerformance(self: *WorkerGlobalScope) *Performance {
|
||||||
|
return &self._performance;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getOnError(self: *const WorkerGlobalScope) ?js.Function.Global {
|
||||||
|
return self._on_error;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn setOnError(self: *WorkerGlobalScope, setter: ?FunctionSetter) void {
|
||||||
|
self._on_error = getFunctionFromSetter(setter);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getOnRejectionHandled(self: *const WorkerGlobalScope) ?js.Function.Global {
|
||||||
|
return self._on_rejection_handled;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn setOnRejectionHandled(self: *WorkerGlobalScope, setter: ?FunctionSetter) void {
|
||||||
|
self._on_rejection_handled = getFunctionFromSetter(setter);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getOnUnhandledRejection(self: *const WorkerGlobalScope) ?js.Function.Global {
|
||||||
|
return self._on_unhandled_rejection;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn setOnUnhandledRejection(self: *WorkerGlobalScope, setter: ?FunctionSetter) void {
|
||||||
|
self._on_unhandled_rejection = getFunctionFromSetter(setter);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn btoa(_: *const WorkerGlobalScope, input: []const u8, exec: *js.Execution) ![]const u8 {
|
||||||
|
return base64.encode(exec.call_arena, input);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn atob(_: *const WorkerGlobalScope, input: []const u8, exec: *js.Execution) ![]const u8 {
|
||||||
|
return base64.decode(exec.call_arena, input);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn structuredClone(_: *const WorkerGlobalScope, value: js.Value) !js.Value {
|
||||||
|
return value.structuredClone();
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: importScripts - needs script loading infrastructure
|
||||||
|
// TODO: location - needs WorkerLocation
|
||||||
|
// TODO: navigator - needs WorkerNavigator
|
||||||
|
// TODO: Timer functions - need scheduler integration
|
||||||
|
|
||||||
|
const FunctionSetter = union(enum) {
|
||||||
|
func: js.Function.Global,
|
||||||
|
anything: js.Value,
|
||||||
|
};
|
||||||
|
|
||||||
|
fn getFunctionFromSetter(setter_: ?FunctionSetter) ?js.Function.Global {
|
||||||
|
const setter = setter_ orelse return null;
|
||||||
|
return switch (setter) {
|
||||||
|
.func => |func| func,
|
||||||
|
.anything => null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const JsApi = struct {
|
||||||
|
pub const bridge = js.Bridge(WorkerGlobalScope);
|
||||||
|
|
||||||
|
pub const Meta = struct {
|
||||||
|
pub const name = "WorkerGlobalScope";
|
||||||
|
pub const prototype_chain = bridge.prototypeChain();
|
||||||
|
pub var class_id: bridge.ClassId = undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const self = bridge.accessor(WorkerGlobalScope.getSelf, null, .{});
|
||||||
|
pub const console = bridge.accessor(WorkerGlobalScope.getConsole, null, .{});
|
||||||
|
pub const crypto = bridge.accessor(WorkerGlobalScope.getCrypto, null, .{});
|
||||||
|
pub const performance = bridge.accessor(WorkerGlobalScope.getPerformance, null, .{});
|
||||||
|
|
||||||
|
pub const onerror = bridge.accessor(WorkerGlobalScope.getOnError, WorkerGlobalScope.setOnError, .{});
|
||||||
|
pub const onrejectionhandled = bridge.accessor(WorkerGlobalScope.getOnRejectionHandled, WorkerGlobalScope.setOnRejectionHandled, .{});
|
||||||
|
pub const onunhandledrejection = bridge.accessor(WorkerGlobalScope.getOnUnhandledRejection, WorkerGlobalScope.setOnUnhandledRejection, .{});
|
||||||
|
|
||||||
|
pub const btoa = bridge.function(WorkerGlobalScope.btoa, .{});
|
||||||
|
pub const atob = bridge.function(WorkerGlobalScope.atob, .{ .dom_exception = true });
|
||||||
|
pub const structuredClone = bridge.function(WorkerGlobalScope.structuredClone, .{});
|
||||||
|
|
||||||
|
// Return false since workers don't have secure-context-only APIs
|
||||||
|
pub const isSecureContext = bridge.property(false, .{ .template = false });
|
||||||
|
};
|
||||||
50
src/browser/webapi/encoding/base64.zig
Normal file
50
src/browser/webapi/encoding/base64.zig
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
// Copyright (C) 2023-2026 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/>.
|
||||||
|
|
||||||
|
//! Base64 encoding/decoding helpers for btoa/atob.
|
||||||
|
//! Used by both Window and WorkerGlobalScope.
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
const Allocator = std.mem.Allocator;
|
||||||
|
|
||||||
|
/// Encodes input to base64 (btoa).
|
||||||
|
pub fn encode(alloc: Allocator, input: []const u8) ![]const u8 {
|
||||||
|
const encoded_len = std.base64.standard.Encoder.calcSize(input.len);
|
||||||
|
const encoded = try alloc.alloc(u8, encoded_len);
|
||||||
|
return std.base64.standard.Encoder.encode(encoded, input);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Decodes base64 input (atob).
|
||||||
|
/// Implements forgiving base64 decode per WHATWG spec.
|
||||||
|
pub fn decode(alloc: Allocator, input: []const u8) ![]const u8 {
|
||||||
|
const trimmed = std.mem.trim(u8, input, &std.ascii.whitespace);
|
||||||
|
// Forgiving base64 decode per WHATWG spec:
|
||||||
|
// https://infra.spec.whatwg.org/#forgiving-base64-decode
|
||||||
|
// Remove trailing padding to use standard_no_pad decoder
|
||||||
|
const unpadded = std.mem.trimRight(u8, trimmed, "=");
|
||||||
|
|
||||||
|
// Length % 4 == 1 is invalid (can't represent valid base64)
|
||||||
|
if (unpadded.len % 4 == 1) {
|
||||||
|
return error.InvalidCharacterError;
|
||||||
|
}
|
||||||
|
|
||||||
|
const decoded_len = std.base64.standard_no_pad.Decoder.calcSizeForSlice(unpadded) catch return error.InvalidCharacterError;
|
||||||
|
const decoded = try alloc.alloc(u8, decoded_len);
|
||||||
|
std.base64.standard_no_pad.Decoder.decode(decoded, unpadded) catch return error.InvalidCharacterError;
|
||||||
|
return decoded;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user