// Copyright (C) 2023-2024 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 v8 = @import("v8"); const log = @import("../log.zig"); const SubType = @import("subtype.zig").SubType; const ScriptManager = @import("../browser/ScriptManager.zig"); const Allocator = std.mem.Allocator; const ArenaAllocator = std.heap.ArenaAllocator; const CALL_ARENA_RETAIN = 1024 * 16; const CONTEXT_ARENA_RETAIN = 1024 * 64; const js = @This(); // Global, should only be initialized once. pub const Platform = struct { inner: v8.Platform, pub fn init() !Platform { if (v8.initV8ICU() == false) { return error.FailedToInitializeICU; } const platform = v8.Platform.initDefault(0, true); v8.initV8Platform(platform); v8.initV8(); return .{ .inner = platform }; } pub fn deinit(self: Platform) void { _ = v8.deinitV8(); v8.deinitV8Platform(); self.inner.deinit(); } }; // 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. // The `S` parameter is arbitrary state. When we start an ExecutionWorld, an instance // of S must be given. This instance is available to any Zig binding. // The `types` parameter is a tuple of Zig structures we want to bind to V8. pub fn Env(comptime State: type, comptime WebApis: type) type { const Types = @typeInfo(WebApis.Interfaces).@"struct".fields; // Imagine we have a type Cat which has a getter: // // fn get_owner(self: *Cat) *Owner { // return self.owner; // } // // When we execute caller.getter, we'll end up doing something like: // const res = @call(.auto, Cat.get_owner, .{cat_instance}); // // How do we turn `res`, which is an *Owner, into something we can return // to v8? We need the ObjectTemplate associated with Owner. How do we // get that? Well, we store all the ObjectTemplates in an array that's // tied to env. So we do something like: // // env.templates[index_of_owner].initInstance(...); // // But how do we get that `index_of_owner`? `TypeLookup` is a struct // that looks like: // // const TypeLookup = struct { // comptime cat: usize = 0, // comptime owner: usize = 1, // ... // } // // So to get the template index of `owner`, we can do: // // const index_id = @field(type_lookup, @typeName(@TypeOf(res)); // const TypeLookup = comptime blk: { var fields: [Types.len]std.builtin.Type.StructField = undefined; for (Types, 0..) |s, i| { // This prototype type check has nothing to do with building our // TypeLookup. But we put it here, early, so that the rest of the // code doesn't have to worry about checking if Struct.prototype is // a pointer. const Struct = s.defaultValue().?; if (@hasDecl(Struct, "prototype") and @typeInfo(Struct.prototype) != .pointer) { @compileError(std.fmt.comptimePrint("Prototype '{s}' for type '{s} must be a pointer", .{ @typeName(Struct.prototype), @typeName(Struct) })); } fields[i] = .{ .name = @typeName(Receiver(Struct)), .type = usize, .is_comptime = true, .alignment = @alignOf(usize), .default_value_ptr = &i, }; } break :blk @Type(.{ .@"struct" = .{ .layout = .auto, .decls = &.{}, .is_tuple = false, .fields = &fields, } }); }; // Creates a list where the index of a type contains its prototype index // const Animal = struct{}; // const Cat = struct{ // pub const prototype = *Animal; // }; // // Would create an array: [0, 0] // Animal, at index, 0, has no prototype, so we set it to itself // Cat, at index 1, has an Animal prototype, so we set it to 0. // // When we're trying to pass an argument to a Zig function, we'll know the // target type (the function parameter type), and we'll have a // TaggedAnyOpaque which will have the index of the type of that parameter. // We'll use the PROTOTYPE_TABLE to see if the TaggedAnyType should be // cast to a prototype. const PROTOTYPE_TABLE = comptime blk: { var table: [Types.len]u16 = undefined; const TYPE_LOOKUP = TypeLookup{}; for (Types, 0..) |s, i| { var prototype_index = i; const Struct = s.defaultValue().?; if (@hasDecl(Struct, "prototype")) { const TI = @typeInfo(Struct.prototype); const proto_name = @typeName(Receiver(TI.pointer.child)); prototype_index = @field(TYPE_LOOKUP, proto_name); } table[i] = prototype_index; } break :blk table; }; return struct { allocator: Allocator, platform: *const Platform, // the global isolate 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 TYPE_LOOKUP and then have // access to its TunctionTemplate (the thing we need to create an instance // of it) // I.e.: // const index = @field(TYPE_LOOKUP, @typeName(type_name)) // const template = templates[index]; templates: [Types.len]v8.FunctionTemplate, // Given a type index (retrieved via the TYPE_LOOKUP), we can retrieve // the index of its prototype. Types without a prototype have their own // index. prototype_lookup: [Types.len]u16, meta_lookup: [Types.len]TypeMeta, context_id: usize, const Self = @This(); const TYPE_LOOKUP = TypeLookup{}; const Opts = struct {}; pub fn init(allocator: Allocator, platform: *const Platform, _: Opts) !*Self { // var params = v8.initCreateParams(); var params = try allocator.create(v8.CreateParams); errdefer allocator.destroy(params); v8.c.v8__Isolate__CreateParams__CONSTRUCT(params); params.array_buffer_allocator = v8.createDefaultArrayBufferAllocator(); errdefer v8.destroyArrayBufferAllocator(params.array_buffer_allocator.?); var isolate = v8.Isolate.init(params); errdefer isolate.deinit(); // This is the callback that runs whenever a module is dynamically imported. isolate.setHostImportModuleDynamicallyCallback(JsContext.dynamicModuleCallback); isolate.setPromiseRejectCallback(promiseRejectCallback); isolate.setMicrotasksPolicy(v8.c.kExplicit); isolate.enter(); errdefer isolate.exit(); isolate.setHostInitializeImportMetaObjectCallback(struct { fn callback(c_context: ?*v8.C_Context, c_module: ?*v8.C_Module, c_meta: ?*v8.C_Value) callconv(.c) void { const v8_context = v8.Context{ .handle = c_context.? }; const js_context: *JsContext = @ptrFromInt(v8_context.getEmbedderData(1).castTo(v8.BigInt).getUint64()); js_context.initializeImportMeta(v8.Module{ .handle = c_module.? }, v8.Object{ .handle = c_meta.? }) catch |err| { log.err(.js, "import meta", .{ .err = err }); }; } }.callback); var temp_scope: v8.HandleScope = undefined; v8.HandleScope.init(&temp_scope, isolate); defer temp_scope.deinit(); const env = try allocator.create(Self); errdefer allocator.destroy(env); env.* = .{ .context_id = 0, .platform = platform, .isolate = isolate, .templates = undefined, .allocator = allocator, .isolate_params = params, .meta_lookup = undefined, .prototype_lookup = undefined, }; // 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(TYPE_LOOKUP, type_name) const templates = &env.templates; inline for (Types, 0..) |s, i| { @setEvalBranchQuota(10_000); templates[i] = v8.Persistent(v8.FunctionTemplate).init(isolate, generateClass(s.defaultValue().?, isolate)).castToFunctionTemplate(); } // Above, we've created all our our FunctionTemplates. Now that we // have them all, we can hook up the prototypes. const meta_lookup = &env.meta_lookup; inline for (Types, 0..) |s, i| { const Struct = s.defaultValue().?; if (@hasDecl(Struct, "prototype")) { const TI = @typeInfo(Struct.prototype); const proto_name = @typeName(Receiver(TI.pointer.child)); if (@hasField(TypeLookup, proto_name) == false) { @compileError(std.fmt.comptimePrint("Prototype '{s}' for '{s}' is undefined", .{ proto_name, @typeName(Struct) })); } // Hey, look! This is our first real usage of the TYPE_LOOKUP. // Just like we said above, given a type, we can get its // template index. const proto_index = @field(TYPE_LOOKUP, proto_name); templates[i].inherit(templates[proto_index]); } // while we're here, let's populate our meta lookup const subtype: ?SubType = if (@hasDecl(Struct, "subtype")) Struct.subtype else null; const proto_offset = comptime blk: { if (!@hasField(Struct, "proto")) { break :blk 0; } const proto_info = std.meta.fieldInfo(Struct, .proto); if (@typeInfo(proto_info.type) == .pointer) { // we store the offset as a negative, to so that, // when we reverse this, we know that it's // behind a pointer that we need to resolve. break :blk -@offsetOf(Struct, "proto"); } break :blk @offsetOf(Struct, "proto"); }; meta_lookup[i] = .{ .index = i, .subtype = subtype, .proto_offset = proto_offset, }; } return env; } pub fn deinit(self: *Self) void { self.isolate.exit(); self.isolate.deinit(); v8.destroyArrayBufferAllocator(self.isolate_params.array_buffer_allocator.?); self.allocator.destroy(self.isolate_params); self.allocator.destroy(self); } pub fn newInspector(self: *Self, arena: Allocator, ctx: anytype) !Inspector { return Inspector.init(arena, self.isolate, ctx); } pub fn runMicrotasks(self: *const Self) void { self.isolate.performMicrotasksCheckpoint(); } pub fn pumpMessageLoop(self: *const Self) bool { return self.platform.inner.pumpMessageLoop(self.isolate, false); } pub fn runIdleTasks(self: *const Self) void { return self.platform.inner.runIdleTasks(self.isolate, 1); } pub fn newExecutionWorld(self: *Self) !ExecutionWorld { return .{ .env = self, .js_context = null, .call_arena = ArenaAllocator.init(self.allocator), .context_arena = ArenaAllocator.init(self.allocator), }; } // V8 doesn't immediately free memory associated with // a Context, it's managed by the garbage collector. We use the // `lowMemoryNotification` call on the isolate to encourage v8 to free // any contexts which have been freed. pub fn lowMemoryNotification(self: *Self) void { var handle_scope: v8.HandleScope = undefined; v8.HandleScope.init(&handle_scope, self.isolate); defer handle_scope.deinit(); self.isolate.lowMemoryNotification(); } pub fn dumpMemoryStats(self: *Self) void { const stats = self.isolate.getHeapStatistics(); std.debug.print( \\ Total Heap Size: {d} \\ Total Heap Size Executable: {d} \\ Total Physical Size: {d} \\ Total Available Size: {d} \\ Used Heap Size: {d} \\ Heap Size Limit: {d} \\ Malloced Memory: {d} \\ External Memory: {d} \\ Peak Malloced Memory: {d} \\ Number Of Native Contexts: {d} \\ Number Of Detached Contexts: {d} \\ Total Global Handles Size: {d} \\ Used Global Handles Size: {d} \\ Zap Garbage: {any} \\ , .{ stats.total_heap_size, stats.total_heap_size_executable, stats.total_physical_size, stats.total_available_size, stats.used_heap_size, stats.heap_size_limit, stats.malloced_memory, stats.external_memory, stats.peak_malloced_memory, stats.number_of_native_contexts, stats.number_of_detached_contexts, stats.total_global_handles_size, stats.used_global_handles_size, stats.does_zap_garbage }); } fn promiseRejectCallback(v8_msg: v8.C_PromiseRejectMessage) callconv(.c) void { const msg = v8.PromiseRejectMessage.initFromC(v8_msg); const isolate = msg.getPromise().toObject().getIsolate(); const v8_context = isolate.getCurrentContext(); const context: *JsContext = @ptrFromInt(v8_context.getEmbedderData(1).castTo(v8.BigInt).getUint64()); const value = if (msg.getValue()) |v8_value| valueToString(context.call_arena, v8_value, isolate, v8_context) catch |err| @errorName(err) else "no value"; log.debug(.js, "unhandled rejection", .{ .value = value }); } // 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 pub const ExecutionWorld = struct { env: *Self, // Arena whose lifetime is for a single getter/setter/function/etc. // Largely used to get strings out of V8, like a stack trace from // a TryCatch. The allocator will be owned by the JsContext, but the // arena itself is owned by the ExecutionWorld so that we can re-use it // from context to context. call_arena: ArenaAllocator, // Arena whose lifetime is for a single page load. Where // the call_arena lives for a single function call, the context_arena // lives for the lifetime of the entire page. The allocator will be // owned by the JsContext, but the arena itself is owned by the ExecutionWorld // so that we can re-use it from context to context. context_arena: ArenaAllocator, // Currently a context maps to a Browser's Page. Here though, it's only a // mechanism to organization page-specific memory. The ExecutionWorld // does all the work, but having all page-specific data structures // grouped together helps keep things clean. js_context: ?JsContext = null, // no init, must be initialized via env.newExecutionWorld() pub fn deinit(self: *ExecutionWorld) void { if (self.js_context != null) { self.removeJsContext(); } self.call_arena.deinit(); self.context_arena.deinit(); } // Only the top JsContext in the Main ExecutionWorld should hold a handle_scope. // A v8.HandleScope is like an arena. Once created, any "Local" that // v8 creates will be released (or at least, releasable by the v8 GC) // 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 createJsContext(self: *ExecutionWorld, global: anytype, state: State, script_manager: ?*ScriptManager, enter: bool, global_callback: ?GlobalMissingCallback) !*JsContext { std.debug.assert(self.js_context == null); const env = self.env; const isolate = env.isolate; const Global = @TypeOf(global.*); 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); attachClass(Global, 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 _isolate = info.getIsolate(); const v8_context = _isolate.getCurrentContext(); const js_context: *JsContext = @ptrFromInt(v8_context.getEmbedderData(1).castTo(v8.BigInt).getUint64()); const property = valueToString(js_context.call_arena, .{ .handle = c_name.? }, _isolate, v8_context) catch "???"; if (js_context.global_callback.?.missing(property, js_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 (Types, 0..) |s, i| { const Struct = s.defaultValue().?; const class_name = v8.String.initUtf8(isolate, comptime classNameForStruct(Struct)); global_template.set(class_name.toName(), templates[i], v8.PropertyAttribute.None); } // The global object (Window) has already been hooked into the v8 // engine when the Env was initialized - like every other type. // But the V8 global is its own FunctionTemplate instance so even // though it's also a Window, we need to set the prototype for this // specific instance of the the Window. if (@hasDecl(Global, "prototype")) { const proto_type = Receiver(@typeInfo(Global.prototype).pointer.child); const proto_name = @typeName(proto_type); const proto_index = @field(TYPE_LOOKUP, proto_name); js_global.inherit(templates[proto_index]); } const context_local = v8.Context.init(isolate, global_template, 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 (Types, 0..) |s, i| { const Struct = s.defaultValue().?; if (@hasDecl(Struct, "prototype")) { const proto_type = Receiver(@typeInfo(Struct.prototype).pointer.child); const proto_name = @typeName(proto_type); if (@hasField(TypeLookup, proto_name) == false) { @compileError("Type '" ++ @typeName(Struct) ++ "' defines an unknown prototype: " ++ proto_name); } const proto_index = @field(TYPE_LOOKUP, proto_name); 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; }; // For a Page we only create one HandleScope, it is stored in the main World (enter==true). A page can have multple contexts, 1 for each World. // The main Context that enters and holds the HandleScope should therefore always be created first. Following other worlds for this page // like isolated Worlds, will thereby place their objects on the main page's HandleScope. Note: In the furure the number of context will multiply multiple frames support var handle_scope: ?v8.HandleScope = null; if (enter) { handle_scope = @as(v8.HandleScope, undefined); v8.HandleScope.init(&handle_scope.?, isolate); } errdefer if (enter) handle_scope.?.deinit(); { // If we want to overwrite the built-in console, we have to // delete the built-in one. const js_obj = v8_context.getGlobal(); const console_key = v8.String.initUtf8(isolate, "console"); if (js_obj.deleteValue(v8_context, console_key) == false) { return error.ConsoleDeleteError; } } const context_id = env.context_id; env.context_id = context_id + 1; self.js_context = JsContext{ .state = state, .id = context_id, .isolate = isolate, .v8_context = v8_context, .templates = &env.templates, .meta_lookup = &env.meta_lookup, .handle_scope = handle_scope, .script_manager = script_manager, .call_arena = self.call_arena.allocator(), .context_arena = self.context_arena.allocator(), .global_callback = global_callback, }; var js_context = &self.js_context.?; { // Given a context, we can get our executor. // (we store a pointer to our executor in the context's // embeddeder data) const data = isolate.initBigIntU64(@intCast(@intFromPtr(js_context))); v8_context.setEmbedderData(1, data); } { // Not the prettiest but we want to make the `call_arena` // optionally available to the WebAPIs. If `state` has a // call_arena field, fill-it in now. const state_type_info = @typeInfo(@TypeOf(state)); if (state_type_info == .pointer and @hasField(state_type_info.pointer.child, "call_arena")) { js_context.state.call_arena = js_context.call_arena; } } // Custom exception // NOTE: there is no way in v8 to subclass the Error built-in type // TODO: this is an horrible hack inline for (Types) |s| { const Struct = s.defaultValue().?; if (@hasDecl(Struct, "ErrorSet")) { const script = comptime classNameForStruct(Struct) ++ ".prototype.__proto__ = Error.prototype"; _ = try js_context.exec(script, "errorSubclass"); } } // Primitive attributes are set directly on the FunctionTemplate // when we setup the environment. But we cannot set more complex // types (v8 will crash). // // Plus, just to create more complex types, we always need a // context, i.e. an Array has to have a Context to exist. // // As far as I can tell, getting the FunctionTemplate's object // and setting values directly on it, for each context, is the // way to do this. inline for (Types, 0..) |s, i| { const Struct = s.defaultValue().?; inline for (@typeInfo(Struct).@"struct".decls) |declaration| { const name = declaration.name; if (comptime name[0] == '_') { const value = @field(Struct, name); if (comptime isComplexAttributeType(@typeInfo(@TypeOf(value)))) { const js_obj = templates[i].getFunction(v8_context).toObject(); const js_name = v8.String.initUtf8(isolate, name[1..]).toName(); const js_val = try js_context.zigValueToJs(value); if (!js_obj.setValue(v8_context, js_name, js_val)) { log.fatal(.app, "set class attribute", .{ .@"struct" = @typeName(Struct), .name = name, }); } } } } } _ = try js_context._mapZigInstanceToJs(v8_context.getGlobal(), global); return js_context; } pub fn removeJsContext(self: *ExecutionWorld) void { self.js_context.?.deinit(); self.js_context = null; _ = self.context_arena.reset(.{ .retain_with_limit = CONTEXT_ARENA_RETAIN }); } pub fn terminateExecution(self: *const ExecutionWorld) void { self.env.isolate.terminateExecution(); } pub fn resumeExecution(self: *const ExecutionWorld) void { self.env.isolate.cancelTerminateExecution(); } }; const PersistentObject = v8.Persistent(v8.Object); const PersistentModule = v8.Persistent(v8.Module); const PersistentPromise = v8.Persistent(v8.Promise); const PersistentFunction = v8.Persistent(v8.Function); // Loosely maps to a Browser Page. pub const JsContext = struct { id: usize, state: State, isolate: v8.Isolate, // This context is a persistent object. The persistent needs to be recovered and reset. v8_context: v8.Context, handle_scope: ?v8.HandleScope, // references Env.templates templates: []v8.FunctionTemplate, // references the Env.meta_lookup meta_lookup: []TypeMeta, // An arena for the lifetime of a call-group. Gets reset whenever // call_depth reaches 0. call_arena: Allocator, // An arena for the lifetime of the context context_arena: Allocator, // Because calls can be nested (i.e.a function calling a callback), // we can only reset the call_arena when call_depth == 0. If we were // to reset it within a callback, it would invalidate the data of // the call which is calling the callback. call_depth: usize = 0, // Callbacks are PesistendObjects. When the context ends, we need // to free every callback we created. callbacks: std.ArrayListUnmanaged(v8.Persistent(v8.Function)) = .empty, // Serves two purposes. Like `callbacks` above, this is used to free // every PeristentObjet we've created during the lifetime of the context. // More importantly, it serves as an identity map - for a given Zig // instance, we map it to the same PersistentObject. // The key is the @intFromPtr of the Zig value identity_map: std.AutoHashMapUnmanaged(usize, PersistentObject) = .empty, // Some web APIs have to manage opaque values. Ideally, they use an // JsObject, but the JsObject has no lifetime guarantee beyond the // current call. They can call .persist() on their JsObject to get // a `*PersistentObject()`. We need to track these to free them. // This used to be a map and acted like identity_map; the key was // the @intFromPtr(js_obj.handle). But v8 can re-use address. Without // a reliable way to know if an object has already been persisted, // we now simply persist every time persist() is called. js_object_list: std.ArrayListUnmanaged(PersistentObject) = .empty, // Various web APIs depend on having a persistent promise resolver. They // require for this PromiseResolver to be valid for a lifetime longer than // the function that resolves/rejects them. persisted_promise_resolvers: std.ArrayListUnmanaged(v8.Persistent(v8.PromiseResolver)) = .empty, // Some Zig types have code to execute to cleanup destructor_callbacks: std.ArrayListUnmanaged(DestructorCallback) = .empty, // Our module cache: normalized module specifier => module. module_cache: std.StringHashMapUnmanaged(ModuleEntry) = .empty, // Module => Path. The key is the module hashcode (module.getIdentityHash) // and the value is the full path to the module. We need to capture this // so that when we're asked to resolve a dependent module, and all we're // given is the specifier, we can form the full path. The full path is // necessary to lookup/store the dependent module in the module_cache. module_identifier: std.AutoHashMapUnmanaged(u32, []const u8) = .empty, // the page's script manager script_manager: ?*ScriptManager, // Global callback is called on missing property. global_callback: ?GlobalMissingCallback = null, const ModuleEntry = struct { // Can be null if we're asynchrously loading the module, in // which case resolver_promise cannot be null. module: ?PersistentModule = null, // The promise of the evaluating module. The resolved value is // meaningless to us, but the resolver promise needs to chain // to this, since we need to know when it's complete. module_promise: ?PersistentPromise = null, // The promise for the resolver which is loading the module. // (AKA, the first time we try to load it). This resolver will // chain to the module_promise and, when it's done evaluating // will resolve its namespace. Any other attempt to load the // module willchain to this. resolver_promise: ?PersistentPromise = null, }; // no init, started with executor.createJsContext() fn deinit(self: *JsContext) void { { // reverse order, as this has more chance of respecting any // dependencies objects might have with each other. const items = self.destructor_callbacks.items; var i = items.len; while (i > 0) { i -= 1; items[i].destructor(); } } { var it = self.identity_map.valueIterator(); while (it.next()) |p| { p.deinit(); } } for (self.js_object_list.items) |*p| { p.deinit(); } for (self.persisted_promise_resolvers.items) |*p| { p.deinit(); } { var it = self.module_cache.valueIterator(); while (it.next()) |entry| { if (entry.module) |*mod| { mod.deinit(); } if (entry.module_promise) |*p| { p.deinit(); } if (entry.resolver_promise) |*p| { p.deinit(); } } } for (self.callbacks.items) |*cb| { cb.deinit(); } if (self.handle_scope) |*scope| { scope.deinit(); self.v8_context.exit(); } var presistent_context = v8.Persistent(v8.Context).recoverCast(self.v8_context); presistent_context.deinit(); } fn trackCallback(self: *JsContext, pf: PersistentFunction) !void { return self.callbacks.append(self.context_arena, pf); } // Given an anytype, turns it into a v8.Object. The anytype could be: // 1 - A V8.object already // 2 - Our JsObject wrapper around a V8.Object // 3 - A zig instance that has previously been given to V8 // (i.e., the value has to be known to the executor) fn valueToExistingObject(self: *const JsContext, value: anytype) !v8.Object { if (@TypeOf(value) == v8.Object) { return value; } if (@TypeOf(value) == JsObject) { return value.js_obj; } const persistent_object = self.identity_map.get(@intFromPtr(value)) orelse { return error.InvalidThisForCallback; }; return persistent_object.castToObject(); } pub fn stackTrace(self: *const JsContext) !?[]const u8 { return stackForLogs(self.call_arena, self.isolate); } // Executes the src pub fn eval(self: *JsContext, src: []const u8, name: ?[]const u8) !void { _ = try self.exec(src, name); } pub fn exec(self: *JsContext, src: []const u8, name: ?[]const u8) !Value { const v8_context = self.v8_context; const scr = try compileScript(self.isolate, v8_context, src, name); const value = scr.run(v8_context) catch { return error.ExecutionError; }; return self.createValue(value); } pub fn module(self: *JsContext, comptime want_result: bool, src: []const u8, url: []const u8, cacheable: bool) !(if (want_result) ModuleEntry else void) { if (cacheable) { if (self.module_cache.get(url)) |entry| { // The dynamic import will create an entry without the // module to prevent multiple calls from asynchronously // loading the same module. If we're here, without the // module, then it's time to load it. if (entry.module != null) { return if (comptime want_result) entry else {}; } } } errdefer _ = self.module_cache.remove(url); const m = try compileModule(self.isolate, src, url); const arena = self.context_arena; const owned_url = try arena.dupe(u8, url); try self.module_identifier.putNoClobber(arena, m.getIdentityHash(), owned_url); errdefer _ = self.module_identifier.remove(m.getIdentityHash()); const v8_context = self.v8_context; { // Non-async modules are blocking. We can download them in // parallel, but they need to be processed serially. So we // want to get the list of dependent modules this module has // and start downloading them asap. const requests = m.getModuleRequests(); const isolate = self.isolate; for (0..requests.length()) |i| { const req = requests.get(v8_context, @intCast(i)).castTo(v8.ModuleRequest); const specifier = try jsStringToZig(self.call_arena, req.getSpecifier(), isolate); const normalized_specifier = try @import("../url.zig").stitch( self.call_arena, specifier, owned_url, .{ .alloc = .if_needed, .null_terminated = true }, ); const gop = try self.module_cache.getOrPut(self.context_arena, normalized_specifier); if (!gop.found_existing) { const owned_specifier = try self.context_arena.dupeZ(u8, normalized_specifier); gop.key_ptr.* = owned_specifier; gop.value_ptr.* = .{}; try self.script_manager.?.getModule(owned_specifier); } } } if (try m.instantiate(v8_context, resolveModuleCallback) == false) { return error.ModuleInstantiationError; } const evaluated = try m.evaluate(v8_context); // https://v8.github.io/api/head/classv8_1_1Module.html#a1f1758265a4082595757c3251bb40e0f // Must be a promise that gets returned here. std.debug.assert(evaluated.isPromise()); if (comptime !want_result) { // avoid creating a bunch of persisted objects if it isn't // cacheable and the caller doesn't care about results. // This is pretty common, i.e. every