From 32226297ab967f1690cf4db2b342b3699708305a Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Wed, 1 Oct 2025 16:11:44 +0800 Subject: [PATCH] Remove the generic nature of Env and most of the JS classes Back in the zig-js-runtime days, globals were used for the state and webapi declarations. This caused problems largely because it was done across compilation units (using @import("root")...). The generic Env(S, WebApi) was used to solve these problems, while still making it work for different States and WebApis. This change removes the generics and hard-codes the *Page as the state and only supports our WebApis for the class declarations. To accommodate this change, the runtime/*tests* have been removed. I don't consider this a huge loss - whatever behavior these were testing, already exists in the browser/**/*.zig web api. As we write more complex/complete WebApis, we're seeing more and more cases that need to rely on js objects directly (JsObject, Function, Promises, etc...). The goal is to make these easier to use. Rather than using Env.JsObject, you now import "js.zig" and use js.JsObject (TODO: rename JsObject to Object). Everything is just a plain Zig struct, rather than being nested in a generic. After this change, I plan on: 1 - Renaming the js objects, JsObject -> Object. These should be referenced in the webapi as js.Object, js.This, ... 2 - Splitting the code across multiple files (Env.zig, Context.zig, Caller.zig, ...) --- src/app.zig | 2 +- src/browser/ScriptManager.zig | 10 +- src/browser/State.zig | 8 +- src/browser/browser.zig | 6 +- src/browser/console/console.zig | 18 +- src/browser/crypto/crypto.zig | 4 +- src/browser/cssom/CSSStyleSheet.zig | 4 +- src/browser/dom/Animation.zig | 26 +- src/browser/dom/MessageChannel.zig | 40 +- src/browser/dom/document.zig | 11 +- src/browser/dom/element.zig | 9 +- src/browser/dom/html_collection.zig | 1 - src/browser/dom/intersection_observer.zig | 8 +- src/browser/dom/mutation_observer.zig | 8 +- src/browser/dom/node.zig | 2 +- src/browser/dom/node_filter.zig | 4 +- src/browser/dom/node_iterator.zig | 12 +- src/browser/dom/nodelist.zig | 9 +- src/browser/dom/performance.zig | 12 +- src/browser/dom/performance_observer.zig | 4 +- src/browser/dom/resize_observer.zig | 4 +- src/browser/dom/shadow_root.zig | 8 +- src/browser/dom/token_list.zig | 7 +- src/browser/dom/tree_walker.zig | 12 +- src/browser/encoding/TextDecoder.zig | 1 - src/browser/encoding/TextEncoder.zig | 4 +- src/browser/env.zig | 51 - src/browser/events/custom_event.zig | 11 +- src/browser/events/event.zig | 15 +- src/browser/fetch/Headers.zig | 7 +- src/browser/fetch/Request.zig | 8 +- src/browser/fetch/Response.zig | 10 +- src/browser/fetch/fetch.zig | 6 +- src/browser/html/AbortController.zig | 4 +- src/browser/html/DataSet.zig | 5 +- src/browser/html/History.zig | 14 +- src/browser/html/elements.zig | 12 +- src/browser/html/error_event.zig | 8 +- src/browser/html/media_query_list.zig | 6 +- src/browser/html/window.zig | 36 +- src/{runtime => browser/js}/generate.zig | 2 +- src/browser/js/js.zig | 4331 ++++++++++++++++ src/{runtime => browser/js}/subtype.zig | 0 src/browser/page.zig | 8 +- src/browser/polyfill/polyfill.zig | 12 +- src/browser/session.zig | 4 +- src/browser/streams/ReadableStream.zig | 26 +- .../ReadableStreamDefaultController.zig | 4 +- .../streams/ReadableStreamDefaultReader.zig | 8 +- src/browser/url/url.zig | 4 +- src/browser/xhr/event_target.zig | 34 +- src/cdp/cdp.zig | 13 +- src/main_wpt.zig | 5 +- src/runtime/js.zig | 4344 ----------------- src/runtime/test_complex_types.zig | 295 -- src/runtime/test_object_types.zig | 275 -- src/runtime/test_primitive_types.zig | 352 -- src/runtime/testing.zig | 110 - src/testing.zig | 4 +- 59 files changed, 4574 insertions(+), 5684 deletions(-) delete mode 100644 src/browser/env.zig rename src/{runtime => browser/js}/generate.zig (98%) create mode 100644 src/browser/js/js.zig rename src/{runtime => browser/js}/subtype.zig (100%) delete mode 100644 src/runtime/js.zig delete mode 100644 src/runtime/test_complex_types.zig delete mode 100644 src/runtime/test_object_types.zig delete mode 100644 src/runtime/test_primitive_types.zig delete mode 100644 src/runtime/testing.zig diff --git a/src/app.zig b/src/app.zig index 3b3772c4..a5ceab82 100644 --- a/src/app.zig +++ b/src/app.zig @@ -4,7 +4,7 @@ const Allocator = std.mem.Allocator; const log = @import("log.zig"); const Http = @import("http/Http.zig"); -const Platform = @import("runtime/js.zig").Platform; +const Platform = @import("browser/js/js.zig").Platform; const Telemetry = @import("telemetry/telemetry.zig").Telemetry; const Notification = @import("notification.zig").Notification; diff --git a/src/browser/ScriptManager.zig b/src/browser/ScriptManager.zig index 437d13c5..82e76013 100644 --- a/src/browser/ScriptManager.zig +++ b/src/browser/ScriptManager.zig @@ -18,10 +18,10 @@ const std = @import("std"); +const js = @import("js/js.zig"); const log = @import("../log.zig"); const parser = @import("netsurf.zig"); -const Env = @import("env.zig").Env; const Page = @import("page.zig").Page; const DataURI = @import("DataURI.zig"); const Http = @import("../http/Http.zig"); @@ -627,7 +627,7 @@ const Script = struct { const Callback = union(enum) { string: []const u8, - function: Env.Function, + function: js.Function, }; const Source = union(enum) { @@ -664,7 +664,7 @@ const Script = struct { }); const js_context = page.main_context; - var try_catch: Env.TryCatch = undefined; + var try_catch: js.TryCatch = undefined; try_catch.init(js_context); defer try_catch.deinit(); @@ -706,7 +706,7 @@ const Script = struct { switch (callback) { .string => |str| { - var try_catch: Env.TryCatch = undefined; + var try_catch: js.TryCatch = undefined; try_catch.init(page.main_context); defer try_catch.deinit(); @@ -728,7 +728,7 @@ const Script = struct { }; defer parser.eventDestroy(loadevt); - var result: Env.Function.Result = undefined; + var result: js.Function.Result = undefined; const iface = Event.toInterface(loadevt); f.tryCall(void, .{iface}, &result) catch { log.warn(.user_script, "script callback", .{ diff --git a/src/browser/State.zig b/src/browser/State.zig index 17817e30..77165e5b 100644 --- a/src/browser/State.zig +++ b/src/browser/State.zig @@ -26,7 +26,7 @@ // this quickly proved necessary, since different fields are needed on the same // data at different levels of the prototype chain. This isn't memory efficient. -const Env = @import("env.zig").Env; +const js = @import("js/js.zig"); const parser = @import("netsurf.zig"); const DataSet = @import("html/DataSet.zig"); const ShadowRoot = @import("dom/shadow_root.zig").ShadowRoot; @@ -34,8 +34,8 @@ const StyleSheet = @import("cssom/StyleSheet.zig"); const CSSStyleDeclaration = @import("cssom/CSSStyleDeclaration.zig"); // for HTMLScript (but probably needs to be added to more) -onload: ?Env.Function = null, -onerror: ?Env.Function = null, +onload: ?js.Function = null, +onerror: ?js.Function = null, // for HTMLElement style: CSSStyleDeclaration = .empty, @@ -53,7 +53,7 @@ style_sheet: ?*StyleSheet = null, // for dom/document active_element: ?*parser.Element = null, -adopted_style_sheets: ?Env.JsObject = null, +adopted_style_sheets: ?js.JsObject = null, // for HTMLSelectElement // By default, if no option is explicitly selected, the first option should diff --git a/src/browser/browser.zig b/src/browser/browser.zig index 09ceef1c..f787a6db 100644 --- a/src/browser/browser.zig +++ b/src/browser/browser.zig @@ -21,8 +21,8 @@ const std = @import("std"); const Allocator = std.mem.Allocator; const ArenaAllocator = std.heap.ArenaAllocator; +const js = @import("js/js.zig"); const State = @import("State.zig"); -const Env = @import("env.zig").Env; const App = @import("../app.zig").App; const Session = @import("session.zig").Session; const Notification = @import("../notification.zig").Notification; @@ -34,7 +34,7 @@ const HttpClient = @import("../http/Client.zig"); // You can create multiple browser instances. // A browser contains only one session. pub const Browser = struct { - env: *Env, + env: *js.Env, app: *App, session: ?Session, allocator: Allocator, @@ -48,7 +48,7 @@ pub const Browser = struct { pub fn init(app: *App) !Browser { const allocator = app.allocator; - const env = try Env.init(allocator, &app.platform, .{}); + const env = try js.Env.init(allocator, &app.platform, .{}); errdefer env.deinit(); const notification = try Notification.init(allocator, app.notification); diff --git a/src/browser/console/console.zig b/src/browser/console/console.zig index 2ab3a5d3..16d3731a 100644 --- a/src/browser/console/console.zig +++ b/src/browser/console/console.zig @@ -20,47 +20,47 @@ const std = @import("std"); const builtin = @import("builtin"); const log = @import("../../log.zig"); +const js = @import("../js/js.zig"); const Page = @import("../page.zig").Page; -const JsObject = @import("../env.zig").Env.JsObject; pub const Console = struct { // TODO: configurable writer timers: std.StringHashMapUnmanaged(u32) = .{}, counts: std.StringHashMapUnmanaged(u32) = .{}, - pub fn _lp(values: []JsObject, page: *Page) !void { + pub fn _lp(values: []js.JsObject, page: *Page) !void { if (values.len == 0) { return; } log.fatal(.console, "lightpanda", .{ .args = try serializeValues(values, page) }); } - pub fn _log(values: []JsObject, page: *Page) !void { + pub fn _log(values: []js.JsObject, page: *Page) !void { if (values.len == 0) { return; } log.info(.console, "info", .{ .args = try serializeValues(values, page) }); } - pub fn _info(values: []JsObject, page: *Page) !void { + pub fn _info(values: []js.JsObject, page: *Page) !void { return _log(values, page); } - pub fn _debug(values: []JsObject, page: *Page) !void { + pub fn _debug(values: []js.JsObject, page: *Page) !void { if (values.len == 0) { return; } log.debug(.console, "debug", .{ .args = try serializeValues(values, page) }); } - pub fn _warn(values: []JsObject, page: *Page) !void { + pub fn _warn(values: []js.JsObject, page: *Page) !void { if (values.len == 0) { return; } log.warn(.console, "warn", .{ .args = try serializeValues(values, page) }); } - pub fn _error(values: []JsObject, page: *Page) !void { + pub fn _error(values: []js.JsObject, page: *Page) !void { if (values.len == 0) { return; } @@ -132,7 +132,7 @@ pub const Console = struct { log.warn(.console, "timer stop", .{ .label = label, .elapsed = elapsed - kv.value }); } - pub fn _assert(assertion: JsObject, values: []JsObject, page: *Page) !void { + pub fn _assert(assertion: js.JsObject, values: []js.JsObject, page: *Page) !void { if (assertion.isTruthy()) { return; } @@ -143,7 +143,7 @@ pub const Console = struct { log.info(.console, "assertion failed", .{ .values = serialized_values }); } - fn serializeValues(values: []JsObject, page: *Page) ![]const u8 { + fn serializeValues(values: []js.JsObject, page: *Page) ![]const u8 { if (values.len == 0) { return ""; } diff --git a/src/browser/crypto/crypto.zig b/src/browser/crypto/crypto.zig index 27813b4b..8d69176a 100644 --- a/src/browser/crypto/crypto.zig +++ b/src/browser/crypto/crypto.zig @@ -17,14 +17,14 @@ // along with this program. If not, see . const std = @import("std"); -const Env = @import("../env.zig").Env; +const js = @import("../js/js.zig"); const uuidv4 = @import("../../id.zig").uuidv4; // https://w3c.github.io/webcrypto/#crypto-interface pub const Crypto = struct { _not_empty: bool = true, - pub fn _getRandomValues(_: *const Crypto, js_obj: Env.JsObject) !Env.JsObject { + pub fn _getRandomValues(_: *const Crypto, js_obj: js.JsObject) !js.JsObject { var into = try js_obj.toZig(Crypto, "getRandomValues", RandomValues); const buf = into.asBuffer(); if (buf.len > 65_536) { diff --git a/src/browser/cssom/CSSStyleSheet.zig b/src/browser/cssom/CSSStyleSheet.zig index b963cc57..13d2a0a6 100644 --- a/src/browser/cssom/CSSStyleSheet.zig +++ b/src/browser/cssom/CSSStyleSheet.zig @@ -18,7 +18,7 @@ const std = @import("std"); -const Env = @import("../env.zig").Env; +const js = @import("../js/js.zig"); const Page = @import("../page.zig").Page; const StyleSheet = @import("StyleSheet.zig"); const CSSRuleList = @import("CSSRuleList.zig"); @@ -73,7 +73,7 @@ pub fn _deleteRule(self: *CSSStyleSheet, index: usize) !void { _ = self.css_rules.list.orderedRemove(index); } -pub fn _replace(self: *CSSStyleSheet, text: []const u8, page: *Page) !Env.Promise { +pub fn _replace(self: *CSSStyleSheet, text: []const u8, page: *Page) !js.Promise { _ = self; _ = text; // TODO: clear self.css_rules diff --git a/src/browser/dom/Animation.zig b/src/browser/dom/Animation.zig index 090c8aa7..8f25af6e 100644 --- a/src/browser/dom/Animation.zig +++ b/src/browser/dom/Animation.zig @@ -18,19 +18,17 @@ const std = @import("std"); +const js = @import("../js/js.zig"); const Page = @import("../page.zig").Page; -const JsObject = @import("../env.zig").JsObject; -const Promise = @import("../env.zig").Promise; -const PromiseResolver = @import("../env.zig").PromiseResolver; const Animation = @This(); -effect: ?JsObject, -timeline: ?JsObject, -ready_resolver: ?PromiseResolver, -finished_resolver: ?PromiseResolver, +effect: ?js.JsObject, +timeline: ?js.JsObject, +ready_resolver: ?js.PromiseResolver, +finished_resolver: ?js.PromiseResolver, -pub fn constructor(effect: ?JsObject, timeline: ?JsObject) !Animation { +pub fn constructor(effect: ?js.JsObject, timeline: ?js.JsObject) !Animation { return .{ .effect = if (effect) |eo| try eo.persist() else null, .timeline = if (timeline) |to| try to.persist() else null, @@ -49,7 +47,7 @@ pub fn get_pending(self: *const Animation) bool { return false; } -pub fn get_finished(self: *Animation, page: *Page) !Promise { +pub fn get_finished(self: *Animation, page: *Page) !js.Promise { if (self.finished_resolver == null) { const resolver = page.main_context.createPromiseResolver(); try resolver.resolve(self); @@ -58,7 +56,7 @@ pub fn get_finished(self: *Animation, page: *Page) !Promise { return self.finished_resolver.?.promise(); } -pub fn get_ready(self: *Animation, page: *Page) !Promise { +pub fn get_ready(self: *Animation, page: *Page) !js.Promise { // never resolved, because we're always "finished" if (self.ready_resolver == null) { const resolver = page.main_context.createPromiseResolver(); @@ -67,19 +65,19 @@ pub fn get_ready(self: *Animation, page: *Page) !Promise { return self.ready_resolver.?.promise(); } -pub fn get_effect(self: *const Animation) ?JsObject { +pub fn get_effect(self: *const Animation) ?js.JsObject { return self.effect; } -pub fn set_effect(self: *Animation, effect: JsObject) !void { +pub fn set_effect(self: *Animation, effect: js.JsObject) !void { self.effect = try effect.persist(); } -pub fn get_timeline(self: *const Animation) ?JsObject { +pub fn get_timeline(self: *const Animation) ?js.JsObject { return self.timeline; } -pub fn set_timeline(self: *Animation, timeline: JsObject) !void { +pub fn set_timeline(self: *Animation, timeline: js.JsObject) !void { self.timeline = try timeline.persist(); } diff --git a/src/browser/dom/MessageChannel.zig b/src/browser/dom/MessageChannel.zig index c2816616..354c990b 100644 --- a/src/browser/dom/MessageChannel.zig +++ b/src/browser/dom/MessageChannel.zig @@ -20,13 +20,11 @@ const std = @import("std"); const log = @import("../../log.zig"); const parser = @import("../netsurf.zig"); -const Env = @import("../env.zig").Env; +const js = @import("../js/js.zig"); const Page = @import("../page.zig").Page; const EventTarget = @import("../dom/event_target.zig").EventTarget; const EventHandler = @import("../events/event.zig").EventHandler; -const JsObject = Env.JsObject; -const Function = Env.Function; const Allocator = std.mem.Allocator; const MAX_QUEUE_SIZE = 10; @@ -72,22 +70,22 @@ pub const MessagePort = struct { pair: *MessagePort, closed: bool = false, started: bool = false, - onmessage_cbk: ?Function = null, - onmessageerror_cbk: ?Function = null, + onmessage_cbk: ?js.Function = null, + onmessageerror_cbk: ?js.Function = null, // This is the queue of messages to dispatch to THIS MessagePort when the // MessagePort is started. - queue: std.ArrayListUnmanaged(JsObject) = .empty, + queue: std.ArrayListUnmanaged(js.JsObject) = .empty, pub const PostMessageOption = union(enum) { - transfer: JsObject, + transfer: js.JsObject, options: Opts, pub const Opts = struct { - transfer: JsObject, + transfer: js.JsObject, }; }; - pub fn _postMessage(self: *MessagePort, obj: JsObject, opts_: ?PostMessageOption, page: *Page) !void { + pub fn _postMessage(self: *MessagePort, obj: js.JsObject, opts_: ?PostMessageOption, page: *Page) !void { if (self.closed) { return; } @@ -124,10 +122,10 @@ pub const MessagePort = struct { self.pair.closed = true; } - pub fn get_onmessage(self: *MessagePort) ?Function { + pub fn get_onmessage(self: *MessagePort) ?js.Function { return self.onmessage_cbk; } - pub fn get_onmessageerror(self: *MessagePort) ?Function { + pub fn get_onmessageerror(self: *MessagePort) ?js.Function { return self.onmessageerror_cbk; } @@ -152,7 +150,7 @@ pub const MessagePort = struct { // called from our pair. If port1.postMessage("x") is called, then this // will be called on port2. - fn dispatchOrQueue(self: *MessagePort, obj: JsObject, arena: Allocator) !void { + fn dispatchOrQueue(self: *MessagePort, obj: js.JsObject, arena: Allocator) !void { // our pair should have checked this already std.debug.assert(self.closed == false); @@ -167,7 +165,7 @@ pub const MessagePort = struct { return self.queue.append(arena, try obj.persist()); } - fn dispatch(self: *MessagePort, obj: JsObject) !void { + fn dispatch(self: *MessagePort, obj: js.JsObject) !void { // obj is already persisted, don't use `MessageEvent.constructor`, but // go directly to `init`, which assumes persisted objects. var evt = try MessageEvent.init(.{ .data = obj }); @@ -182,7 +180,7 @@ pub const MessagePort = struct { alloc: Allocator, typ: []const u8, listener: EventHandler.Listener, - ) !?Function { + ) !?js.Function { const target = @as(*parser.EventTarget, @ptrCast(self)); const eh = (try EventHandler.register(alloc, target, typ, listener, null)) orelse unreachable; return eh.callback; @@ -207,12 +205,12 @@ pub const MessageEvent = struct { pub const union_make_copy = true; proto: parser.Event, - data: ?JsObject, + data: ?js.JsObject, // You would think if port1 sends to port2, the source would be port2 // (which is how I read the documentation), but it appears to always be // null. It can always be set explicitly via the constructor; - source: ?JsObject, + source: ?js.JsObject, origin: []const u8, @@ -226,8 +224,8 @@ pub const MessageEvent = struct { ports: []*MessagePort, const Options = struct { - data: ?JsObject = null, - source: ?JsObject = null, + data: ?js.JsObject = null, + source: ?js.JsObject = null, origin: []const u8 = "", lastEventId: []const u8 = "", ports: []*MessagePort = &.{}, @@ -243,7 +241,7 @@ pub const MessageEvent = struct { }); } - // This is like "constructor", but it assumes JsObjects have already been + // This is like "constructor", but it assumes js.JsObjects have already been // persisted. Necessary because this `new MessageEvent()` can be called // directly from JS OR from a port.postMessage. In the latter case, data // may have already been persisted (as it might need to be queued); @@ -263,7 +261,7 @@ pub const MessageEvent = struct { }; } - pub fn get_data(self: *const MessageEvent) !?JsObject { + pub fn get_data(self: *const MessageEvent) !?js.JsObject { return self.data; } @@ -271,7 +269,7 @@ pub const MessageEvent = struct { return self.origin; } - pub fn get_source(self: *const MessageEvent) ?JsObject { + pub fn get_source(self: *const MessageEvent) ?js.JsObject { return self.source; } diff --git a/src/browser/dom/document.zig b/src/browser/dom/document.zig index 6dfd069b..002333f6 100644 --- a/src/browser/dom/document.zig +++ b/src/browser/dom/document.zig @@ -18,6 +18,7 @@ const std = @import("std"); +const js = @import("../js/js.zig"); const parser = @import("../netsurf.zig"); const Page = @import("../page.zig").Page; @@ -37,8 +38,6 @@ const Range = @import("range.zig").Range; const CustomEvent = @import("../events/custom_event.zig").CustomEvent; -const Env = @import("../env.zig").Env; - const DOMImplementation = @import("implementation.zig").DOMImplementation; // WEB IDL https://dom.spec.whatwg.org/#document @@ -155,13 +154,13 @@ pub const Document = struct { // the spec changed to return an HTMLCollection instead. // That's why we reimplemented getElementsByTagName by using an // HTMLCollection in zig here. - pub fn _getElementsByTagName(self: *parser.Document, tag_name: Env.String) !collection.HTMLCollection { + pub fn _getElementsByTagName(self: *parser.Document, tag_name: js.String) !collection.HTMLCollection { return collection.HTMLCollectionByTagName(parser.documentToNode(self), tag_name.string, .{ .include_root = true, }); } - pub fn _getElementsByClassName(self: *parser.Document, class_names: Env.String) !collection.HTMLCollection { + pub fn _getElementsByClassName(self: *parser.Document, class_names: js.String) !collection.HTMLCollection { return collection.HTMLCollectionByClassName(parser.documentToNode(self), class_names.string, .{ .include_root = true, }); @@ -299,7 +298,7 @@ pub const Document = struct { return &.{}; } - pub fn get_adoptedStyleSheets(self: *parser.Document, page: *Page) !Env.JsObject { + pub fn get_adoptedStyleSheets(self: *parser.Document, page: *Page) !js.JsObject { const state = try page.getOrCreateNodeState(@ptrCast(@alignCast(self))); if (state.adopted_style_sheets) |obj| { return obj; @@ -310,7 +309,7 @@ pub const Document = struct { return obj; } - pub fn set_adoptedStyleSheets(self: *parser.Document, sheets: Env.JsObject, page: *Page) !void { + pub fn set_adoptedStyleSheets(self: *parser.Document, sheets: js.JsObject, page: *Page) !void { const state = try page.getOrCreateNodeState(@ptrCast(@alignCast(self))); state.adopted_style_sheets = try sheets.persist(); } diff --git a/src/browser/dom/element.zig b/src/browser/dom/element.zig index a1a7f33e..65b34dd4 100644 --- a/src/browser/dom/element.zig +++ b/src/browser/dom/element.zig @@ -18,8 +18,8 @@ const std = @import("std"); +const js = @import("../js/js.zig"); const parser = @import("../netsurf.zig"); -const Env = @import("../env.zig").Env; const Page = @import("../page.zig").Page; const css = @import("css.zig"); @@ -34,7 +34,6 @@ const HTMLElem = @import("../html/elements.zig"); const ShadowRoot = @import("../dom/shadow_root.zig").ShadowRoot; const Animation = @import("Animation.zig"); -const JsObject = @import("../env.zig").JsObject; pub const Union = @import("../html/elements.zig").Union; @@ -436,7 +435,7 @@ pub const Element = struct { return try parser.elementRemoveAttributeNode(self, attr); } - pub fn _getElementsByTagName(self: *parser.Element, tag_name: Env.String) !collection.HTMLCollection { + pub fn _getElementsByTagName(self: *parser.Element, tag_name: js.String) !collection.HTMLCollection { return collection.HTMLCollectionByTagName( parser.elementToNode(self), tag_name.string, @@ -444,7 +443,7 @@ pub const Element = struct { ); } - pub fn _getElementsByClassName(self: *parser.Element, class_names: Env.String) !collection.HTMLCollection { + pub fn _getElementsByClassName(self: *parser.Element, class_names: js.String) !collection.HTMLCollection { return try collection.HTMLCollectionByClassName( parser.elementToNode(self), class_names.string, @@ -661,7 +660,7 @@ pub const Element = struct { return sr; } - pub fn _animate(self: *parser.Element, effect: JsObject, opts: JsObject) !Animation { + pub fn _animate(self: *parser.Element, effect: js.JsObject, opts: js.JsObject) !Animation { _ = self; _ = opts; return Animation.constructor(effect, null); diff --git a/src/browser/dom/html_collection.zig b/src/browser/dom/html_collection.zig index a3c7ccc0..34b957e2 100644 --- a/src/browser/dom/html_collection.zig +++ b/src/browser/dom/html_collection.zig @@ -23,7 +23,6 @@ const parser = @import("../netsurf.zig"); const Element = @import("element.zig").Element; const Union = @import("element.zig").Union; -const JsThis = @import("../env.zig").JsThis; const Walker = @import("walker.zig").Walker; const Matcher = union(enum) { diff --git a/src/browser/dom/intersection_observer.zig b/src/browser/dom/intersection_observer.zig index c493a539..8dccd29f 100644 --- a/src/browser/dom/intersection_observer.zig +++ b/src/browser/dom/intersection_observer.zig @@ -18,11 +18,11 @@ const std = @import("std"); +const js = @import("../js/js.zig"); const log = @import("../../log.zig"); const parser = @import("../netsurf.zig"); const Page = @import("../page.zig").Page; -const Env = @import("../env.zig").Env; const Element = @import("element.zig").Element; pub const Interfaces = .{ @@ -40,14 +40,14 @@ pub const Interfaces = .{ // https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver pub const IntersectionObserver = struct { page: *Page, - callback: Env.Function, + callback: js.Function, options: IntersectionObserverOptions, observed_entries: std.ArrayListUnmanaged(IntersectionObserverEntry), // new IntersectionObserver(callback) // new IntersectionObserver(callback, options) [not supported yet] - pub fn constructor(callback: Env.Function, options_: ?IntersectionObserverOptions, page: *Page) !IntersectionObserver { + pub fn constructor(callback: js.Function, options_: ?IntersectionObserverOptions, page: *Page) !IntersectionObserver { var options = IntersectionObserverOptions{ .root = parser.documentToNode(parser.documentHTMLToDocument(page.window.document)), .rootMargin = "0px 0px 0px 0px", @@ -84,7 +84,7 @@ pub const IntersectionObserver = struct { .options = &self.options, }); - var result: Env.Function.Result = undefined; + var result: js.Function.Result = undefined; self.callback.tryCall(void, .{self.observed_entries.items}, &result) catch { log.debug(.user_script, "callback error", .{ .err = result.exception, diff --git a/src/browser/dom/mutation_observer.zig b/src/browser/dom/mutation_observer.zig index 24f5afa3..703156a7 100644 --- a/src/browser/dom/mutation_observer.zig +++ b/src/browser/dom/mutation_observer.zig @@ -18,11 +18,11 @@ const std = @import("std"); +const js = @import("../js/js.zig"); const log = @import("../../log.zig"); const parser = @import("../netsurf.zig"); const Page = @import("../page.zig").Page; -const Env = @import("../env.zig").Env; const NodeList = @import("nodelist.zig").NodeList; pub const Interfaces = .{ @@ -35,7 +35,7 @@ const Walker = @import("../dom/walker.zig").WalkerChildren; // WEB IDL https://dom.spec.whatwg.org/#interface-mutationobserver pub const MutationObserver = struct { page: *Page, - cbk: Env.Function, + cbk: js.Function, scheduled: bool, observers: std.ArrayListUnmanaged(*Observer), @@ -43,7 +43,7 @@ pub const MutationObserver = struct { // execute our callback with it. observed: std.ArrayListUnmanaged(MutationRecord), - pub fn constructor(cbk: Env.Function, page: *Page) !MutationObserver { + pub fn constructor(cbk: js.Function, page: *Page) !MutationObserver { return .{ .cbk = cbk, .page = page, @@ -122,7 +122,7 @@ pub const MutationObserver = struct { defer self.observed.clearRetainingCapacity(); - var result: Env.Function.Result = undefined; + var result: js.Function.Result = undefined; self.cbk.tryCallWithThis(void, self, .{records}, &result) catch { log.debug(.user_script, "callback error", .{ .err = result.exception, diff --git a/src/browser/dom/node.zig b/src/browser/dom/node.zig index b95a549b..71b88e0e 100644 --- a/src/browser/dom/node.zig +++ b/src/browser/dom/node.zig @@ -20,7 +20,7 @@ const std = @import("std"); const log = @import("../../log.zig"); const parser = @import("../netsurf.zig"); -const generate = @import("../../runtime/generate.zig"); +const generate = @import("../js/generate.zig"); const Page = @import("../page.zig").Page; const EventTarget = @import("event_target.zig").EventTarget; diff --git a/src/browser/dom/node_filter.zig b/src/browser/dom/node_filter.zig index b8b9cc44..56cdd493 100644 --- a/src/browser/dom/node_filter.zig +++ b/src/browser/dom/node_filter.zig @@ -17,8 +17,8 @@ // along with this program. If not, see . const std = @import("std"); +const js = @import("../js/js.zig"); const parser = @import("../netsurf.zig"); -const Env = @import("../env.zig").Env; const Node = @import("node.zig").Node; pub const NodeFilter = struct { @@ -43,7 +43,7 @@ pub const NodeFilter = struct { const VerifyResult = enum { accept, skip, reject }; -pub fn verify(what_to_show: u32, filter: ?Env.Function, node: *parser.Node) !VerifyResult { +pub fn verify(what_to_show: u32, filter: ?js.Function, node: *parser.Node) !VerifyResult { const node_type = parser.nodeType(node); // Verify that we can show this node type. diff --git a/src/browser/dom/node_iterator.zig b/src/browser/dom/node_iterator.zig index 5cce2399..ebeee7a2 100644 --- a/src/browser/dom/node_iterator.zig +++ b/src/browser/dom/node_iterator.zig @@ -18,8 +18,8 @@ const std = @import("std"); +const js = @import("../js/js.zig"); const parser = @import("../netsurf.zig"); -const Env = @import("../env.zig").Env; const NodeFilter = @import("node_filter.zig"); const Node = @import("node.zig").Node; const NodeUnion = @import("node.zig").Union; @@ -37,7 +37,7 @@ pub const NodeIterator = struct { reference_node: *parser.Node, what_to_show: u32, filter: ?NodeIteratorOpts, - filter_func: ?Env.Function, + filter_func: ?js.Function, pointer_before_current: bool = true, // used to track / block recursive filters is_in_callback: bool = false, @@ -45,15 +45,15 @@ pub const NodeIterator = struct { // One of the few cases where null and undefined resolve to different default. // We need the raw JsObject so that we can probe the tri state: // null, undefined or i32. - pub const WhatToShow = Env.JsObject; + pub const WhatToShow = js.JsObject; pub const NodeIteratorOpts = union(enum) { - function: Env.Function, - object: struct { acceptNode: Env.Function }, + function: js.Function, + object: struct { acceptNode: js.Function }, }; pub fn init(node: *parser.Node, what_to_show_: ?WhatToShow, filter: ?NodeIteratorOpts) !NodeIterator { - var filter_func: ?Env.Function = null; + var filter_func: ?js.Function = null; if (filter) |f| { filter_func = switch (f) { .function => |func| func, diff --git a/src/browser/dom/nodelist.zig b/src/browser/dom/nodelist.zig index 6ee5bca4..7a32f7bb 100644 --- a/src/browser/dom/nodelist.zig +++ b/src/browser/dom/nodelist.zig @@ -19,11 +19,10 @@ const std = @import("std"); const Allocator = std.mem.Allocator; +const js = @import("../js/js.zig"); const log = @import("../../log.zig"); const parser = @import("../netsurf.zig"); -const JsThis = @import("../env.zig").JsThis; -const Function = @import("../env.zig").Function; const NodeUnion = @import("node.zig").Union; const Node = @import("node.zig").Node; @@ -148,10 +147,10 @@ pub const NodeList = struct { // }; // } - pub fn _forEach(self: *NodeList, cbk: Function) !void { // TODO handle thisArg + pub fn _forEach(self: *NodeList, cbk: js.Function) !void { // TODO handle thisArg for (self.nodes.items, 0..) |n, i| { const ii: u32 = @intCast(i); - var result: Function.Result = undefined; + var result: js.Function.Result = undefined; cbk.tryCall(void, .{ n, ii, self }, &result) catch { log.debug(.user_script, "forEach callback", .{ .err = result.exception, .stack = result.stack }); }; @@ -175,7 +174,7 @@ pub const NodeList = struct { } // TODO entries() https://developer.mozilla.org/en-US/docs/Web/API/NodeList/entries - pub fn postAttach(self: *NodeList, js_this: JsThis) !void { + pub fn postAttach(self: *NodeList, js_this: js.JsThis) !void { const len = self.get_length(); for (0..len) |i| { const node = try self._item(@intCast(i)) orelse unreachable; diff --git a/src/browser/dom/performance.zig b/src/browser/dom/performance.zig index 09c0c6f8..d5de884d 100644 --- a/src/browser/dom/performance.zig +++ b/src/browser/dom/performance.zig @@ -18,9 +18,9 @@ const std = @import("std"); +const js = @import("../js/js.zig"); const parser = @import("../netsurf.zig"); const EventTarget = @import("../dom/event_target.zig").EventTarget; -const Env = @import("../env.zig").Env; const Page = @import("../page.zig").Page; const milliTimestamp = @import("../../datetime.zig").milliTimestamp; @@ -61,7 +61,7 @@ pub const Performance = struct { return milliTimestamp() - self.time_origin; } - pub fn _mark(_: *Performance, name: Env.String, _options: ?PerformanceMark.Options, page: *Page) !PerformanceMark { + pub fn _mark(_: *Performance, name: js.String, _options: ?PerformanceMark.Options, page: *Page) !PerformanceMark { const mark: PerformanceMark = try .constructor(name, _options, page); // TODO: Should store this in an entries list return mark; @@ -148,14 +148,14 @@ pub const PerformanceMark = struct { pub const prototype = *PerformanceEntry; proto: PerformanceEntry, - detail: ?Env.JsObject, + detail: ?js.JsObject, const Options = struct { - detail: ?Env.JsObject = null, + detail: ?js.JsObject = null, startTime: ?f64 = null, }; - pub fn constructor(name: Env.String, _options: ?Options, page: *Page) !PerformanceMark { + pub fn constructor(name: js.String, _options: ?Options, page: *Page) !PerformanceMark { const perf = &page.window.performance; const options = _options orelse Options{}; @@ -171,7 +171,7 @@ pub const PerformanceMark = struct { return .{ .proto = proto, .detail = detail }; } - pub fn get_detail(self: *const PerformanceMark) ?Env.JsObject { + pub fn get_detail(self: *const PerformanceMark) ?js.JsObject { return self.detail; } }; diff --git a/src/browser/dom/performance_observer.zig b/src/browser/dom/performance_observer.zig index 5a53090d..2e835ef8 100644 --- a/src/browser/dom/performance_observer.zig +++ b/src/browser/dom/performance_observer.zig @@ -17,7 +17,7 @@ // along with this program. If not, see . const std = @import("std"); -const Env = @import("../env.zig").Env; +const js = @import("../js/js.zig"); const PerformanceEntry = @import("performance.zig").PerformanceEntry; @@ -25,7 +25,7 @@ const PerformanceEntry = @import("performance.zig").PerformanceEntry; pub const PerformanceObserver = struct { pub const _supportedEntryTypes = [0][]const u8{}; - pub fn constructor(cbk: Env.Function) PerformanceObserver { + pub fn constructor(cbk: js.Function) PerformanceObserver { _ = cbk; return .{}; } diff --git a/src/browser/dom/resize_observer.zig b/src/browser/dom/resize_observer.zig index 4f1e37c3..448825ab 100644 --- a/src/browser/dom/resize_observer.zig +++ b/src/browser/dom/resize_observer.zig @@ -16,7 +16,7 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -const Env = @import("../env.zig").Env; +const js = @import("../js/js.zig"); const parser = @import("../netsurf.zig"); pub const Interfaces = .{ @@ -25,7 +25,7 @@ pub const Interfaces = .{ // WEB IDL https://drafts.csswg.org/resize-observer/#resize-observer-interface pub const ResizeObserver = struct { - pub fn constructor(cbk: Env.Function) ResizeObserver { + pub fn constructor(cbk: js.Function) ResizeObserver { _ = cbk; return .{}; } diff --git a/src/browser/dom/shadow_root.zig b/src/browser/dom/shadow_root.zig index 6c230e18..f7d6d1da 100644 --- a/src/browser/dom/shadow_root.zig +++ b/src/browser/dom/shadow_root.zig @@ -20,7 +20,7 @@ const std = @import("std"); const dump = @import("../dump.zig"); const parser = @import("../netsurf.zig"); -const Env = @import("../env.zig").Env; +const js = @import(".././js/js.zig"); const Page = @import("../page.zig").Page; const Node = @import("node.zig").Node; const Element = @import("element.zig").Element; @@ -34,7 +34,7 @@ pub const ShadowRoot = struct { mode: Mode, host: *parser.Element, proto: *parser.DocumentFragment, - adopted_style_sheets: ?Env.JsObject = null, + adopted_style_sheets: ?js.JsObject = null, pub const Mode = enum { open, @@ -45,7 +45,7 @@ pub const ShadowRoot = struct { return Element.toInterface(self.host); } - pub fn get_adoptedStyleSheets(self: *ShadowRoot, page: *Page) !Env.JsObject { + pub fn get_adoptedStyleSheets(self: *ShadowRoot, page: *Page) !js.JsObject { if (self.adopted_style_sheets) |obj| { return obj; } @@ -55,7 +55,7 @@ pub const ShadowRoot = struct { return obj; } - pub fn set_adoptedStyleSheets(self: *ShadowRoot, sheets: Env.JsObject) !void { + pub fn set_adoptedStyleSheets(self: *ShadowRoot, sheets: js.JsObject) !void { self.adopted_style_sheets = try sheets.persist(); } diff --git a/src/browser/dom/token_list.zig b/src/browser/dom/token_list.zig index 37a78df5..4d989a90 100644 --- a/src/browser/dom/token_list.zig +++ b/src/browser/dom/token_list.zig @@ -18,12 +18,11 @@ const std = @import("std"); +const js = @import("../js/js.zig"); const log = @import("../../log.zig"); const parser = @import("../netsurf.zig"); const iterator = @import("../iterator/iterator.zig"); -const Function = @import("../env.zig").Function; -const JsObject = @import("../env.zig").JsObject; const DOMException = @import("exceptions.zig").DOMException; pub const Interfaces = .{ @@ -137,10 +136,10 @@ pub const DOMTokenList = struct { } // TODO handle thisArg - pub fn _forEach(self: *parser.TokenList, cbk: Function, this_arg: JsObject) !void { + pub fn _forEach(self: *parser.TokenList, cbk: js.Function, this_arg: js.JsObject) !void { var entries = _entries(self); while (try entries._next()) |entry| { - var result: Function.Result = undefined; + var result: js.Function.Result = undefined; cbk.tryCallWithThis(void, this_arg, .{ entry.@"1", entry.@"0", self }, &result) catch { log.debug(.user_script, "callback error", .{ .err = result.exception, diff --git a/src/browser/dom/tree_walker.zig b/src/browser/dom/tree_walker.zig index 8ece93cc..06a6552a 100644 --- a/src/browser/dom/tree_walker.zig +++ b/src/browser/dom/tree_walker.zig @@ -17,10 +17,10 @@ // along with this program. If not, see . const std = @import("std"); +const js = @import("../js/js.zig"); const parser = @import("../netsurf.zig"); const NodeFilter = @import("node_filter.zig"); -const Env = @import("../env.zig").Env; const Node = @import("node.zig").Node; const NodeUnion = @import("node.zig").Union; @@ -30,20 +30,20 @@ pub const TreeWalker = struct { current_node: *parser.Node, what_to_show: u32, filter: ?TreeWalkerOpts, - filter_func: ?Env.Function, + filter_func: ?js.Function, // One of the few cases where null and undefined resolve to different default. // We need the raw JsObject so that we can probe the tri state: // null, undefined or i32. - pub const WhatToShow = Env.JsObject; + pub const WhatToShow = js.JsObject; pub const TreeWalkerOpts = union(enum) { - function: Env.Function, - object: struct { acceptNode: Env.Function }, + function: js.Function, + object: struct { acceptNode: js.Function }, }; pub fn init(node: *parser.Node, what_to_show_: ?WhatToShow, filter: ?TreeWalkerOpts) !TreeWalker { - var filter_func: ?Env.Function = null; + var filter_func: ?js.Function = null; if (filter) |f| { filter_func = switch (f) { diff --git a/src/browser/encoding/TextDecoder.zig b/src/browser/encoding/TextDecoder.zig index 811945b6..a15ba436 100644 --- a/src/browser/encoding/TextDecoder.zig +++ b/src/browser/encoding/TextDecoder.zig @@ -19,7 +19,6 @@ const std = @import("std"); const log = @import("../../log.zig"); -const Env = @import("../env.zig").Env; const Page = @import("../page.zig").Page; // https://encoding.spec.whatwg.org/#interface-textdecoder diff --git a/src/browser/encoding/TextEncoder.zig b/src/browser/encoding/TextEncoder.zig index a1551e81..4ac32cd8 100644 --- a/src/browser/encoding/TextEncoder.zig +++ b/src/browser/encoding/TextEncoder.zig @@ -18,7 +18,7 @@ const std = @import("std"); -const Env = @import("../env.zig").Env; +const js = @import("../js/js.zig"); // https://encoding.spec.whatwg.org/#interface-textencoder const TextEncoder = @This(); @@ -31,7 +31,7 @@ pub fn get_encoding(_: *const TextEncoder) []const u8 { return "utf-8"; } -pub fn _encode(_: *const TextEncoder, v: []const u8) !Env.TypedArray(u8) { +pub fn _encode(_: *const TextEncoder, v: []const u8) !js.TypedArray(u8) { // Ensure the input is a valid utf-8 // It seems chrome accepts invalid utf-8 sequence. // diff --git a/src/browser/env.zig b/src/browser/env.zig deleted file mode 100644 index d7a20cf0..00000000 --- a/src/browser/env.zig +++ /dev/null @@ -1,51 +0,0 @@ -const std = @import("std"); - -const Page = @import("page.zig").Page; -const js = @import("../runtime/js.zig"); -const generate = @import("../runtime/generate.zig"); - -const WebApis = struct { - // Wrapped like this for debug ergonomics. - // When we create our Env, a few lines down, we define it as: - // pub const Env = js.Env(*Page, WebApis); - // - // If there's a compile time error witht he Env, it's type will be readable, - // i.e.: runtime.js.Env(*browser.env.Page, browser.env.WebApis) - // - // But if we didn't wrap it in the struct, like we once didn't, and defined - // env as: - // pub const Env = js.Env(*Page, Interfaces); - // - // Because Interfaces is an anynoumous type, it doesn't have a friendly name - // and errors would be something like: - // runtime.js.Env(*browser.Page, .{...A HUNDRED TYPES...}) - pub const Interfaces = generate.Tuple(.{ - @import("crypto/crypto.zig").Crypto, - @import("console/console.zig").Console, - @import("css/css.zig").Interfaces, - @import("cssom/cssom.zig").Interfaces, - @import("dom/dom.zig").Interfaces, - @import("dom/shadow_root.zig").ShadowRoot, - @import("encoding/encoding.zig").Interfaces, - @import("events/event.zig").Interfaces, - @import("html/html.zig").Interfaces, - @import("iterator/iterator.zig").Interfaces, - @import("storage/storage.zig").Interfaces, - @import("url/url.zig").Interfaces, - @import("xhr/xhr.zig").Interfaces, - @import("xhr/form_data.zig").Interfaces, - @import("xhr/File.zig"), - @import("xmlserializer/xmlserializer.zig").Interfaces, - @import("fetch/fetch.zig").Interfaces, - @import("streams/streams.zig").Interfaces, - }); -}; - -pub const JsThis = Env.JsThis; -pub const JsObject = Env.JsObject; -pub const Function = Env.Function; -pub const Promise = Env.Promise; -pub const PromiseResolver = Env.PromiseResolver; - -pub const Env = js.Env(*Page, WebApis); -pub const Global = @import("html/window.zig").Window; diff --git a/src/browser/events/custom_event.zig b/src/browser/events/custom_event.zig index 530eaf5c..bf54d2b2 100644 --- a/src/browser/events/custom_event.zig +++ b/src/browser/events/custom_event.zig @@ -16,9 +16,10 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . +const js = @import("../js/js.zig"); const parser = @import("../netsurf.zig"); const Event = @import("event.zig").Event; -const JsObject = @import("../env.zig").JsObject; + const netsurf = @import("../netsurf.zig"); // https://dom.spec.whatwg.org/#interface-customevent @@ -27,13 +28,13 @@ pub const CustomEvent = struct { pub const union_make_copy = true; proto: parser.Event, - detail: ?JsObject, + detail: ?js.JsObject, const CustomEventInit = struct { bubbles: bool = false, cancelable: bool = false, composed: bool = false, - detail: ?JsObject = null, + detail: ?js.JsObject = null, }; pub fn constructor(event_type: []const u8, opts_: ?CustomEventInit) !CustomEvent { @@ -53,7 +54,7 @@ pub const CustomEvent = struct { }; } - pub fn get_detail(self: *CustomEvent) ?JsObject { + pub fn get_detail(self: *CustomEvent) ?js.JsObject { return self.detail; } @@ -64,7 +65,7 @@ pub const CustomEvent = struct { event_type: []const u8, can_bubble: bool, cancelable: bool, - maybe_detail: ?JsObject, + maybe_detail: ?js.JsObject, ) !void { // This function can only be called after the constructor has called. // So we assume proto is initialized already by constructor. diff --git a/src/browser/events/event.zig b/src/browser/events/event.zig index ef039fac..7e399849 100644 --- a/src/browser/events/event.zig +++ b/src/browser/events/event.zig @@ -21,7 +21,7 @@ const Allocator = std.mem.Allocator; const log = @import("../../log.zig"); const parser = @import("../netsurf.zig"); -const generate = @import("../../runtime/generate.zig"); +const generate = @import("../js/generate.zig"); const Page = @import("../page.zig").Page; const Node = @import("../dom/node.zig").Node; @@ -219,18 +219,17 @@ pub const Event = struct { pub const EventHandler = struct { once: bool, capture: bool, - callback: Function, + callback: js.Function, node: parser.EventNode, listener: *parser.EventListener, - const Env = @import("../env.zig").Env; - const Function = Env.Function; + const js = @import("../js/js.zig"); pub const Listener = union(enum) { - function: Function, - object: Env.JsObject, + function: js.Function, + object: js.JsObject, - pub fn callback(self: Listener, target: *parser.EventTarget) !?Function { + pub fn callback(self: Listener, target: *parser.EventTarget) !?js.Function { return switch (self) { .function => |func| try func.withThis(target), .object => |obj| blk: { @@ -331,7 +330,7 @@ pub const EventHandler = struct { fn handle(node: *parser.EventNode, event: *parser.Event) void { const ievent = Event.toInterface(event); const self: *EventHandler = @fieldParentPtr("node", node); - var result: Function.Result = undefined; + var result: js.Function.Result = undefined; self.callback.tryCall(void, .{ievent}, &result) catch { log.debug(.user_script, "callback error", .{ .err = result.exception, diff --git a/src/browser/fetch/Headers.zig b/src/browser/fetch/Headers.zig index f7d83c3f..9c78046e 100644 --- a/src/browser/fetch/Headers.zig +++ b/src/browser/fetch/Headers.zig @@ -17,6 +17,7 @@ // along with this program. If not, see . const std = @import("std"); +const js = @import("../js/js.zig"); const log = @import("../../log.zig"); const URL = @import("../../url.zig").URL; const Page = @import("../page.zig").Page; @@ -24,8 +25,6 @@ const Page = @import("../page.zig").Page; const iterator = @import("../iterator/iterator.zig"); const v8 = @import("v8"); -const Env = @import("../env.zig").Env; - // https://developer.mozilla.org/en-US/docs/Web/API/Headers const Headers = @This(); @@ -69,7 +68,7 @@ pub const HeadersInit = union(enum) { // Headers headers: *Headers, // Mappings - object: Env.JsObject, + object: js.JsObject, }; pub fn constructor(_init: ?HeadersInit, page: *Page) !Headers { @@ -159,7 +158,7 @@ pub fn _entries(self: *const Headers) HeadersEntryIterable { }; } -pub fn _forEach(self: *Headers, callback_fn: Env.Function, this_arg: ?Env.JsObject) !void { +pub fn _forEach(self: *Headers, callback_fn: js.Function, this_arg: ?js.JsObject) !void { var iter = self.headers.iterator(); const cb = if (this_arg) |this| try callback_fn.withThis(this) else callback_fn; diff --git a/src/browser/fetch/Request.zig b/src/browser/fetch/Request.zig index 5881b427..172c8976 100644 --- a/src/browser/fetch/Request.zig +++ b/src/browser/fetch/Request.zig @@ -17,6 +17,7 @@ // along with this program. If not, see . const std = @import("std"); +const js = @import("../js/js.zig"); const log = @import("../../log.zig"); const URL = @import("../../url.zig").URL; @@ -27,7 +28,6 @@ const Http = @import("../../http/Http.zig"); const ReadableStream = @import("../streams/ReadableStream.zig"); const v8 = @import("v8"); -const Env = @import("../env.zig").Env; const Headers = @import("Headers.zig"); const HeadersInit = @import("Headers.zig").HeadersInit; @@ -241,7 +241,7 @@ pub fn _clone(self: *Request) !Request { }; } -pub fn _bytes(self: *Response, page: *Page) !Env.Promise { +pub fn _bytes(self: *Response, page: *Page) !js.Promise { if (self.body_used) { return error.TypeError; } @@ -253,7 +253,7 @@ pub fn _bytes(self: *Response, page: *Page) !Env.Promise { return resolver.promise(); } -pub fn _json(self: *Response, page: *Page) !Env.Promise { +pub fn _json(self: *Response, page: *Page) !js.Promise { if (self.body_used) { return error.TypeError; } @@ -280,7 +280,7 @@ pub fn _json(self: *Response, page: *Page) !Env.Promise { return resolver.promise(); } -pub fn _text(self: *Response, page: *Page) !Env.Promise { +pub fn _text(self: *Response, page: *Page) !js.Promise { if (self.body_used) { return error.TypeError; } diff --git a/src/browser/fetch/Response.zig b/src/browser/fetch/Response.zig index d9530fb1..6320c557 100644 --- a/src/browser/fetch/Response.zig +++ b/src/browser/fetch/Response.zig @@ -17,6 +17,7 @@ // along with this program. If not, see . const std = @import("std"); +const js = @import("../js/js.zig"); const log = @import("../../log.zig"); const v8 = @import("v8"); @@ -29,7 +30,6 @@ const ReadableStream = @import("../streams/ReadableStream.zig"); const Headers = @import("Headers.zig"); const HeadersInit = @import("Headers.zig").HeadersInit; -const Env = @import("../env.zig").Env; const Mime = @import("../mime.zig").Mime; const Page = @import("../page.zig").Page; @@ -165,12 +165,12 @@ pub fn _clone(self: *const Response) !Response { }; } -pub fn _bytes(self: *Response, page: *Page) !Env.Promise { +pub fn _bytes(self: *Response, page: *Page) !js.Promise { if (self.body_used) { return error.TypeError; } - const resolver = Env.PromiseResolver{ + const resolver = js.PromiseResolver{ .js_context = page.main_context, .resolver = v8.PromiseResolver.init(page.main_context.v8_context), }; @@ -180,7 +180,7 @@ pub fn _bytes(self: *Response, page: *Page) !Env.Promise { return resolver.promise(); } -pub fn _json(self: *Response, page: *Page) !Env.Promise { +pub fn _json(self: *Response, page: *Page) !js.Promise { if (self.body_used) { return error.TypeError; } @@ -207,7 +207,7 @@ pub fn _json(self: *Response, page: *Page) !Env.Promise { return resolver.promise(); } -pub fn _text(self: *Response, page: *Page) !Env.Promise { +pub fn _text(self: *Response, page: *Page) !js.Promise { if (self.body_used) { return error.TypeError; } diff --git a/src/browser/fetch/fetch.zig b/src/browser/fetch/fetch.zig index e1e29a81..3b538fe2 100644 --- a/src/browser/fetch/fetch.zig +++ b/src/browser/fetch/fetch.zig @@ -19,7 +19,7 @@ const std = @import("std"); const log = @import("../../log.zig"); -const Env = @import("../env.zig").Env; +const js = @import("../js/js.zig"); const Page = @import("../page.zig").Page; const Http = @import("../../http/Http.zig"); @@ -45,7 +45,7 @@ pub const Interfaces = .{ pub const FetchContext = struct { page: *Page, arena: std.mem.Allocator, - promise_resolver: Env.PersistentPromiseResolver, + promise_resolver: js.PersistentPromiseResolver, method: Http.Method, url: []const u8, @@ -111,7 +111,7 @@ pub const FetchContext = struct { }; // https://developer.mozilla.org/en-US/docs/Web/API/Window/fetch -pub fn fetch(input: RequestInput, options: ?RequestInit, page: *Page) !Env.Promise { +pub fn fetch(input: RequestInput, options: ?RequestInit, page: *Page) !js.Promise { const arena = page.arena; const req = try Request.constructor(input, options, page); diff --git a/src/browser/html/AbortController.zig b/src/browser/html/AbortController.zig index fba46e9d..0f5b0f36 100644 --- a/src/browser/html/AbortController.zig +++ b/src/browser/html/AbortController.zig @@ -17,9 +17,9 @@ // along with this program. If not, see . const std = @import("std"); +const js = @import("../js/js.zig"); const log = @import("../../log.zig"); const parser = @import("../netsurf.zig"); -const Env = @import("../env.zig").Env; const Page = @import("../page.zig").Page; const EventTarget = @import("../dom/event_target.zig").EventTarget; @@ -113,7 +113,7 @@ pub const AbortSignal = struct { } const ThrowIfAborted = union(enum) { - exception: Env.Exception, + exception: js.Exception, undefined: void, }; pub fn _throwIfAborted(self: *const AbortSignal, page: *Page) ThrowIfAborted { diff --git a/src/browser/html/DataSet.zig b/src/browser/html/DataSet.zig index e5365733..e234f9c4 100644 --- a/src/browser/html/DataSet.zig +++ b/src/browser/html/DataSet.zig @@ -17,7 +17,8 @@ // along with this program. If not, see . const std = @import("std"); const parser = @import("../netsurf.zig"); -const Env = @import("../env.zig").Env; +const js = @import("../js/js.zig"); + const Page = @import("../page.zig").Page; const Allocator = std.mem.Allocator; @@ -26,7 +27,7 @@ const DataSet = @This(); element: *parser.Element, -pub fn named_get(self: *const DataSet, name: []const u8, _: *bool, page: *Page) !Env.UndefinedOr([]const u8) { +pub fn named_get(self: *const DataSet, name: []const u8, _: *bool, page: *Page) !js.UndefinedOr([]const u8) { const normalized_name = try normalize(page.call_arena, name); if (try parser.elementGetAttribute(self.element, normalized_name)) |value| { return .{ .value = value }; diff --git a/src/browser/html/History.zig b/src/browser/html/History.zig index 308bfb1f..8111ba44 100644 --- a/src/browser/html/History.zig +++ b/src/browser/html/History.zig @@ -19,7 +19,7 @@ const std = @import("std"); const log = @import("../../log.zig"); -const Env = @import("../env.zig").Env; +const js = @import("../js/js.zig"); const Page = @import("../page.zig").Page; // https://html.spec.whatwg.org/multipage/nav-history-apis.html#the-history-interface @@ -67,11 +67,11 @@ pub fn set_scrollRestoration(self: *History, mode: []const u8) void { self.scroll_restoration = ScrollRestorationMode.fromString(mode) orelse self.scroll_restoration; } -pub fn get_state(self: *History, page: *Page) !?Env.Value { +pub fn get_state(self: *History, page: *Page) !?js.Value { if (self.current) |curr| { const entry = self.stack.items[curr]; if (entry.state) |state| { - const value = try Env.Value.fromJson(page.main_context, state); + const value = try js.Value.fromJson(page.main_context, state); return value; } else { return null; @@ -113,7 +113,7 @@ fn _dispatchPopStateEvent(state: ?[]const u8, page: *Page) !void { ); } -pub fn _pushState(self: *History, state: Env.JsObject, _: ?[]const u8, _url: ?[]const u8, page: *Page) !void { +pub fn _pushState(self: *History, state: js.JsObject, _: ?[]const u8, _url: ?[]const u8, page: *Page) !void { const arena = page.session.arena; const json = try state.toJson(arena); @@ -123,7 +123,7 @@ pub fn _pushState(self: *History, state: Env.JsObject, _: ?[]const u8, _url: ?[] self.current = self.stack.items.len - 1; } -pub fn _replaceState(self: *History, state: Env.JsObject, _: ?[]const u8, _url: ?[]const u8, page: *Page) !void { +pub fn _replaceState(self: *History, state: js.JsObject, _: ?[]const u8, _url: ?[]const u8, page: *Page) !void { const arena = page.session.arena; if (self.current) |curr| { @@ -199,9 +199,9 @@ pub const PopStateEvent = struct { // `hasUAVisualTransition` is not implemented. It isn't baseline so this is okay. - pub fn get_state(self: *const PopStateEvent, page: *Page) !?Env.Value { + pub fn get_state(self: *const PopStateEvent, page: *Page) !?js.Value { if (self.state) |state| { - const value = try Env.Value.fromJson(page.main_context, state); + const value = try js.Value.fromJson(page.main_context, state); return value; } else { return null; diff --git a/src/browser/html/elements.zig b/src/browser/html/elements.zig index b74d7ca2..e34915c2 100644 --- a/src/browser/html/elements.zig +++ b/src/browser/html/elements.zig @@ -18,9 +18,9 @@ const std = @import("std"); const log = @import("../../log.zig"); +const js = @import("../js/js.zig"); const parser = @import("../netsurf.zig"); -const generate = @import("../../runtime/generate.zig"); -const Env = @import("../env.zig").Env; +const generate = @import("../js/generate.zig"); const Page = @import("../page.zig").Page; const urlStitch = @import("../../url.zig").URL.stitch; @@ -1000,22 +1000,22 @@ pub const HTMLScriptElement = struct { ); } - pub fn get_onload(self: *parser.Script, page: *Page) !?Env.Function { + pub fn get_onload(self: *parser.Script, page: *Page) !?js.Function { const state = page.getNodeState(@ptrCast(@alignCast(self))) orelse return null; return state.onload; } - pub fn set_onload(self: *parser.Script, function: ?Env.Function, page: *Page) !void { + pub fn set_onload(self: *parser.Script, function: ?js.Function, page: *Page) !void { const state = try page.getOrCreateNodeState(@ptrCast(@alignCast(self))); state.onload = function; } - pub fn get_onerror(self: *parser.Script, page: *Page) !?Env.Function { + pub fn get_onerror(self: *parser.Script, page: *Page) !?js.Function { const state = page.getNodeState(@ptrCast(@alignCast(self))) orelse return null; return state.onerror; } - pub fn set_onerror(self: *parser.Script, function: ?Env.Function, page: *Page) !void { + pub fn set_onerror(self: *parser.Script, function: ?js.Function, page: *Page) !void { const state = try page.getOrCreateNodeState(@ptrCast(@alignCast(self))); state.onerror = function; } diff --git a/src/browser/html/error_event.zig b/src/browser/html/error_event.zig index ee006e22..31558418 100644 --- a/src/browser/html/error_event.zig +++ b/src/browser/html/error_event.zig @@ -15,7 +15,7 @@ // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -const Env = @import("../env.zig").Env; +const js = @import("../js/js.zig"); const parser = @import("../netsurf.zig"); // https://developer.mozilla.org/en-US/docs/Web/API/ErrorEvent @@ -28,14 +28,14 @@ pub const ErrorEvent = struct { filename: []const u8, lineno: i32, colno: i32, - @"error": ?Env.JsObject, + @"error": ?js.JsObject, const ErrorEventInit = struct { message: []const u8 = "", filename: []const u8 = "", lineno: i32 = 0, colno: i32 = 0, - @"error": ?Env.JsObject = null, + @"error": ?js.JsObject = null, }; pub fn constructor(event_type: []const u8, opts: ?ErrorEventInit) !ErrorEvent { @@ -72,7 +72,7 @@ pub const ErrorEvent = struct { return self.colno; } - pub fn get_error(self: *const ErrorEvent) Env.UndefinedOr(Env.JsObject) { + pub fn get_error(self: *const ErrorEvent) js.UndefinedOr(js.JsObject) { if (self.@"error") |e| { return .{ .value = e }; } diff --git a/src/browser/html/media_query_list.zig b/src/browser/html/media_query_list.zig index 28216db3..4d05b7c9 100644 --- a/src/browser/html/media_query_list.zig +++ b/src/browser/html/media_query_list.zig @@ -16,8 +16,8 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . +const js = @import("../js/js.zig"); const parser = @import("../netsurf.zig"); -const Function = @import("../env.zig").Function; const EventTarget = @import("../dom/event_target.zig").EventTarget; // https://drafts.csswg.org/cssom-view/#the-mediaquerylist-interface @@ -39,7 +39,7 @@ pub const MediaQueryList = struct { return self.media; } - pub fn _addListener(_: *const MediaQueryList, _: Function) void {} + pub fn _addListener(_: *const MediaQueryList, _: js.Function) void {} - pub fn _removeListener(_: *const MediaQueryList, _: Function) void {} + pub fn _removeListener(_: *const MediaQueryList, _: js.Function) void {} }; diff --git a/src/browser/html/window.zig b/src/browser/html/window.zig index 19e21c8e..1ca681b3 100644 --- a/src/browser/html/window.zig +++ b/src/browser/html/window.zig @@ -18,9 +18,9 @@ const std = @import("std"); +const js = @import("../js/js.zig"); const log = @import("../../log.zig"); const parser = @import("../netsurf.zig"); -const Env = @import("../env.zig").Env; const Page = @import("../page.zig").Page; const Navigator = @import("navigator.zig").Navigator; @@ -37,8 +37,6 @@ const domcss = @import("../dom/css.zig"); const Css = @import("../css/css.zig").Css; const EventHandler = @import("../events/event.zig").EventHandler; -const Function = Env.Function; - const v8 = @import("v8"); const Request = @import("../fetch/Request.zig"); const fetchFn = @import("../fetch/fetch.zig").fetch; @@ -70,7 +68,7 @@ pub const Window = struct { css: Css = .{}, scroll_x: u32 = 0, scroll_y: u32 = 0, - onload_callback: ?Function = null, + onload_callback: ?js.Function = null, pub fn create(target: ?[]const u8, navigator: ?Navigator) !Window { var fbs = std.io.fixedBufferStream(""); @@ -101,12 +99,12 @@ pub const Window = struct { self.storage_shelf = shelf; } - pub fn _fetch(_: *Window, input: Request.RequestInput, options: ?Request.RequestInit, page: *Page) !Env.Promise { + pub fn _fetch(_: *Window, input: Request.RequestInput, options: ?Request.RequestInit, page: *Page) !js.Promise { return fetchFn(input, options, page); } /// Returns `onload_callback`. - pub fn get_onload(self: *const Window) ?Function { + pub fn get_onload(self: *const Window) ?js.Function { return self.onload_callback; } @@ -258,7 +256,7 @@ pub const Window = struct { return &self.css; } - pub fn _requestAnimationFrame(self: *Window, cbk: Function, page: *Page) !u32 { + pub fn _requestAnimationFrame(self: *Window, cbk: js.Function, page: *Page) !u32 { return self.createTimeout(cbk, 5, page, .{ .animation_frame = true, .name = "animationFrame", @@ -270,11 +268,11 @@ pub const Window = struct { _ = self.timers.remove(id); } - pub fn _setTimeout(self: *Window, cbk: Function, delay: ?u32, params: []Env.JsObject, page: *Page) !u32 { + pub fn _setTimeout(self: *Window, cbk: js.Function, delay: ?u32, params: []js.JsObject, page: *Page) !u32 { return self.createTimeout(cbk, delay, page, .{ .args = params, .name = "setTimeout" }); } - pub fn _setInterval(self: *Window, cbk: Function, delay: ?u32, params: []Env.JsObject, page: *Page) !u32 { + pub fn _setInterval(self: *Window, cbk: js.Function, delay: ?u32, params: []js.JsObject, page: *Page) !u32 { return self.createTimeout(cbk, delay, page, .{ .repeat = true, .args = params, .name = "setInterval" }); } @@ -286,11 +284,11 @@ pub const Window = struct { _ = self.timers.remove(id); } - pub fn _queueMicrotask(self: *Window, cbk: Function, page: *Page) !u32 { + pub fn _queueMicrotask(self: *Window, cbk: js.Function, page: *Page) !u32 { return self.createTimeout(cbk, 0, page, .{ .name = "queueMicrotask" }); } - pub fn _setImmediate(self: *Window, cbk: Function, page: *Page) !u32 { + pub fn _setImmediate(self: *Window, cbk: js.Function, page: *Page) !u32 { return self.createTimeout(cbk, 0, page, .{ .name = "setImmediate" }); } @@ -298,7 +296,7 @@ pub const Window = struct { _ = self.timers.remove(id); } - pub fn _matchMedia(_: *const Window, media: Env.String) !MediaQueryList { + pub fn _matchMedia(_: *const Window, media: js.String) !MediaQueryList { return .{ .matches = false, // TODO? .media = media.string, @@ -322,12 +320,12 @@ pub const Window = struct { const CreateTimeoutOpts = struct { name: []const u8, - args: []Env.JsObject = &.{}, + args: []js.JsObject = &.{}, repeat: bool = false, animation_frame: bool = false, low_priority: bool = false, }; - fn createTimeout(self: *Window, cbk: Function, delay_: ?u32, page: *Page, opts: CreateTimeoutOpts) !u32 { + fn createTimeout(self: *Window, cbk: js.Function, delay_: ?u32, page: *Page, opts: CreateTimeoutOpts) !u32 { const delay = delay_ orelse 0; if (self.timers.count() > 512) { return error.TooManyTimeout; @@ -347,9 +345,9 @@ pub const Window = struct { errdefer _ = self.timers.remove(timer_id); const args = opts.args; - var persisted_args: []Env.JsObject = &.{}; + var persisted_args: []js.JsObject = &.{}; if (args.len > 0) { - persisted_args = try page.arena.alloc(Env.JsObject, args.len); + persisted_args = try page.arena.alloc(js.JsObject, args.len); for (args, persisted_args) |a, *ca| { ca.* = try a.persist(); } @@ -476,13 +474,13 @@ const TimerCallback = struct { repeat: ?u32, // The JavaScript callback to execute - cbk: Function, + cbk: js.Function, animation_frame: bool = false, window: *Window, - args: []Env.JsObject = &.{}, + args: []js.JsObject = &.{}, fn run(ctx: *anyopaque) ?u32 { const self: *TimerCallback = @ptrCast(@alignCast(ctx)); @@ -496,7 +494,7 @@ const TimerCallback = struct { return null; } - var result: Function.Result = undefined; + var result: js.Function.Result = undefined; var call: anyerror!void = undefined; if (self.animation_frame) { diff --git a/src/runtime/generate.zig b/src/browser/js/generate.zig similarity index 98% rename from src/runtime/generate.zig rename to src/browser/js/generate.zig index c5000cba..e2cf010a 100644 --- a/src/runtime/generate.zig +++ b/src/browser/js/generate.zig @@ -190,7 +190,7 @@ test "generate: Union" { const value = Union(.{ Astruct, Bstruct, .{Cstruct} }); const ti = @typeInfo(value).@"union"; try std.testing.expectEqual(3, ti.fields.len); - try std.testing.expectEqualStrings("*runtime.generate.test.generate: Union.Astruct.Other", @typeName(ti.fields[0].type)); + try std.testing.expectEqualStrings("*browser.js.generate.test.generate: Union.Astruct.Other", @typeName(ti.fields[0].type)); try std.testing.expectEqualStrings(ti.fields[0].name, "Astruct"); try std.testing.expectEqual(*Bstruct, ti.fields[1].type); try std.testing.expectEqualStrings(ti.fields[1].name, "Bstruct"); diff --git a/src/browser/js/js.zig b/src/browser/js/js.zig new file mode 100644 index 00000000..d5982391 --- /dev/null +++ b/src/browser/js/js.zig @@ -0,0 +1,4331 @@ +// 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 generate = @import("generate.zig"); + +const SubType = @import("subtype.zig").SubType; +const Page = @import("../page.zig").Page; +const ScriptManager = @import("../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 Interfaces = generate.Tuple(.{ + @import("../crypto/crypto.zig").Crypto, + @import("../console/console.zig").Console, + @import("../css/css.zig").Interfaces, + @import("../cssom/cssom.zig").Interfaces, + @import("../dom/dom.zig").Interfaces, + @import("../dom/shadow_root.zig").ShadowRoot, + @import("../encoding/encoding.zig").Interfaces, + @import("../events/event.zig").Interfaces, + @import("../html/html.zig").Interfaces, + @import("../iterator/iterator.zig").Interfaces, + @import("../storage/storage.zig").Interfaces, + @import("../url/url.zig").Interfaces, + @import("../xhr/xhr.zig").Interfaces, + @import("../xhr/form_data.zig").Interfaces, + @import("../xhr/File.zig"), + @import("../xmlserializer/xmlserializer.zig").Interfaces, + @import("../fetch/fetch.zig").Interfaces, + @import("../streams/streams.zig").Interfaces, +}); +const Types = @typeInfo(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 = 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, + } }); +}; + +const TYPE_LOOKUP = TypeLookup{}; + +// 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 = blk: { + var table: [Types.len]u16 = undefined; + 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; +}; + +// 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 const Env = 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 Opts = struct {}; + + pub fn init(allocator: Allocator, platform: *const Platform, _: Opts) !*Env { + // 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(Env); + 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: *Env) 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: *Env, arena: Allocator, ctx: anytype) !Inspector { + return Inspector.init(arena, self.isolate, ctx); + } + + pub fn runMicrotasks(self: *const Env) void { + self.isolate.performMicrotasksCheckpoint(); + } + + pub fn pumpMessageLoop(self: *const Env) bool { + return self.platform.inner.pumpMessageLoop(self.isolate, false); + } + + pub fn runIdleTasks(self: *const Env) void { + return self.platform.inner.runIdleTasks(self.isolate, 1); + } + + pub fn newExecutionWorld(self: *Env) !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: *Env) 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: *Env) 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: *Env, + + // 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, page: *Page, 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{ + .page = page, + .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); + } + + page.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, + page: *Page, + 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