port Snapshot

This commit is contained in:
Karl Seguin
2025-12-31 13:47:56 +08:00
parent 3442f99a49
commit a5038893fe

View File

@@ -41,8 +41,8 @@ const embedded_snapshot_blob = if (@import("build_config").snapshot_path) |path|
// sequence // sequence
data_start: usize, data_start: usize,
// The snapshot data (v8.StartupData is a ptr to the data and len). // The snapshot data (v8.c.StartupData is a ptr to the data and len).
startup_data: v8.StartupData, startup_data: v8.c.StartupData,
// V8 doesn't know how to serialize external references, and pretty much any hook // 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). // into Zig is an external reference (e.g. every accessor and function callback).
@@ -74,8 +74,8 @@ fn loadEmbedded() ?Snapshot {
const data_start = std.mem.readInt(usize, embedded_snapshot_blob[0..@sizeOf(usize)], .little); const data_start = std.mem.readInt(usize, embedded_snapshot_blob[0..@sizeOf(usize)], .little);
const blob = embedded_snapshot_blob[@sizeOf(usize)..]; const blob = embedded_snapshot_blob[@sizeOf(usize)..];
const startup_data = v8.StartupData{ .data = blob.ptr, .raw_size = @intCast(blob.len) }; const startup_data = v8.c.StartupData{ .data = blob.ptr, .raw_size = @intCast(blob.len) };
if (!v8.SnapshotCreator.startupDataIsValid(startup_data)) { if (!v8.c.v8__StartupData__IsValid(startup_data)) {
return null; return null;
} }
@@ -110,45 +110,48 @@ pub fn fromEmbedded(self: Snapshot) bool {
} }
fn isValid(self: Snapshot) bool { fn isValid(self: Snapshot) bool {
return v8.SnapshotCreator.startupDataIsValid(self.startup_data); return v8.c.v8__StartupData__IsValid(self.startup_data);
} }
pub fn createGlobalTemplate(isolate: v8.Isolate, templates: []const v8.FunctionTemplate) v8.ObjectTemplate { pub fn createGlobalTemplate(isolate: v8.Isolate, templates: []const v8.FunctionTemplate) v8.ObjectTemplate {
// Set up the global template to inherit from Window's template // Set up the global template to inherit from Window's template
// This way the global object gets all Window properties through inheritance // This way the global object gets all Window properties through inheritance
const js_global = v8.FunctionTemplate.initDefault(isolate); const js_global = v8.c.v8__FunctionTemplate__New__DEFAULT(isolate);
js_global.setClassName(v8.String.initUtf8(isolate, "Window")); const window_name = v8.c.v8__String__NewFromUtf8(isolate, "Window", v8.c.kNormal, 6);
v8.c.v8__FunctionTemplate__SetClassName(js_global, window_name);
// Find Window in JsApis by name (avoids circular import) // Find Window in JsApis by name (avoids circular import)
const window_index = comptime bridge.JsApiLookup.getId(Window.JsApi); const window_index = comptime bridge.JsApiLookup.getId(Window.JsApi);
js_global.inherit(templates[window_index]); v8.c.v8__FunctionTemplate__Inherit(js_global, templates[window_index]);
return js_global.getInstanceTemplate();
return v8.c.v8__FunctionTemplate__InstanceTemplate(js_global);
} }
pub fn create(allocator: Allocator) !Snapshot { pub fn create(allocator: Allocator) !Snapshot {
var external_references = collectExternalReferences(); var external_references = collectExternalReferences();
var params = v8.initCreateParams(); var params: v8.c.CreateParams = undefined;
params.array_buffer_allocator = v8.createDefaultArrayBufferAllocator(); v8.c.v8__Isolate__CreateParams__CONSTRUCT(&params);
defer v8.destroyArrayBufferAllocator(params.array_buffer_allocator.?); params.array_buffer_allocator = v8.c.v8__ArrayBuffer__Allocator__NewDefaultAllocator();
defer v8.c.v8__ArrayBuffer__Allocator__DELETE(params.array_buffer_allocator.?);
params.external_references = @ptrCast(&external_references); params.external_references = @ptrCast(&external_references);
var snapshot_creator: v8.SnapshotCreator = undefined; const snapshot_creator = v8.c.v8__SnapshotCreator__CREATE(&params);
v8.SnapshotCreator.init(&snapshot_creator, &params); defer v8.c.v8__SnapshotCreator__DESTRUCT(snapshot_creator);
defer snapshot_creator.deinit();
var data_start: usize = 0; var data_start: usize = 0;
const isolate = snapshot_creator.getIsolate(); const isolate = v8.c.v8__SnapshotCreator__getIsolate(snapshot_creator).?;
{ {
// CreateBlob, which we'll call once everything is setup, MUST NOT // CreateBlob, which we'll call once everything is setup, MUST NOT
// be called from an active HandleScope. Hence we have this scope to // be called from an active HandleScope. Hence we have this scope to
// clean it up before we call CreateBlob // clean it up before we call CreateBlob
var handle_scope: v8.HandleScope = undefined; var handle_scope: v8.c.HandleScope = undefined;
v8.HandleScope.init(&handle_scope, isolate); v8.c.v8__HandleScope__CONSTRUCT(&handle_scope, isolate);
defer handle_scope.deinit(); defer v8.c.v8__HandleScope__DESTRUCT(&handle_scope);
// Create templates (constructors only) FIRST // Create templates (constructors only) FIRST
var templates: [JsApis.len]v8.FunctionTemplate = undefined; var templates: [JsApis.len]*v8.c.FunctionTemplate = undefined;
inline for (JsApis, 0..) |JsApi, i| { inline for (JsApis, 0..) |JsApi, i| {
@setEvalBranchQuota(10_000); @setEvalBranchQuota(10_000);
templates[i] = generateConstructor(JsApi, isolate); templates[i] = generateConstructor(JsApi, isolate);
@@ -159,23 +162,23 @@ pub fn create(allocator: Allocator) !Snapshot {
// This must come before attachClass so inheritance is set up first // 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| {
templates[i].inherit(templates[proto_index]); v8.c.v8__FunctionTemplate__Inherit(templates[i], templates[proto_index]);
} }
} }
// Set up the global template to inherit from Window's template // Set up the global template to inherit from Window's template
// This way the global object gets all Window properties through inheritance // This way the global object gets all Window properties through inheritance
const global_template = createGlobalTemplate(isolate, templates[0..]);
const context = v8.Context.init(isolate, global_template, null); const global_template = createGlobalTemplate(isolate, templates[0..]);
context.enter(); const context = v8.c.v8__Context__New(isolate, global_template, null);
defer context.exit(); v8.c.v8__Context__Enter(context);
defer v8.c.v8__Context__Exit(context);
// Add templates to context snapshot // Add 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);
const data_index = snapshot_creator.addDataWithContext(context, @ptrCast(templates[i].handle)); const data_index = v8.c.v8__SnapshotCreator__AddData2(snapshot_creator, context, @ptrCast(templates[i]));
if (i == 0) { if (i == 0) {
data_start = data_index; data_start = data_index;
last_data_index = data_index; last_data_index = data_index;
@@ -193,16 +196,18 @@ pub fn create(allocator: Allocator) !Snapshot {
} }
// Realize all templates by getting their functions and attaching to global // Realize all templates by getting their functions and attaching to global
const global_obj = context.getGlobal(); const global_obj = v8.c.v8__Context__Global(context);
inline for (JsApis, 0..) |JsApi, i| { inline for (JsApis, 0..) |JsApi, i| {
const func = templates[i].getFunction(context); const func = v8.c.v8__FunctionTemplate__GetFunction(templates[i], context);
// Attach to global if it has a name // 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 v8_class_name = v8.String.initUtf8(isolate, JsApi.Meta.constructor_alias); const alias = JsApi.Meta.constructor_alias;
_ = global_obj.setValue(context, v8_class_name, func); const v8_class_name = v8.c.v8__String__NewFromUtf8(isolate, alias.ptr, v8.c.kNormal, @intCast(alias.len));
var maybe_result: v8.c.MaybeBool = undefined;
v8.c.v8__Object__Set(global_obj, context, v8_class_name, func, &maybe_result);
// @TODO: This is wrong. This name should be registered with the // @TODO: This is wrong. This name should be registered with the
// illegalConstructorCallback. I.e. new Image() is OK, but // illegalConstructorCallback. I.e. new Image() is OK, but
@@ -210,11 +215,15 @@ pub fn create(allocator: Allocator) !Snapshot {
// But we _have_ to register the name, i.e. HTMLImageElement // But we _have_ to register the name, i.e. HTMLImageElement
// has to be registered so, for now, instead of creating another // has to be registered so, for now, instead of creating another
// template, we just hook it into the constructor. // template, we just hook it into the constructor.
const illegal_class_name = v8.String.initUtf8(isolate, JsApi.Meta.name); const name = JsApi.Meta.name;
_ = global_obj.setValue(context, illegal_class_name, func); const illegal_class_name = v8.c.v8__String__NewFromUtf8(isolate, name.ptr, v8.c.kNormal, @intCast(name.len));
var maybe_result2: v8.c.MaybeBool = undefined;
v8.c.v8__Object__Set(global_obj, context, illegal_class_name, func, &maybe_result2);
} else { } else {
const v8_class_name = v8.String.initUtf8(isolate, JsApi.Meta.name); const name = JsApi.Meta.name;
_ = global_obj.setValue(context, v8_class_name, func); const v8_class_name = v8.c.v8__String__NewFromUtf8(isolate, name.ptr, v8.c.kNormal, @intCast(name.len));
var maybe_result: v8.c.MaybeBool = undefined;
v8.c.v8__Object__Set(global_obj, context, v8_class_name, func, &maybe_result);
} }
} }
} }
@@ -222,8 +231,10 @@ pub fn create(allocator: Allocator) !Snapshot {
{ {
// If we want to overwrite the built-in console, we have to // If we want to overwrite the built-in console, we have to
// delete the built-in one. // delete the built-in one.
const console_key = v8.String.initUtf8(isolate, "console"); const console_key = v8.c.v8__String__NewFromUtf8(isolate, "console", v8.c.kNormal, 7);
if (global_obj.deleteValue(context, console_key) == false) { var maybe_deleted: v8.c.MaybeBool = undefined;
v8.c.v8__Object__Delete(global_obj, context, console_key, &maybe_deleted);
if (maybe_deleted.value == false) {
return error.ConsoleDeleteError; return error.ConsoleDeleteError;
} }
} }
@@ -233,23 +244,30 @@ pub fn create(allocator: Allocator) !Snapshot {
// TODO: see if newer V8 engines have a way around this. // 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_obj = templates[proto_index].getFunction(context).toObject(); const proto_func = v8.c.v8__FunctionTemplate__GetFunction(templates[proto_index], context);
const self_obj = templates[i].getFunction(context).toObject(); const proto_obj: *const v8.c.Object = @ptrCast(proto_func);
_ = self_obj.setPrototype(context, proto_obj);
const self_func = v8.c.v8__FunctionTemplate__GetFunction(templates[i], context);
const self_obj: *const v8.c.Object = @ptrCast(self_func);
var maybe_result: v8.c.MaybeBool = undefined;
v8.c.v8__Object__SetPrototype(self_obj, context, proto_obj, &maybe_result);
} }
} }
{ {
// Custom exception // Custom exception
// TODO: this is an horrible hack, I can't figure out how to do this cleanly. // 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"); const code_str = "DOMException.prototype.__proto__ = Error.prototype";
_ = try (try v8.Script.compile(context, code, null)).run(context); const code = v8.c.v8__String__NewFromUtf8(isolate, code_str.ptr, v8.c.kNormal, @intCast(code_str.len));
const script = v8.c.v8__Script__Compile(context, code, null) orelse return error.ScriptCompileFailed;
_ = v8.c.v8__Script__Run(script, context) orelse return error.ScriptRunFailed;
} }
snapshot_creator.setDefaultContext(context); v8.c.v8__SnapshotCreator__setDefaultContext(snapshot_creator, context);
} }
const blob = snapshot_creator.createBlob(v8.FunctionCodeHandling.kKeep); const blob = v8.c.v8__SnapshotCreator__createBlob(snapshot_creator, v8.c.kKeep);
const owned = try allocator.dupe(u8, blob.data[0..@intCast(blob.raw_size)]); const owned = try allocator.dupe(u8, blob.data[0..@intCast(blob.raw_size)]);
return .{ return .{
@@ -365,7 +383,7 @@ fn collectExternalReferences() [countExternalReferences()]isize {
// via `new ClassName()` - but they could, for example, be created in // via `new ClassName()` - but they could, for example, be created in
// Zig and returned from a function call, which is why we need the // Zig and returned from a function call, which is why we need the
// FunctionTemplate. // FunctionTemplate.
fn generateConstructor(comptime JsApi: type, isolate: v8.Isolate) v8.FunctionTemplate { fn generateConstructor(comptime JsApi: type, isolate: *v8.c.Isolate) *v8.c.FunctionTemplate {
const callback = blk: { const callback = blk: {
if (@hasDecl(JsApi, "constructor")) { if (@hasDecl(JsApi, "constructor")) {
break :blk JsApi.constructor.func; break :blk JsApi.constructor.func;
@@ -375,19 +393,22 @@ fn generateConstructor(comptime JsApi: type, isolate: v8.Isolate) v8.FunctionTem
break :blk illegalConstructorCallback; break :blk illegalConstructorCallback;
}; };
const template = v8.FunctionTemplate.initCallback(isolate, callback); const template = @constCast(v8.c.v8__FunctionTemplate__New__DEFAULT2(isolate, callback).?);
if (!@hasDecl(JsApi.Meta, "empty_with_no_proto")) { if (!@hasDecl(JsApi.Meta, "empty_with_no_proto")) {
template.getInstanceTemplate().setInternalFieldCount(1); const instance_template = v8.c.v8__FunctionTemplate__InstanceTemplate(template);
v8.c.v8__ObjectTemplate__SetInternalFieldCount(instance_template, 1);
} }
const class_name = v8.String.initUtf8(isolate, if (@hasDecl(JsApi.Meta, "name")) JsApi.Meta.name else @typeName(JsApi)); const name_str = if (@hasDecl(JsApi.Meta, "name")) JsApi.Meta.name else @typeName(JsApi);
template.setClassName(class_name); const class_name = v8.c.v8__String__NewFromUtf8(isolate, name_str.ptr, v8.c.kNormal, @intCast(name_str.len));
v8.c.v8__FunctionTemplate__SetClassName(template, class_name);
return template; return template;
} }
// Attaches JsApi members to the prototype template (normal case) // 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(); fn attachClass(comptime JsApi: type, isolate: *v8.Isolate, template: *v8.FunctionTemplate) void {
const instance = template.getInstanceTemplate(); const target = v8.c.v8__FunctionTemplate__PrototypeTemplate(template);
const instance = v8.c.v8__FunctionTemplate__InstanceTemplate(template);
const declarations = @typeInfo(JsApi).@"struct".decls; const declarations = @typeInfo(JsApi).@"struct".decls;
inline for (declarations) |d| { inline for (declarations) |d| {
@@ -397,60 +418,80 @@ fn attachClass(comptime JsApi: type, isolate: v8.Isolate, template: v8.FunctionT
switch (definition) { switch (definition) {
bridge.Accessor => { bridge.Accessor => {
const js_name = v8.String.initUtf8(isolate, name).toName(); const js_name = v8.c.v8__String__NewFromUtf8(isolate, name.ptr, v8.c.kNormal, @intCast(name.len));
const getter_callback = v8.FunctionTemplate.initCallback(isolate, value.getter); const getter_callback = @constCast(v8.c.v8__FunctionTemplate__New__DEFAULT2(isolate, value.getter).?);
if (value.setter == null) { if (value.setter == null) {
if (value.static) { if (value.static) {
template.setAccessorGetter(js_name, getter_callback); v8.c.v8__Template__SetAccessorProperty__DEFAULT(@ptrCast(template), js_name, getter_callback);
} else { } else {
target.setAccessorGetter(js_name, getter_callback); v8.c.v8__ObjectTemplate__SetAccessorProperty__DEFAULT(target, js_name, getter_callback);
} }
} else { } else {
std.debug.assert(value.static == false); std.debug.assert(value.static == false);
const setter_callback = v8.FunctionTemplate.initCallback(isolate, value.setter); const setter_callback = @constCast(v8.c.v8__FunctionTemplate__New__DEFAULT2(isolate, value.setter.?).?);
target.setAccessorGetterAndSetter(js_name, getter_callback, setter_callback); v8.c.v8__ObjectTemplate__SetAccessorProperty__DEFAULT2(target, js_name, getter_callback, setter_callback);
} }
}, },
bridge.Function => { bridge.Function => {
const function_template = v8.FunctionTemplate.initCallback(isolate, value.func); const function_template = @constCast(v8.c.v8__FunctionTemplate__New__DEFAULT2(isolate, value.func).?);
const js_name = v8.String.initUtf8(isolate, name).toName(); const js_name = v8.c.v8__String__NewFromUtf8(isolate, name.ptr, v8.c.kNormal, @intCast(name.len));
if (value.static) { if (value.static) {
template.set(js_name, function_template, v8.PropertyAttribute.None); v8.c.v8__Template__Set(@ptrCast(template), js_name, @ptrCast(function_template), v8.c.None);
} else { } else {
target.set(js_name, function_template, v8.PropertyAttribute.None); v8.c.v8__Template__Set(@ptrCast(target), js_name, @ptrCast(function_template), v8.c.None);
} }
}, },
bridge.Indexed => { bridge.Indexed => {
const configuration = v8.IndexedPropertyHandlerConfiguration{ var configuration: v8.c.IndexedPropertyHandlerConfiguration = .{
.getter = value.getter, .getter = value.getter,
.setter = null,
.query = null,
.deleter = null,
.enumerator = null,
.definer = null,
.descriptor = null,
.data = null,
.flags = 0,
}; };
instance.setIndexedProperty(configuration, null); instance.setIndexedProperty(configuration, null);
}, },
bridge.NamedIndexed => instance.setNamedProperty(.{ bridge.NamedIndexed => {
.getter = value.getter, const instance_template = v8.c.v8__FunctionTemplate__InstanceTemplate(template);
.setter = value.setter, var configuration: v8.c.NamedPropertyHandlerConfiguration = .{
.deleter = value.deleter, .getter = value.getter,
.flags = v8.PropertyHandlerFlags.OnlyInterceptStrings | v8.PropertyHandlerFlags.NonMasking, .setter = value.setter,
}, null), .query = null,
.deleter = value.deleter,
.enumerator = null,
.definer = null,
.descriptor = null,
.data = null,
.flags = v8.c.kOnlyInterceptStrings | v8.c.kNonMasking,
};
v8.c.v8__ObjectTemplate__SetNamedHandler(instance, &configuration);
},
bridge.Iterator => { bridge.Iterator => {
const function_template = v8.FunctionTemplate.initCallback(isolate, value.func); const function_template = @constCast(v8.c.v8__FunctionTemplate__New__DEFAULT2(isolate, value.func).?);
const js_name = if (value.async) const js_name = if (value.async)
v8.Symbol.getAsyncIterator(isolate).toName() v8.c.v8__Symbol__GetAsyncIterator(isolate)
else else
v8.Symbol.getIterator(isolate).toName(); v8.c.v8__Symbol__GetIterator(isolate);
target.set(js_name, function_template, v8.PropertyAttribute.None); v8.c.v8__Template__Set(@ptrCast(target), js_name, @ptrCast(function_template), v8.c.None);
}, },
bridge.Property => { bridge.Property => {
const js_value = switch (value) { // simpleZigValueToJs still uses old v8.Isolate wrapper, so create a temp wrapper
.int => |v| js.simpleZigValueToJs(isolate, v, true, false), const iso_wrapper = v8.Isolate{ .handle = isolate };
const js_value_wrapper = switch (value) {
.int => |v| js.simpleZigValueToJs(iso_wrapper, v, true, false),
}; };
const js_value = js_value_wrapper.handle;
const js_name = v8.String.initUtf8(isolate, name).toName(); const js_name = v8.c.v8__String__NewFromUtf8(isolate, name.ptr, v8.c.kNormal, @intCast(name.len));
// apply it both to the type itself // apply it both to the type itself
template.set(js_name, js_value, v8.PropertyAttribute.ReadOnly + v8.PropertyAttribute.DontDelete); v8.c.v8__Template__Set(@ptrCast(template), js_name, js_value, v8.c.ReadOnly + v8.c.DontDelete);
// and to instances of the type // and to instances of the type
target.set(js_name, js_value, v8.PropertyAttribute.ReadOnly + v8.PropertyAttribute.DontDelete); v8.c.v8__Template__Set(@ptrCast(target), js_name, js_value, v8.c.ReadOnly + v8.c.DontDelete);
}, },
bridge.Constructor => {}, // already handled in generateConstructor bridge.Constructor => {}, // already handled in generateConstructor
else => {}, else => {},
@@ -458,8 +499,8 @@ fn attachClass(comptime JsApi: type, isolate: v8.Isolate, template: v8.FunctionT
} }
if (@hasDecl(JsApi.Meta, "htmldda")) { if (@hasDecl(JsApi.Meta, "htmldda")) {
instance.markAsUndetectable(); v8.c.v8__ObjectTemplate__MarkAsUndetectable(instance);
instance.setCallAsFunctionHandler(JsApi.Meta.callable.func); v8.c.v8__ObjectTemplate__SetCallAsFunctionHandler(instance, JsApi.Meta.callable.func);
} }
if (@hasDecl(JsApi.Meta, "name")) { if (@hasDecl(JsApi.Meta, "name")) {
@@ -482,10 +523,13 @@ fn protoIndexLookup(comptime JsApi: type) ?bridge.JsApiLookup.BackingInt {
} }
// Shared illegal constructor callback for types without explicit constructors // Shared illegal constructor callback for types without explicit constructors
fn illegalConstructorCallback(raw_info: ?*const v8.C_FunctionCallbackInfo) callconv(.c) void { fn illegalConstructorCallback(raw_info: ?*const v8.c.FunctionCallbackInfo) callconv(.c) void {
const info = v8.FunctionCallbackInfo.initFromV8(raw_info); const isolate = v8.c.v8__FunctionCallbackInfo__GetIsolate(raw_info);
const iso = info.getIsolate();
log.warn(.js, "Illegal constructor call", .{}); log.warn(.js, "Illegal constructor call", .{});
const js_exception = iso.throwException(js._createException(iso, "Illegal Constructor")); const message = v8.c.v8__String__NewFromUtf8(isolate, "Illegal Constructor", v8.c.kNormal, 19);
info.getReturnValue().set(js_exception); const js_exception = v8.c.v8__Exception__TypeError(message);
_ = v8.c.v8__Isolate__ThrowException(isolate, js_exception);
var return_value: v8.c.ReturnValue = undefined;
v8.c.v8__FunctionCallbackInfo__GetReturnValue(raw_info, &return_value);
v8.c.v8__ReturnValue__Set(return_value, js_exception);
} }