diff --git a/src/browser/ScriptManager.zig b/src/browser/ScriptManager.zig index 82e760133..07836c7bf 100644 --- a/src/browser/ScriptManager.zig +++ b/src/browser/ScriptManager.zig @@ -663,7 +663,7 @@ const Script = struct { .cacheable = cacheable, }); - const js_context = page.main_context; + const js_context = page.js; var try_catch: js.TryCatch = undefined; try_catch.init(js_context); defer try_catch.deinit(); @@ -707,10 +707,10 @@ const Script = struct { switch (callback) { .string => |str| { var try_catch: js.TryCatch = undefined; - try_catch.init(page.main_context); + try_catch.init(page.js); defer try_catch.deinit(); - _ = page.main_context.exec(str, typ) catch |err| { + _ = page.js.exec(str, typ) catch |err| { const msg = try_catch.err(page.arena) catch @errorName(err) orelse "unknown"; log.warn(.user_script, "script callback", .{ .url = self.url, diff --git a/src/browser/cssom/CSSStyleSheet.zig b/src/browser/cssom/CSSStyleSheet.zig index 13d2a0a62..05ba536af 100644 --- a/src/browser/cssom/CSSStyleSheet.zig +++ b/src/browser/cssom/CSSStyleSheet.zig @@ -79,9 +79,7 @@ pub fn _replace(self: *CSSStyleSheet, text: []const u8, page: *Page) !js.Promise // TODO: clear self.css_rules // parse text and re-populate self.css_rules - const resolver = page.main_context.createPromiseResolver(); - try resolver.resolve({}); - return resolver.promise(); + return page.js.resolvePromise({}); } pub fn _replaceSync(self: *CSSStyleSheet, text: []const u8) !void { diff --git a/src/browser/dom/Animation.zig b/src/browser/dom/Animation.zig index cd5d1702a..e10927a15 100644 --- a/src/browser/dom/Animation.zig +++ b/src/browser/dom/Animation.zig @@ -49,7 +49,7 @@ pub fn get_pending(self: *const Animation) bool { pub fn get_finished(self: *Animation, page: *Page) !js.Promise { if (self.finished_resolver == null) { - const resolver = page.main_context.createPromiseResolver(); + const resolver = page.js.createPromiseResolver(.none); try resolver.resolve(self); self.finished_resolver = resolver; } @@ -59,7 +59,7 @@ pub fn get_finished(self: *Animation, page: *Page) !js.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(); + const resolver = page.js.createPromiseResolver(.none); self.ready_resolver = resolver; } return self.ready_resolver.?.promise(); diff --git a/src/browser/dom/document.zig b/src/browser/dom/document.zig index 81ce57e4c..fc65ad0a8 100644 --- a/src/browser/dom/document.zig +++ b/src/browser/dom/document.zig @@ -304,7 +304,7 @@ pub const Document = struct { return obj; } - const obj = try page.main_context.newArray(0).persist(); + const obj = try page.js.createArray(0).persist(); state.adopted_style_sheets = obj; return obj; } diff --git a/src/browser/dom/shadow_root.zig b/src/browser/dom/shadow_root.zig index 946fd793d..594fd8bad 100644 --- a/src/browser/dom/shadow_root.zig +++ b/src/browser/dom/shadow_root.zig @@ -50,7 +50,7 @@ pub const ShadowRoot = struct { return obj; } - const obj = try page.main_context.newArray(0).persist(); + const obj = try page.js.createArray(0).persist(); self.adopted_style_sheets = obj; return obj; } diff --git a/src/browser/fetch/Headers.zig b/src/browser/fetch/Headers.zig index 82ddba5dc..4ee7d8f6f 100644 --- a/src/browser/fetch/Headers.zig +++ b/src/browser/fetch/Headers.zig @@ -24,7 +24,6 @@ const Page = @import("../page.zig").Page; const iterator = @import("../iterator/iterator.zig"); -const v8 = @import("v8"); // https://developer.mozilla.org/en-US/docs/Web/API/Headers const Headers = @This(); diff --git a/src/browser/fetch/Request.zig b/src/browser/fetch/Request.zig index 172c89765..f13a8cb8c 100644 --- a/src/browser/fetch/Request.zig +++ b/src/browser/fetch/Request.zig @@ -27,8 +27,6 @@ const Response = @import("./Response.zig"); const Http = @import("../../http/Http.zig"); const ReadableStream = @import("../streams/ReadableStream.zig"); -const v8 = @import("v8"); - const Headers = @import("Headers.zig"); const HeadersInit = @import("Headers.zig").HeadersInit; @@ -245,20 +243,15 @@ pub fn _bytes(self: *Response, page: *Page) !js.Promise { if (self.body_used) { return error.TypeError; } - - const resolver = page.main_context.createPromiseResolver(); - - try resolver.resolve(self.body); self.body_used = true; - return resolver.promise(); + return page.js.resolvePromise(self.body); } pub fn _json(self: *Response, page: *Page) !js.Promise { if (self.body_used) { return error.TypeError; } - - const resolver = page.main_context.createPromiseResolver(); + self.body_used = true; if (self.body) |body| { const p = std.json.parseFromSliceLeaky( @@ -271,25 +264,17 @@ pub fn _json(self: *Response, page: *Page) !js.Promise { return error.SyntaxError; }; - try resolver.resolve(p); - } else { - try resolver.resolve(null); + return page.js.resolvePromise(p); } - - self.body_used = true; - return resolver.promise(); + return page.js.resolvePromise(null); } pub fn _text(self: *Response, page: *Page) !js.Promise { if (self.body_used) { return error.TypeError; } - - const resolver = page.main_context.createPromiseResolver(); - - try resolver.resolve(self.body); self.body_used = true; - return resolver.promise(); + return page.js.resolvePromise(self.body); } const testing = @import("../../testing.zig"); diff --git a/src/browser/fetch/Response.zig b/src/browser/fetch/Response.zig index ccda4c068..69f1c39e2 100644 --- a/src/browser/fetch/Response.zig +++ b/src/browser/fetch/Response.zig @@ -20,8 +20,6 @@ const std = @import("std"); const js = @import("../js/js.zig"); const log = @import("../../log.zig"); -const v8 = @import("v8"); - const HttpClient = @import("../../http/Client.zig"); const Http = @import("../../http/Http.zig"); const URL = @import("../../url.zig").URL; @@ -170,14 +168,8 @@ pub fn _bytes(self: *Response, page: *Page) !js.Promise { return error.TypeError; } - const resolver = js.PromiseResolver{ - .context = page.main_context, - .resolver = v8.PromiseResolver.init(page.main_context.v8_context), - }; - - try resolver.resolve(self.body); self.body_used = true; - return resolver.promise(); + return page.js.resolvePromise(self.body); } pub fn _json(self: *Response, page: *Page) !js.Promise { @@ -185,9 +177,8 @@ pub fn _json(self: *Response, page: *Page) !js.Promise { return error.TypeError; } - const resolver = page.main_context.createPromiseResolver(); - if (self.body) |body| { + self.body_used = true; const p = std.json.parseFromSliceLeaky( std.json.Value, page.call_arena, @@ -198,25 +189,18 @@ pub fn _json(self: *Response, page: *Page) !js.Promise { return error.SyntaxError; }; - try resolver.resolve(p); - } else { - try resolver.resolve(null); + return page.js.resolvePromise(p); } - - self.body_used = true; - return resolver.promise(); + return page.js.resolvePromise(null); } pub fn _text(self: *Response, page: *Page) !js.Promise { if (self.body_used) { return error.TypeError; } - - const resolver = page.main_context.createPromiseResolver(); - - try resolver.resolve(self.body); self.body_used = true; - return resolver.promise(); + + return page.js.resolvePromise(self.body); } const testing = @import("../../testing.zig"); diff --git a/src/browser/fetch/fetch.zig b/src/browser/fetch/fetch.zig index 3b538fe29..8c9c760f3 100644 --- a/src/browser/fetch/fetch.zig +++ b/src/browser/fetch/fetch.zig @@ -131,7 +131,7 @@ pub fn fetch(input: RequestInput, options: ?RequestInit, page: *Page) !js.Promis try page.requestCookie(.{}).headersForRequest(arena, req.url, &headers); - const resolver = try page.main_context.createPersistentPromiseResolver(.page); + const resolver = try page.js.createPromiseResolver(.page); const fetch_ctx = try arena.create(FetchContext); fetch_ctx.* = .{ diff --git a/src/browser/html/AbortController.zig b/src/browser/html/AbortController.zig index 0f5b0f36f..05d4fe1e7 100644 --- a/src/browser/html/AbortController.zig +++ b/src/browser/html/AbortController.zig @@ -118,7 +118,7 @@ pub const AbortSignal = struct { }; pub fn _throwIfAborted(self: *const AbortSignal, page: *Page) ThrowIfAborted { if (self.aborted) { - const ex = page.main_context.throw(self.reason orelse DEFAULT_REASON); + const ex = page.js.throw(self.reason orelse DEFAULT_REASON); return .{ .exception = ex }; } return .{ .undefined = {} }; diff --git a/src/browser/html/History.zig b/src/browser/html/History.zig index a11962021..f8be6bb33 100644 --- a/src/browser/html/History.zig +++ b/src/browser/html/History.zig @@ -71,7 +71,7 @@ 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 js.Value.fromJson(page.main_context, state); + const value = try js.Value.fromJson(page.js, state); return value; } else { return null; @@ -201,7 +201,7 @@ pub const PopStateEvent = struct { pub fn get_state(self: *const PopStateEvent, page: *Page) !?js.Value { if (self.state) |state| { - const value = try js.Value.fromJson(page.main_context, state); + const value = try js.Value.fromJson(page.js, state); return value; } else { return null; diff --git a/src/browser/html/window.zig b/src/browser/html/window.zig index 407e83dd8..2aa03b945 100644 --- a/src/browser/html/window.zig +++ b/src/browser/html/window.zig @@ -37,7 +37,6 @@ const domcss = @import("../dom/css.zig"); const Css = @import("../css/css.zig").Css; const EventHandler = @import("../events/event.zig").EventHandler; -const v8 = @import("v8"); const Request = @import("../fetch/Request.zig"); const fetchFn = @import("../fetch/fetch.zig").fetch; diff --git a/src/browser/js/Caller.zig b/src/browser/js/Caller.zig index 635066e1d..8a3c63da6 100644 --- a/src/browser/js/Caller.zig +++ b/src/browser/js/Caller.zig @@ -77,9 +77,9 @@ pub fn constructor(self: *Caller, comptime Struct: type, comptime named_function const this = info.getThis(); if (@typeInfo(ReturnType) == .error_union) { const non_error_res = res catch |err| return err; - _ = try Context.mapZigInstanceToJs(self.v8_context, this, non_error_res); + _ = try self.context.mapZigInstanceToJs(this, non_error_res); } else { - _ = try Context.mapZigInstanceToJs(self.v8_context, this, res); + _ = try self.context.mapZigInstanceToJs(this, res); } info.getReturnValue().set(this); } @@ -209,7 +209,7 @@ fn namedSetOrDeleteCall(res: anytype, has_value: bool) !u8 { } fn nameToString(self: *Caller, name: v8.Name) ![]const u8 { - return js.valueToString(self.call_arena, .{ .handle = name.handle }, self.isolate, self.v8_context); + return self.context.valueToString(.{ .handle = name.handle }, .{}); } fn isSelfReceiver(comptime Struct: type, comptime named_function: NamedFunction) bool { @@ -258,7 +258,7 @@ pub fn handleError(self: *Caller, comptime Struct: type, comptime named_function if (comptime @import("builtin").mode == .Debug and @hasDecl(@TypeOf(info), "length")) { if (log.enabled(.js, .warn)) { - logFunctionCallError(self.call_arena, self.isolate, self.v8_context, err, named_function.full_name, info); + self.logFunctionCallError(err, named_function.full_name, info); } } @@ -461,6 +461,38 @@ fn getArgs(self: *const Caller, comptime Struct: type, comptime named_function: return args; } +// This is extracted to speed up compilation. When left inlined in handleError, +// this can add as much as 10 seconds of compilation time. +fn logFunctionCallError(self: *Caller, err: anyerror, function_name: []const u8, info: v8.FunctionCallbackInfo) void { + const args_dump = self.serializeFunctionArgs(info) catch "failed to serialize args"; + log.info(.js, "function call error", .{ + .name = function_name, + .err = err, + .args = args_dump, + .stack = self.context.stackTrace() catch |err1| @errorName(err1), + }); +} + +fn serializeFunctionArgs(self: *Caller, info: v8.FunctionCallbackInfo) ![]const u8 { + const separator = log.separator(); + const js_parameter_count = info.length(); + + const context = self.context; + var arr: std.ArrayListUnmanaged(u8) = .{}; + for (0..js_parameter_count) |i| { + const js_value = info.getArg(@intCast(i)); + const value_string = try context.valueToDetailString(js_value); + const value_type = try context.jsStringToZig(try js_value.typeOf(self.isolate), .{}); + try std.fmt.format(arr.writer(context.call_arena), "{s}{d}: {s} ({s})", .{ + separator, + i + 1, + value_string, + value_type, + }); + } + return arr.items; +} + // We want the function name, or more precisely, the "Struct.function" for // displaying helpful @compileError. // However, there's no way to get the name from a std.Builtin.Fn, so we create @@ -524,37 +556,6 @@ fn isPage(comptime T: type) bool { return T == *Page or T == *const Page; } -// This is extracted to speed up compilation. When left inlined in handleError, -// this can add as much as 10 seconds of compilation time. -fn logFunctionCallError(arena: Allocator, isolate: v8.Isolate, context: v8.Context, err: anyerror, function_name: []const u8, info: v8.FunctionCallbackInfo) void { - const args_dump = serializeFunctionArgs(arena, isolate, context, info) catch "failed to serialize args"; - log.info(.js, "function call error", .{ - .name = function_name, - .err = err, - .args = args_dump, - .stack = Context.stackForLogs(arena, isolate) catch |err1| @errorName(err1), - }); -} - -fn serializeFunctionArgs(arena: Allocator, isolate: v8.Isolate, context: v8.Context, info: v8.FunctionCallbackInfo) ![]const u8 { - const separator = log.separator(); - const js_parameter_count = info.length(); - - var arr: std.ArrayListUnmanaged(u8) = .{}; - for (0..js_parameter_count) |i| { - const js_value = info.getArg(@intCast(i)); - const value_string = try js.valueToDetailString(arena, js_value, isolate, context); - const value_type = try js.stringToZig(arena, try js_value.typeOf(isolate), isolate); - try std.fmt.format(arr.writer(arena), "{s}{d}: {s} ({s})", .{ - separator, - i + 1, - value_string, - value_type, - }); - } - return arr.items; -} - fn createTypeException(isolate: v8.Isolate, msg: []const u8) v8.Value { return v8.Exception.initTypeError(v8.String.initUtf8(isolate, msg)); } diff --git a/src/browser/js/Context.zig b/src/browser/js/Context.zig index 5ce9da48f..3eb8a632b 100644 --- a/src/browser/js/Context.zig +++ b/src/browser/js/Context.zig @@ -1,4 +1,6 @@ const std = @import("std"); +const builtin = @import("builtin"); + const js = @import("js.zig"); const v8 = js.v8; @@ -109,7 +111,19 @@ const ModuleEntry = struct { resolver_promise: ?PersistentPromise = null, }; -// no init, started with executor.createContext() +pub fn fromC(c_context: *const v8.C_Context) *Context { + const v8_context = v8.Context{ .handle = c_context }; + return @ptrFromInt(v8_context.getEmbedderData(1).castTo(v8.BigInt).getUint64()); +} + +pub fn fromIsolate(isolate: v8.Isolate) *Context { + const v8_context = isolate.getCurrentContext(); + return @ptrFromInt(v8_context.getEmbedderData(1).castTo(v8.BigInt).getUint64()); +} + +pub fn setupGlobal(self: *Context) !void { + _ = try self.mapZigInstanceToJs(self.v8_context.getGlobal(), &self.page.window); +} pub fn deinit(self: *Context) void { { @@ -189,11 +203,7 @@ pub fn valueToExistingObject(self: *const Context, value: anytype) !v8.Object { return persistent_object.castToObject(); } -pub fn stackTrace(self: *const Context) !?[]const u8 { - return stackForLogs(self.call_arena, self.isolate); -} - -// Executes the src +// == Executors == pub fn eval(self: *Context, src: []const u8, name: ?[]const u8) !void { _ = try self.exec(src, name); } @@ -239,10 +249,9 @@ pub fn module(self: *Context, comptime want_result: bool, src: []const u8, url: // 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 js.stringToZig(self.call_arena, req.getSpecifier(), isolate); + const specifier = try self.jsStringToZig(req.getSpecifier(), .{}); const normalized_specifier = try @import("../../url.zig").stitch( self.call_arena, specifier, @@ -306,7 +315,8 @@ pub fn module(self: *Context, comptime want_result: bool, src: []const u8, url: return if (comptime want_result) gop.value_ptr.* else {}; } -pub fn newArray(self: *Context, len: u32) js.Object { +// == Creators == +pub fn createArray(self: *Context, len: u32) js.Object { const arr = v8.Array.init(self.isolate, len); return .{ .context = self, @@ -314,8 +324,7 @@ pub fn newArray(self: *Context, len: u32) js.Object { }; } -// Wrap a v8.Exception -fn createException(self: *const Context, e: v8.Value) js.Exception { +pub fn createException(self: *const Context, e: v8.Value) js.Exception { return .{ .inner = e, .context = self, @@ -331,17 +340,171 @@ pub fn createValue(self: *const Context, value: v8.Value) js.Value { }; } -pub fn zigValueToJs(self: *const Context, value: anytype) !v8.Value { - return _zigValueToJs(self.templates, self.isolate, self.v8_context, value); +pub fn createFunction(self: *Context, js_value: v8.Value) !js.Function { + // caller should have made sure this was a function + std.debug.assert(js_value.isFunction()); + + const func = v8.Persistent(v8.Function).init(self.isolate, js_value.castTo(v8.Function)); + try self.trackCallback(func); + + return .{ + .func = func, + .context = self, + .id = js_value.castTo(v8.Object).getIdentityHash(), + }; +} + +pub fn throw(self: *Context, err: []const u8) js.Exception { + const js_value = js._createException(self.isolate, err); + return self.createException(js_value); } -// See _mapZigInstanceToJs, this is wrapper that can be called -// without a Context. This is possible because we store our -// context in the EmbedderData of the v8.Context. So, as long as -// we have a v8.Context, we can get the context. -pub fn mapZigInstanceToJs(v8_context: v8.Context, js_obj_or_template: anytype, value: anytype) !PersistentObject { - const context: *Context = @ptrFromInt(v8_context.getEmbedderData(1).castTo(v8.BigInt).getUint64()); - return context._mapZigInstanceToJs(js_obj_or_template, value); +pub fn zigValueToJs(self: *Context, value: anytype) !v8.Value { + const isolate = self.isolate; + + // Check if it's a "simple" type. This is extracted so that it can be + // reused by other parts of the code. "simple" types only require an + // isolate to create (specifically, they don't our templates array) + if (js.simpleZigValueToJs(isolate, value, false)) |js_value| { + return js_value; + } + + const v8_context = self.v8_context; + const T = @TypeOf(value); + switch (@typeInfo(T)) { + .void, .bool, .int, .comptime_int, .float, .comptime_float, .@"enum", .null => { + // Need to do this to keep the compiler happy + // simpleZigValueToJs handles all of these cases. + unreachable; + }, + .array => { + var js_arr = v8.Array.init(isolate, value.len); + var js_obj = js_arr.castTo(v8.Object); + for (value, 0..) |v, i| { + const js_val = try self.zigValueToJs(v); + if (js_obj.setValueAtIndex(v8_context, @intCast(i), js_val) == false) { + return error.FailedToCreateArray; + } + } + return js_obj.toValue(); + }, + .pointer => |ptr| switch (ptr.size) { + .one => { + const type_name = @typeName(ptr.child); + if (@hasField(types.Lookup, type_name)) { + const template = self.templates[@field(types.LOOKUP, type_name)]; + const js_obj = try self.mapZigInstanceToJs(template, value); + return js_obj.toValue(); + } + + const one_info = @typeInfo(ptr.child); + if (one_info == .array and one_info.array.child == u8) { + // Need to do this to keep the compiler happy + // If this was the case, simpleZigValueToJs would + // have handled it + unreachable; + } + }, + .slice => { + if (ptr.child == u8) { + // Need to do this to keep the compiler happy + // If this was the case, simpleZigValueToJs would + // have handled it + unreachable; + } + var js_arr = v8.Array.init(isolate, @intCast(value.len)); + var js_obj = js_arr.castTo(v8.Object); + + for (value, 0..) |v, i| { + const js_val = try self.zigValueToJs(v); + if (js_obj.setValueAtIndex(v8_context, @intCast(i), js_val) == false) { + return error.FailedToCreateArray; + } + } + return js_obj.toValue(); + }, + else => {}, + }, + .@"struct" => |s| { + const type_name = @typeName(T); + if (@hasField(types.Lookup, type_name)) { + const template = self.templates[@field(types.LOOKUP, type_name)]; + const js_obj = try self.mapZigInstanceToJs(template, value); + return js_obj.toValue(); + } + + if (T == js.Function) { + // we're returning a callback + return value.func.toValue(); + } + + if (T == js.Object) { + // we're returning a v8.Object + return value.js_obj.toValue(); + } + + if (T == js.Value) { + return value.value; + } + + if (T == js.Promise) { + // we're returning a v8.Promise + return value.toObject().toValue(); + } + + if (T == js.Exception) { + return isolate.throwException(value.inner); + } + + if (s.is_tuple) { + // return the tuple struct as an array + var js_arr = v8.Array.init(isolate, @intCast(s.fields.len)); + var js_obj = js_arr.castTo(v8.Object); + inline for (s.fields, 0..) |f, i| { + const js_val = try self.zigValueToJs(@field(value, f.name)); + if (js_obj.setValueAtIndex(v8_context, @intCast(i), js_val) == false) { + return error.FailedToCreateArray; + } + } + return js_obj.toValue(); + } + + // return the struct as a JS object + const js_obj = v8.Object.init(isolate); + inline for (s.fields) |f| { + const js_val = try self.zigValueToJs(@field(value, f.name)); + const key = v8.String.initUtf8(isolate, f.name); + if (!js_obj.setValue(v8_context, key, js_val)) { + return error.CreateObjectFailure; + } + } + return js_obj.toValue(); + }, + .@"union" => |un| { + if (T == std.json.Value) { + return zigJsonToJs(isolate, v8_context, value); + } + if (un.tag_type) |UnionTagType| { + inline for (un.fields) |field| { + if (value == @field(UnionTagType, field.name)) { + return self.zigValueToJs(@field(value, field.name)); + } + } + unreachable; + } + @compileError("Cannot use untagged union: " ++ @typeName(T)); + }, + .optional => { + if (value) |v| { + return self.zigValueToJs(v); + } + return v8.initNull(isolate).toValue(); + }, + .error_union => return self.zigValueToJs(try value), + else => {}, + } + + @compileError("A function returns an unsupported type: " ++ @typeName(T)); } // To turn a Zig instance into a v8 object, we need to do a number of things. @@ -357,7 +520,7 @@ pub fn mapZigInstanceToJs(v8_context: v8.Context, js_obj_or_template: anytype, v // 4 - Store our TaggedAnyOpaque into the persistent object // 5 - Update our identity_map (so that, if we return this same instance again, // we can just grab it from the identity_map) -pub fn _mapZigInstanceToJs(self: *Context, js_obj_or_template: anytype, value: anytype) !PersistentObject { +pub fn mapZigInstanceToJs(self: *Context, js_obj_or_template: anytype, value: anytype) !PersistentObject { const v8_context = self.v8_context; const context_arena = self.context_arena; @@ -367,7 +530,7 @@ pub fn _mapZigInstanceToJs(self: *Context, js_obj_or_template: anytype, value: a // Struct, has to be placed on the heap const heap = try context_arena.create(T); heap.* = value; - return self._mapZigInstanceToJs(js_obj_or_template, heap); + return self.mapZigInstanceToJs(js_obj_or_template, heap); }, .pointer => |ptr| { const gop = try self.identity_map.getOrPut(context_arena, @intFromPtr(value)); @@ -497,10 +660,10 @@ pub fn jsValueToZig(self: *Context, comptime named_function: NamedFunction, comp if (ptr.child == u8) { if (ptr.sentinel()) |s| { if (comptime s == 0) { - return js.valueToStringZ(self.call_arena, js_value, self.isolate, self.v8_context); + return self.valueToStringZ(js_value, .{}); } } else { - return js.valueToString(self.call_arena, js_value, self.isolate, self.v8_context); + return self.valueToString(js_value, .{}); } } @@ -605,7 +768,7 @@ fn jsValueToStruct(self: *Context, comptime named_function: NamedFunction, compt } if (T == js.String) { - return .{ .string = try js.valueToString(self.context_arena, js_value, self.isolate, self.v8_context) }; + return .{ .string = try self.valueToString(js_value, .{ .allocator = self.context_arena }) }; } const js_obj = js_value.castTo(v8.Object); @@ -733,285 +896,182 @@ fn jsValueToTypedArray(_: *Context, comptime T: type, js_value: v8.Value) !?[]T return error.InvalidArgument; } -pub fn createFunction(self: *Context, js_value: v8.Value) !js.Function { - // caller should have made sure this was a function - std.debug.assert(js_value.isFunction()); +// == Stringifiers == +const valueToStringOpts = struct { + allocator: ?Allocator = null, +}; +pub fn valueToString(self: *const Context, value: v8.Value, opts: valueToStringOpts) ![]u8 { + const allocator = opts.allocator orelse self.call_arena; + if (value.isSymbol()) { + // symbol's can't be converted to a string + return allocator.dupe(u8, "$Symbol"); + } + const str = try value.toString(self.v8_context); + return self.jsStringToZig(str, .{ .allocator = allocator }); +} - const func = v8.Persistent(v8.Function).init(self.isolate, js_value.castTo(v8.Function)); - try self.trackCallback(func); +pub fn valueToStringZ(self: *const Context, value: v8.Value, opts: valueToStringOpts) ![:0]u8 { + const allocator = opts.allocator orelse self.call_arena; + const str = try value.toString(self.v8_context); + const len = str.lenUtf8(self.isolate); + const buf = try allocator.allocSentinel(u8, len, 0); + const n = str.writeUtf8(self.isolate, buf); + std.debug.assert(n == len); + return buf; +} - return .{ - .func = func, - .context = self, - .id = js_value.castTo(v8.Object).getIdentityHash(), - }; +const JsStringToZigOpts = struct { + allocator: ?Allocator = null, +}; +pub fn jsStringToZig(self: *const Context, str: v8.String, opts: JsStringToZigOpts) ![]u8 { + const allocator = opts.allocator orelse self.call_arena; + const len = str.lenUtf8(self.isolate); + const buf = try allocator.alloc(u8, len); + const n = str.writeUtf8(self.isolate, buf); + std.debug.assert(n == len); + return buf; } -pub fn createPromiseResolver(self: *Context) js.PromiseResolver { - return .{ - .context = self, - .resolver = v8.PromiseResolver.init(self.v8_context), - }; +pub fn valueToDetailString(self: *const Context, value: v8.Value) ![]u8 { + var str: ?v8.String = null; + const v8_context = self.v8_context; + + if (value.isObject() and !value.isFunction()) blk: { + str = v8.Json.stringify(v8_context, value, null) catch break :blk; + + if (str.?.lenUtf8(self.isolate) == 2) { + // {} isn't useful, null this so that we can get the toDetailString + // (which might also be useless, but maybe not) + str = null; + } + } + + if (str == null) { + str = try value.toDetailString(v8_context); + } + + const s = try self.jsStringToZig(str.?, .{}); + if (comptime builtin.mode == .Debug) { + if (std.mem.eql(u8, s, "[object Object]")) { + if (self.debugValueToString(value.castTo(v8.Object))) |ds| { + return ds; + } else |err| { + log.err(.js, "debug serialize value", .{ .err = err }); + } + } + } + return s; +} + +fn debugValueToString(self: *const Context, js_obj: v8.Object) ![]u8 { + if (comptime builtin.mode != .Debug) { + @compileError("debugValue can only be called in debug mode"); + } + const v8_context = self.v8_context; + + const names_arr = js_obj.getOwnPropertyNames(v8_context); + const names_obj = names_arr.castTo(v8.Object); + const len = names_arr.length(); + + var arr: std.ArrayListUnmanaged(u8) = .empty; + var writer = arr.writer(self.call_arena); + try writer.writeAll("(JSON.stringify failed, dumping top-level fields)\n"); + for (0..len) |i| { + const field_name = try names_obj.getAtIndex(v8_context, @intCast(i)); + const field_value = try js_obj.getValue(v8_context, field_name); + const name = try self.valueToString(field_name, .{}); + const value = try self.valueToString(field_value, .{}); + try writer.writeAll(name); + try writer.writeAll(": "); + if (std.mem.indexOfAny(u8, value, &std.ascii.whitespace) == null) { + try writer.writeAll(value); + } else { + try writer.writeByte('"'); + try writer.writeAll(value); + try writer.writeByte('"'); + } + try writer.writeByte(' '); + } + return arr.items; +} + +pub fn stackTrace(self: *const Context) !?[]const u8 { + std.debug.assert(@import("builtin").mode == .Debug); + const isolate = self.isolate; + const separator = log.separator(); + + var buf: std.ArrayListUnmanaged(u8) = .empty; + var writer = buf.writer(self.call_arena); + + const stack_trace = v8.StackTrace.getCurrentStackTrace(isolate, 30); + const frame_count = stack_trace.getFrameCount(); + + for (0..frame_count) |i| { + const frame = stack_trace.getFrame(isolate, @intCast(i)); + if (frame.getScriptName()) |name| { + const script = try self.jsStringToZig(name, .{}); + try writer.print("{s}{s}:{d}", .{ separator, script, frame.getLineNumber() }); + } else { + try writer.print("{s}:{d}", .{ separator, frame.getLineNumber() }); + } + } + return buf.items; } -fn rejectPromise(self: *Context, msg: []const u8) v8.Promise { +// == Promise Helpers == +pub fn rejectPromise(self: *Context, value: anytype) js.Promise { const ctx = self.v8_context; - var resolver = v8.PromiseResolver.init(ctx); + const js_value = try self.zigValueToJs(value); - const error_msg = v8.String.initUtf8(self.isolate, msg); - _ = resolver.reject(ctx, error_msg.toValue()); + var resolver = v8.PromiseResolver.init(ctx); + _ = resolver.reject(ctx, js_value); return resolver.getPromise(); } -fn resolvePromise(self: *Context, value: v8.Value) v8.Promise { +pub fn resolvePromise(self: *Context, value: anytype) !js.Promise { const ctx = self.v8_context; + const js_value = try self.zigValueToJs(value); + var resolver = v8.PromiseResolver.init(ctx); - _ = resolver.resolve(ctx, value); + _ = resolver.resolve(ctx, js_value); + return resolver.getPromise(); } // creates a PersistentPromiseResolver, taking in a lifetime parameter. // If the lifetime is page, the page will clean up the PersistentPromiseResolver. // If the lifetime is self, you will be expected to deinitalize the PersistentPromiseResolver. -pub fn createPersistentPromiseResolver( - self: *Context, - lifetime: enum { self, page }, -) !js.PersistentPromiseResolver { - const resolver = v8.Persistent(v8.PromiseResolver).init(self.isolate, v8.PromiseResolver.init(self.v8_context)); - - if (lifetime == .page) { - try self.persisted_promise_resolvers.append(self.context_arena, resolver); +const PromiseResolverLifetime = enum { + none, + self, // it's a persisted promise, but it'll be managed by the caller + page, // it's a persisted promise, tied to the page lifetime +}; +fn PromiseResolverType(comptime lifetime: PromiseResolverLifetime) type { + if (lifetime == .none) { + return js.PromiseResolver; } - - return .{ .context = self, .resolver = resolver }; + return error{OutOfMemory}!js.PersistentPromiseResolver; } +pub fn createPromiseResolver(self: *Context, comptime lifetime: PromiseResolverLifetime) PromiseResolverType(lifetime) { + const resolver = v8.PromiseResolver.init(self.v8_context); + if (comptime lifetime == .none) { + return .{ .context = self, .resolver = resolver }; + } -// Probing is part of trying to map a JS value to a Zig union. There's -// a lot of ambiguity in this process, in part because some JS values -// can almost always be coerced. For example, anything can be coerced -// into an integer (it just becomes 0), or a float (becomes NaN) or a -// string. -// -// The way we'll do this is that, if there's a direct match, we'll use it -// If there's a potential match, we'll keep looking for a direct match -// and only use the (first) potential match as a fallback. -// -// Finally, I considered adding this probing directly into jsValueToZig -// but I decided doing this separately was better. However, the goal is -// obviously that probing is consistent with jsValueToZig. -fn ProbeResult(comptime T: type) type { - return union(enum) { - // The js_value maps directly to T - value: T, + const persisted = v8.Persistent(v8.PromiseResolver).init(self.isolate, resolver); - // The value is a T. This is almost the same as returning value: T, - // but the caller still has to get T by calling jsValueToZig. - // We prefer returning .{.ok => {}}, to avoid reducing duplication - // with jsValueToZig, but in some cases where probing has a cost - // AND yields the value anyways, we'll use .{.value = T}. - ok: void, + if (comptime lifetime == .page) { + try self.persisted_promise_resolvers.append(self.context_arena, persisted); + } - // the js_value is compatible with T (i.e. a int -> float), - compatible: void, - - // the js_value can be coerced to T (this is a lower precedence - // than compatible) - coerce: void, - - // the js_value cannot be turned into T - invalid: void, - }; -} -fn probeJsValueToZig(self: *Context, comptime named_function: NamedFunction, comptime T: type, js_value: v8.Value) !ProbeResult(T) { - switch (@typeInfo(T)) { - .optional => |o| { - if (js_value.isNullOrUndefined()) { - return .{ .value = null }; - } - return self.probeJsValueToZig(named_function, o.child, js_value); - }, - .float => { - if (js_value.isNumber() or js_value.isNumberObject()) { - if (js_value.isInt32() or js_value.isUint32() or js_value.isBigInt() or js_value.isBigIntObject()) { - // int => float is a reasonable match - return .{ .compatible = {} }; - } - return .{ .ok = {} }; - } - // anything can be coerced into a float, it becomes NaN - return .{ .coerce = {} }; - }, - .int => { - if (js_value.isNumber() or js_value.isNumberObject()) { - if (js_value.isInt32() or js_value.isUint32() or js_value.isBigInt() or js_value.isBigIntObject()) { - return .{ .ok = {} }; - } - // float => int is kind of reasonable, I guess - return .{ .compatible = {} }; - } - // anything can be coerced into a int, it becomes 0 - return .{ .coerce = {} }; - }, - .bool => { - if (js_value.isBoolean() or js_value.isBooleanObject()) { - return .{ .ok = {} }; - } - // anything can be coerced into a boolean, it will become - // true or false based on..some complex rules I don't know. - return .{ .coerce = {} }; - }, - .pointer => |ptr| switch (ptr.size) { - .one => { - if (!js_value.isObject()) { - return .{ .invalid = {} }; - } - if (@hasField(types.Lookup, @typeName(ptr.child))) { - const js_obj = js_value.castTo(v8.Object); - // There's a bit of overhead in doing this, so instead - // of having a version of typeTaggedAnyOpaque which - // returns a boolean or an optional, we rely on the - // main implementation and just handle the error. - const attempt = self.typeTaggedAnyOpaque(named_function, *types.Receiver(ptr.child), js_obj); - if (attempt) |value| { - return .{ .value = value }; - } else |_| { - return .{ .invalid = {} }; - } - } - // probably an error, but not for us to deal with - return .{ .invalid = {} }; - }, - .slice => { - if (js_value.isTypedArray()) { - switch (ptr.child) { - u8 => if (ptr.sentinel() == null) { - if (js_value.isUint8Array() or js_value.isUint8ClampedArray()) { - return .{ .ok = {} }; - } - }, - i8 => if (js_value.isInt8Array()) { - return .{ .ok = {} }; - }, - u16 => if (js_value.isUint16Array()) { - return .{ .ok = {} }; - }, - i16 => if (js_value.isInt16Array()) { - return .{ .ok = {} }; - }, - u32 => if (js_value.isUint32Array()) { - return .{ .ok = {} }; - }, - i32 => if (js_value.isInt32Array()) { - return .{ .ok = {} }; - }, - u64 => if (js_value.isBigUint64Array()) { - return .{ .ok = {} }; - }, - i64 => if (js_value.isBigInt64Array()) { - return .{ .ok = {} }; - }, - else => {}, - } - return .{ .invalid = {} }; - } - - if (ptr.child == u8) { - if (js_value.isString()) { - return .{ .ok = {} }; - } - // anything can be coerced into a string - return .{ .coerce = {} }; - } - - if (!js_value.isArray()) { - return .{ .invalid = {} }; - } - - // This can get tricky. - const js_arr = js_value.castTo(v8.Array); - - if (js_arr.length() == 0) { - // not so tricky in this case. - return .{ .value = &.{} }; - } - - // We settle for just probing the first value. Ok, actually - // not tricky in this case either. - const v8_context = self.v8_context; - const js_obj = js_arr.castTo(v8.Object); - switch (try self.probeJsValueToZig(named_function, ptr.child, try js_obj.getAtIndex(v8_context, 0))) { - .value, .ok => return .{ .ok = {} }, - .compatible => return .{ .compatible = {} }, - .coerce => return .{ .coerce = {} }, - .invalid => return .{ .invalid = {} }, - } - }, - else => {}, - }, - .array => |arr| { - // Retrieve fixed-size array as slice then probe - const slice_type = []arr.child; - switch (try self.probeJsValueToZig(named_function, slice_type, js_value)) { - .value => |slice_value| { - if (slice_value.len == arr.len) { - return .{ .value = @as(*T, @ptrCast(slice_value.ptr)).* }; - } - return .{ .invalid = {} }; - }, - .ok => { - // Exact length match, we could allow smaller arrays as .compatible, but we would not be able to communicate how many were written - if (js_value.isArray()) { - const js_arr = js_value.castTo(v8.Array); - if (js_arr.length() == arr.len) { - return .{ .ok = {} }; - } - } else if (js_value.isString() and arr.child == u8) { - const str = try js_value.toString(self.v8_context); - if (str.lenUtf8(self.isolate) == arr.len) { - return .{ .ok = {} }; - } - } - return .{ .invalid = {} }; - }, - .compatible => return .{ .compatible = {} }, - .coerce => return .{ .coerce = {} }, - .invalid => return .{ .invalid = {} }, - } - }, - .@"struct" => { - // We don't want to duplicate the code for this, so we call - // the actual conversion function. - const value = (try self.jsValueToStruct(named_function, T, js_value)) orelse { - return .{ .invalid = {} }; - }; - return .{ .value = value }; - }, - else => {}, - } - - return .{ .invalid = {} }; -} - -pub fn throw(self: *Context, err: []const u8) js.Exception { - const js_value = js._createException(self.isolate, err); - return self.createException(js_value); -} - -pub fn initializeImportMeta(self: *Context, m: v8.Module, meta: v8.Object) !void { - const url = self.module_identifier.get(m.getIdentityHash()) orelse { - // Shouldn't be possible. - return error.UnknownModuleReferrer; - }; - - const js_key = v8.String.initUtf8(self.isolate, "url"); - const js_value = try self.zigValueToJs(url); - const res = meta.defineOwnProperty(self.v8_context, js_key.toName(), js_value, 0) orelse false; - if (!res) { - return error.FailedToSet; - } -} + return .{ + .context = self, + .resolver = persisted, + }; +} +// == Callbacks == // Callback from V8, asking us to load a module. The "specifier" is // the src of the module to load. fn resolveModuleCallback( @@ -1022,10 +1082,9 @@ fn resolveModuleCallback( ) callconv(.c) ?*const v8.C_Module { _ = import_attributes; - const v8_context = v8.Context{ .handle = c_context.? }; - const self: *Context = @ptrFromInt(v8_context.getEmbedderData(1).castTo(v8.BigInt).getUint64()); + const self = fromC(c_context.?); - const specifier = js.stringToZig(self.call_arena, .{ .handle = c_specifier.? }, self.isolate) catch |err| { + const specifier = self.jsStringToZig(.{ .handle = c_specifier.? }, .{}) catch |err| { log.err(.js, "resolve module", .{ .err = err }); return null; }; @@ -1040,6 +1099,66 @@ fn resolveModuleCallback( }; } +pub fn dynamicModuleCallback( + c_context: ?*const v8.c.Context, + host_defined_options: ?*const v8.c.Data, + resource_name: ?*const v8.c.Value, + v8_specifier: ?*const v8.c.String, + import_attrs: ?*const v8.c.FixedArray, +) callconv(.c) ?*v8.c.Promise { + _ = host_defined_options; + _ = import_attrs; + + const self = fromC(c_context.?); + + const resource = self.jsStringToZig(.{ .handle = resource_name.? }, .{}) catch |err| { + log.err(.app, "OOM", .{ .err = err, .src = "dynamicModuleCallback1" }); + return @constCast(self.rejectPromise("Out of memory").handle); + }; + + const specifier = self.jsStringToZig(.{ .handle = v8_specifier.? }, .{}) catch |err| { + log.err(.app, "OOM", .{ .err = err, .src = "dynamicModuleCallback2" }); + return @constCast(self.rejectPromise("Out of memory").handle); + }; + + const normalized_specifier = @import("../../url.zig").stitch( + self.context_arena, // might need to survive until the module is loaded + specifier, + resource, + .{ .alloc = .if_needed, .null_terminated = true }, + ) catch |err| { + log.err(.app, "OOM", .{ .err = err, .src = "dynamicModuleCallback3" }); + return @constCast(self.rejectPromise("Out of memory").handle); + }; + + const promise = self._dynamicModuleCallback(normalized_specifier) catch |err| blk: { + log.err(.js, "dynamic module callback", .{ + .err = err, + }); + break :blk self.rejectPromise("Failed to load module"); + }; + return @constCast(promise.handle); +} + +pub fn metaObjectCallback(c_context: ?*v8.C_Context, c_module: ?*v8.C_Module, c_meta: ?*v8.C_Value) callconv(.c) void { + const self = fromC(c_context.?); + const m = v8.Module{ .handle = c_module.? }; + const meta = v8.Object{ .handle = c_meta.? }; + + const url = self.module_identifier.get(m.getIdentityHash()) orelse { + // Shouldn't be possible. + log.err(.js, "import meta", .{ .err = error.UnknownModuleReferrer }); + return; + }; + + const js_key = v8.String.initUtf8(self.isolate, "url"); + const js_value = try self.zigValueToJs(url); + const res = meta.defineOwnProperty(self.v8_context, js_key.toName(), js_value, 0) orelse false; + if (!res) { + log.err(.js, "import meta", .{ .err = error.FailedToSet }); + } +} + fn _resolveModuleCallback(self: *Context, referrer: v8.Module, specifier: []const u8) !?*const v8.C_Module { const referrer_path = self.module_identifier.get(referrer.getIdentityHash()) orelse { // Shouldn't be possible. @@ -1096,49 +1215,6 @@ fn _resolveModuleCallback(self: *Context, referrer: v8.Module, specifier: []cons return entry.module.?.handle; } -pub fn dynamicModuleCallback( - v8_ctx: ?*const v8.c.Context, - host_defined_options: ?*const v8.c.Data, - resource_name: ?*const v8.c.Value, - v8_specifier: ?*const v8.c.String, - import_attrs: ?*const v8.c.FixedArray, -) callconv(.c) ?*v8.c.Promise { - _ = host_defined_options; - _ = import_attrs; - - const ctx: v8.Context = .{ .handle = v8_ctx.? }; - const self: *Context = @ptrFromInt(ctx.getEmbedderData(1).castTo(v8.BigInt).getUint64()); - const isolate = self.isolate; - - const resource = js.stringToZig(self.call_arena, .{ .handle = resource_name.? }, isolate) catch |err| { - log.err(.app, "OOM", .{ .err = err, .src = "dynamicModuleCallback1" }); - return @constCast(self.rejectPromise("Out of memory").handle); - }; - - const specifier = js.stringToZig(self.call_arena, .{ .handle = v8_specifier.? }, isolate) catch |err| { - log.err(.app, "OOM", .{ .err = err, .src = "dynamicModuleCallback2" }); - return @constCast(self.rejectPromise("Out of memory").handle); - }; - - const normalized_specifier = @import("../../url.zig").stitch( - self.context_arena, // might need to survive until the module is loaded - specifier, - resource, - .{ .alloc = .if_needed, .null_terminated = true }, - ) catch |err| { - log.err(.app, "OOM", .{ .err = err, .src = "dynamicModuleCallback3" }); - return @constCast(self.rejectPromise("Out of memory").handle); - }; - - const promise = self._dynamicModuleCallback(normalized_specifier) catch |err| blk: { - log.err(.js, "dynamic module callback", .{ - .err = err, - }); - break :blk self.rejectPromise("Failed to load module"); - }; - return @constCast(promise.handle); -} - // Will get passed to ScriptManager and then passed back to us when // the src of the module is loaded const DynamicModuleResolveState = struct { @@ -1323,6 +1399,8 @@ fn resolveDynamicModule(self: *Context, state: *DynamicModuleResolveState, modul }; } +// == Zig <-> JS == + // Reverses the mapZigInstanceToJs, making sure that our TaggedAnyOpaque // contains a ptr to the correct type. pub fn typeTaggedAnyOpaque(self: *const Context, comptime named_function: NamedFunction, comptime R: type, js_obj: v8.Object) !R { @@ -1395,183 +1473,210 @@ pub fn typeTaggedAnyOpaque(self: *const Context, comptime named_function: NamedF } } -// An interface for types that want to have their jsDeinit function to be -// called when the call context ends -const DestructorCallback = struct { - ptr: *anyopaque, - destructorFn: *const fn (ptr: *anyopaque) void, - - fn init(ptr: anytype) DestructorCallback { - const T = @TypeOf(ptr); - const ptr_info = @typeInfo(T); - - const gen = struct { - pub fn destructor(pointer: *anyopaque) void { - const self: T = @ptrCast(@alignCast(pointer)); - return ptr_info.pointer.child.destructor(self); - } - }; +// Probing is part of trying to map a JS value to a Zig union. There's +// a lot of ambiguity in this process, in part because some JS values +// can almost always be coerced. For example, anything can be coerced +// into an integer (it just becomes 0), or a float (becomes NaN) or a +// string. +// +// The way we'll do this is that, if there's a direct match, we'll use it +// If there's a potential match, we'll keep looking for a direct match +// and only use the (first) potential match as a fallback. +// +// Finally, I considered adding this probing directly into jsValueToZig +// but I decided doing this separately was better. However, the goal is +// obviously that probing is consistent with jsValueToZig. +fn ProbeResult(comptime T: type) type { + return union(enum) { + // The js_value maps directly to T + value: T, - return .{ - .ptr = ptr, - .destructorFn = gen.destructor, - }; - } + // The value is a T. This is almost the same as returning value: T, + // but the caller still has to get T by calling jsValueToZig. + // We prefer returning .{.ok => {}}, to avoid reducing duplication + // with jsValueToZig, but in some cases where probing has a cost + // AND yields the value anyways, we'll use .{.value = T}. + ok: void, - pub fn destructor(self: DestructorCallback) void { - self.destructorFn(self.ptr); - } -}; + // the js_value is compatible with T (i.e. a int -> float), + compatible: void, -// Turns a Zig value into a JS one. -fn _zigValueToJs( - templates: []v8.FunctionTemplate, - isolate: v8.Isolate, - v8_context: v8.Context, - value: anytype, -) anyerror!v8.Value { - // Check if it's a "simple" type. This is extracted so that it can be - // reused by other parts of the code. "simple" types only require an - // isolate to create (specifically, they don't our templates array) - if (js.simpleZigValueToJs(isolate, value, false)) |js_value| { - return js_value; - } + // the js_value can be coerced to T (this is a lower precedence + // than compatible) + coerce: void, - const T = @TypeOf(value); + // the js_value cannot be turned into T + invalid: void, + }; +} +fn probeJsValueToZig(self: *Context, comptime named_function: NamedFunction, comptime T: type, js_value: v8.Value) !ProbeResult(T) { switch (@typeInfo(T)) { - .void, .bool, .int, .comptime_int, .float, .comptime_float, .@"enum", .null => { - // Need to do this to keep the compiler happy - // simpleZigValueToJs handles all of these cases. - unreachable; + .optional => |o| { + if (js_value.isNullOrUndefined()) { + return .{ .value = null }; + } + return self.probeJsValueToZig(named_function, o.child, js_value); }, - .array => { - var js_arr = v8.Array.init(isolate, value.len); - var js_obj = js_arr.castTo(v8.Object); - for (value, 0..) |v, i| { - const js_val = try _zigValueToJs(templates, isolate, v8_context, v); - if (js_obj.setValueAtIndex(v8_context, @intCast(i), js_val) == false) { - return error.FailedToCreateArray; + .float => { + if (js_value.isNumber() or js_value.isNumberObject()) { + if (js_value.isInt32() or js_value.isUint32() or js_value.isBigInt() or js_value.isBigIntObject()) { + // int => float is a reasonable match + return .{ .compatible = {} }; + } + return .{ .ok = {} }; + } + // anything can be coerced into a float, it becomes NaN + return .{ .coerce = {} }; + }, + .int => { + if (js_value.isNumber() or js_value.isNumberObject()) { + if (js_value.isInt32() or js_value.isUint32() or js_value.isBigInt() or js_value.isBigIntObject()) { + return .{ .ok = {} }; } + // float => int is kind of reasonable, I guess + return .{ .compatible = {} }; } - return js_obj.toValue(); + // anything can be coerced into a int, it becomes 0 + return .{ .coerce = {} }; + }, + .bool => { + if (js_value.isBoolean() or js_value.isBooleanObject()) { + return .{ .ok = {} }; + } + // anything can be coerced into a boolean, it will become + // true or false based on..some complex rules I don't know. + return .{ .coerce = {} }; }, .pointer => |ptr| switch (ptr.size) { .one => { - const type_name = @typeName(ptr.child); - if (@hasField(types.Lookup, type_name)) { - const template = templates[@field(types.LOOKUP, type_name)]; - const js_obj = try Context.mapZigInstanceToJs(v8_context, template, value); - return js_obj.toValue(); + if (!js_value.isObject()) { + return .{ .invalid = {} }; } - - const one_info = @typeInfo(ptr.child); - if (one_info == .array and one_info.array.child == u8) { - // Need to do this to keep the compiler happy - // If this was the case, simpleZigValueToJs would - // have handled it - unreachable; + if (@hasField(types.Lookup, @typeName(ptr.child))) { + const js_obj = js_value.castTo(v8.Object); + // There's a bit of overhead in doing this, so instead + // of having a version of typeTaggedAnyOpaque which + // returns a boolean or an optional, we rely on the + // main implementation and just handle the error. + const attempt = self.typeTaggedAnyOpaque(named_function, *types.Receiver(ptr.child), js_obj); + if (attempt) |value| { + return .{ .value = value }; + } else |_| { + return .{ .invalid = {} }; + } } + // probably an error, but not for us to deal with + return .{ .invalid = {} }; }, .slice => { - if (ptr.child == u8) { - // Need to do this to keep the compiler happy - // If this was the case, simpleZigValueToJs would - // have handled it - unreachable; + if (js_value.isTypedArray()) { + switch (ptr.child) { + u8 => if (ptr.sentinel() == null) { + if (js_value.isUint8Array() or js_value.isUint8ClampedArray()) { + return .{ .ok = {} }; + } + }, + i8 => if (js_value.isInt8Array()) { + return .{ .ok = {} }; + }, + u16 => if (js_value.isUint16Array()) { + return .{ .ok = {} }; + }, + i16 => if (js_value.isInt16Array()) { + return .{ .ok = {} }; + }, + u32 => if (js_value.isUint32Array()) { + return .{ .ok = {} }; + }, + i32 => if (js_value.isInt32Array()) { + return .{ .ok = {} }; + }, + u64 => if (js_value.isBigUint64Array()) { + return .{ .ok = {} }; + }, + i64 => if (js_value.isBigInt64Array()) { + return .{ .ok = {} }; + }, + else => {}, + } + return .{ .invalid = {} }; } - var js_arr = v8.Array.init(isolate, @intCast(value.len)); - var js_obj = js_arr.castTo(v8.Object); - for (value, 0..) |v, i| { - const js_val = try _zigValueToJs(templates, isolate, v8_context, v); - if (js_obj.setValueAtIndex(v8_context, @intCast(i), js_val) == false) { - return error.FailedToCreateArray; + if (ptr.child == u8) { + if (js_value.isString()) { + return .{ .ok = {} }; } + // anything can be coerced into a string + return .{ .coerce = {} }; } - return js_obj.toValue(); - }, - else => {}, - }, - .@"struct" => |s| { - const type_name = @typeName(T); - if (@hasField(types.Lookup, type_name)) { - const template = templates[@field(types.LOOKUP, type_name)]; - const js_obj = try Context.mapZigInstanceToJs(v8_context, template, value); - return js_obj.toValue(); - } - - if (T == js.Function) { - // we're returning a callback - return value.func.toValue(); - } - - if (T == js.Object) { - // we're returning a v8.Object - return value.js_obj.toValue(); - } - if (T == js.Value) { - return value.value; - } - - if (T == js.Promise) { - // we're returning a v8.Promise - return value.promise.toObject().toValue(); - } + if (!js_value.isArray()) { + return .{ .invalid = {} }; + } - if (T == js.Exception) { - return isolate.throwException(value.inner); - } + // This can get tricky. + const js_arr = js_value.castTo(v8.Array); - if (s.is_tuple) { - // return the tuple struct as an array - var js_arr = v8.Array.init(isolate, @intCast(s.fields.len)); - var js_obj = js_arr.castTo(v8.Object); - inline for (s.fields, 0..) |f, i| { - const js_val = try _zigValueToJs(templates, isolate, v8_context, @field(value, f.name)); - if (js_obj.setValueAtIndex(v8_context, @intCast(i), js_val) == false) { - return error.FailedToCreateArray; - } + if (js_arr.length() == 0) { + // not so tricky in this case. + return .{ .value = &.{} }; } - return js_obj.toValue(); - } - // return the struct as a JS object - const js_obj = v8.Object.init(isolate); - inline for (s.fields) |f| { - const js_val = try _zigValueToJs(templates, isolate, v8_context, @field(value, f.name)); - const key = v8.String.initUtf8(isolate, f.name); - if (!js_obj.setValue(v8_context, key, js_val)) { - return error.CreateObjectFailure; + // We settle for just probing the first value. Ok, actually + // not tricky in this case either. + const v8_context = self.v8_context; + const js_obj = js_arr.castTo(v8.Object); + switch (try self.probeJsValueToZig(named_function, ptr.child, try js_obj.getAtIndex(v8_context, 0))) { + .value, .ok => return .{ .ok = {} }, + .compatible => return .{ .compatible = {} }, + .coerce => return .{ .coerce = {} }, + .invalid => return .{ .invalid = {} }, } - } - return js_obj.toValue(); + }, + else => {}, }, - .@"union" => |un| { - if (T == std.json.Value) { - return zigJsonToJs(isolate, v8_context, value); - } - if (un.tag_type) |UnionTagType| { - inline for (un.fields) |field| { - if (value == @field(UnionTagType, field.name)) { - return _zigValueToJs(templates, isolate, v8_context, @field(value, field.name)); + .array => |arr| { + // Retrieve fixed-size array as slice then probe + const slice_type = []arr.child; + switch (try self.probeJsValueToZig(named_function, slice_type, js_value)) { + .value => |slice_value| { + if (slice_value.len == arr.len) { + return .{ .value = @as(*T, @ptrCast(slice_value.ptr)).* }; } - } - unreachable; + return .{ .invalid = {} }; + }, + .ok => { + // Exact length match, we could allow smaller arrays as .compatible, but we would not be able to communicate how many were written + if (js_value.isArray()) { + const js_arr = js_value.castTo(v8.Array); + if (js_arr.length() == arr.len) { + return .{ .ok = {} }; + } + } else if (js_value.isString() and arr.child == u8) { + const str = try js_value.toString(self.v8_context); + if (str.lenUtf8(self.isolate) == arr.len) { + return .{ .ok = {} }; + } + } + return .{ .invalid = {} }; + }, + .compatible => return .{ .compatible = {} }, + .coerce => return .{ .coerce = {} }, + .invalid => return .{ .invalid = {} }, } - @compileError("Cannot use untagged union: " ++ @typeName(T)); }, - .optional => { - if (value) |v| { - return _zigValueToJs(templates, isolate, v8_context, v); - } - return v8.initNull(isolate).toValue(); + .@"struct" => { + // We don't want to duplicate the code for this, so we call + // the actual conversion function. + const value = (try self.jsValueToStruct(named_function, T, js_value)) orelse { + return .{ .invalid = {} }; + }; + return .{ .value = value }; }, - .error_union => return _zigValueToJs(templates, isolate, v8_context, try value), else => {}, } - @compileError("A function returns an unsupported type: " ++ @typeName(T)); + return .{ .invalid = {} }; } fn jsIntToZig(comptime T: type, js_value: v8.Value, v8_context: v8.Context) !T { @@ -1621,28 +1726,6 @@ fn jsUnsignedIntToZig(comptime T: type, max: comptime_int, maybe: u32) !T { return error.InvalidArgument; } -pub fn stackForLogs(arena: Allocator, isolate: v8.Isolate) !?[]const u8 { - std.debug.assert(@import("builtin").mode == .Debug); - - const separator = log.separator(); - var buf: std.ArrayListUnmanaged(u8) = .empty; - var writer = buf.writer(arena); - - const stack_trace = v8.StackTrace.getCurrentStackTrace(isolate, 30); - const frame_count = stack_trace.getFrameCount(); - - for (0..frame_count) |i| { - const frame = stack_trace.getFrame(isolate, @intCast(i)); - if (frame.getScriptName()) |name| { - const script = try js.stringToZig(arena, name, isolate); - try writer.print("{s}{s}:{d}", .{ separator, script, frame.getLineNumber() }); - } else { - try writer.print("{s}:{d}", .{ separator, frame.getLineNumber() }); - } - } - return buf.items; -} - fn compileScript(isolate: v8.Isolate, ctx: v8.Context, src: []const u8, name: ?[]const u8) !v8.Script { // compile const script_name = v8.String.initUtf8(isolate, name orelse "anonymous"); @@ -1729,3 +1812,32 @@ fn zigJsonToJs(isolate: v8.Isolate, v8_context: v8.Context, value: std.json.Valu }, } } + +// == Misc == +// An interface for types that want to have their jsDeinit function to be +// called when the call context ends +const DestructorCallback = struct { + ptr: *anyopaque, + destructorFn: *const fn (ptr: *anyopaque) void, + + fn init(ptr: anytype) DestructorCallback { + const T = @TypeOf(ptr); + const ptr_info = @typeInfo(T); + + const gen = struct { + pub fn destructor(pointer: *anyopaque) void { + const self: T = @ptrCast(@alignCast(pointer)); + return ptr_info.pointer.child.destructor(self); + } + }; + + return .{ + .ptr = ptr, + .destructorFn = gen.destructor, + }; + } + + pub fn destructor(self: DestructorCallback) void { + self.destructorFn(self.ptr); + } +}; diff --git a/src/browser/js/Env.zig b/src/browser/js/Env.zig index 9f268e7dc..0aee15c27 100644 --- a/src/browser/js/Env.zig +++ b/src/browser/js/Env.zig @@ -74,15 +74,7 @@ pub fn init(allocator: Allocator, platform: *const Platform, _: Opts) !*Env { 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 context: *Context = @ptrFromInt(v8_context.getEmbedderData(1).castTo(v8.BigInt).getUint64()); - context.initializeImportMeta(v8.Module{ .handle = c_module.? }, v8.Object{ .handle = c_meta.? }) catch |err| { - log.err(.js, "import meta", .{ .err = err }); - }; - } - }.callback); + isolate.setHostInitializeImportMetaObjectCallback(Context.metaObjectCallback); var temp_scope: v8.HandleScope = undefined; v8.HandleScope.init(&temp_scope, isolate); @@ -226,11 +218,10 @@ pub fn dumpMemoryStats(self: *Env) void { 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: *Context = @ptrFromInt(v8_context.getEmbedderData(1).castTo(v8.BigInt).getUint64()); + const context = Context.fromIsolate(isolate); const value = - if (msg.getValue()) |v8_value| js.valueToString(context.call_arena, v8_value, isolate, v8_context) catch |err| @errorName(err) else "no value"; + if (msg.getValue()) |v8_value| context.valueToString(v8_value, .{}) catch |err| @errorName(err) else "no value"; log.debug(.js, "unhandled rejection", .{ .value = value }); } diff --git a/src/browser/js/ExecutionWorld.zig b/src/browser/js/ExecutionWorld.zig index bd76e4145..fb20c928e 100644 --- a/src/browser/js/ExecutionWorld.zig +++ b/src/browser/js/ExecutionWorld.zig @@ -58,12 +58,12 @@ pub fn deinit(self: *ExecutionWorld) void { // when the handle_scope is freed. // We also maintain our own "context_arena" which allows us to have // all page related memory easily managed. -pub fn createContext(self: *ExecutionWorld, global: anytype, page: *Page, script_manager: ?*ScriptManager, enter: bool, global_callback: ?js.GlobalMissingCallback) !*Context { +pub fn createContext(self: *ExecutionWorld, page: *Page, enter: bool, global_callback: ?js.GlobalMissingCallback) !*Context { std.debug.assert(self.context == null); const env = self.env; const isolate = env.isolate; - const Global = @TypeOf(global.*); + const Global = @TypeOf(page.window); const templates = &self.env.templates; var v8_context: v8.Context = blk: { @@ -84,12 +84,9 @@ pub fn createContext(self: *ExecutionWorld, global: anytype, page: *Page, script .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 context = Context.fromIsolate(info.getIsolate()); - const context: *Context = @ptrFromInt(v8_context.getEmbedderData(1).castTo(v8.BigInt).getUint64()); - - const property = js.valueToString(context.call_arena, .{ .handle = c_name.? }, _isolate, v8_context) catch "???"; + const property = context.valueToString(.{ .handle = c_name.? }, .{}) catch "???"; if (context.global_callback.?.missing(property, context)) { return v8.Intercepted.Yes; } @@ -180,7 +177,7 @@ pub fn createContext(self: *ExecutionWorld, global: anytype, page: *Page, script .templates = &env.templates, .meta_lookup = &env.meta_lookup, .handle_scope = handle_scope, - .script_manager = script_manager, + .script_manager = &page.script_manager, .call_arena = self.call_arena.allocator(), .context_arena = self.context_arena.allocator(), .global_callback = global_callback, @@ -188,9 +185,8 @@ pub fn createContext(self: *ExecutionWorld, global: anytype, page: *Page, script var context = &self.context.?; { - // Given a context, we can get our executor. - // (we store a pointer to our executor in the context's - // embeddeder data) + // Store a pointer to our context inside the v8 context so that, given + // a v8 context, we can get our context out const data = isolate.initBigIntU64(@intCast(@intFromPtr(context))); v8_context.setEmbedderData(1, data); } @@ -240,7 +236,7 @@ pub fn createContext(self: *ExecutionWorld, global: anytype, page: *Page, script } } - _ = try context._mapZigInstanceToJs(v8_context.getGlobal(), global); + try context.setupGlobal(); return context; } diff --git a/src/browser/js/Function.zig b/src/browser/js/Function.zig index f4e2b15fb..bcb35a80c 100644 --- a/src/browser/js/Function.zig +++ b/src/browser/js/Function.zig @@ -22,7 +22,7 @@ pub const Result = struct { pub fn getName(self: *const Function, allocator: Allocator) ![]const u8 { const name = self.func.castToFunction().getName(); - return js.valueToString(allocator, name, self.context.isolate, self.context.v8_context); + return self.context.valueToString(name, .{ .allocator = allocator }); } pub fn setName(self: *const Function, name: []const u8) void { diff --git a/src/browser/js/Inspector.zig b/src/browser/js/Inspector.zig index 5c2cb7b05..264e1f0f8 100644 --- a/src/browser/js/Inspector.zig +++ b/src/browser/js/Inspector.zig @@ -80,7 +80,7 @@ pub fn contextCreated( // we'll create it and track it for cleanup when the context ends. pub fn getRemoteObject( self: *const Inspector, - context: *const Context, + context: *Context, group: []const u8, value: anytype, ) !RemoteObject { diff --git a/src/browser/js/Object.zig b/src/browser/js/Object.zig index 4af47c0a0..24b2c0d42 100644 --- a/src/browser/js/Object.zig +++ b/src/browser/js/Object.zig @@ -52,24 +52,22 @@ pub fn isTruthy(self: Object) bool { } pub fn toString(self: Object) ![]const u8 { - const context = self.context; const js_value = self.js_obj.toValue(); - return js.valueToString(context.call_arena, js_value, context.isolate, context.v8_context); + return self.context.valueToString(js_value, .{}); } pub fn toDetailString(self: Object) ![]const u8 { - const context = self.context; const js_value = self.js_obj.toValue(); - return js.valueToDetailString(context.call_arena, js_value, context.isolate, context.v8_context); + return self.context.valueToDetailString(js_value); } pub fn format(self: Object, comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void { return writer.writeAll(try self.toString()); } -pub fn toJson(self: Object, allocator: std.mem.Allocator) ![]u8 { +pub fn toJson(self: Object, allocator: Allocator) ![]u8 { const json_string = try v8.Json.stringify(self.context.v8_context, self.js_obj.toValue(), null); - const str = try js.stringToZig(allocator, json_string, self.context.isolate); + const str = try self.context.jsStringToZig(json_string, .{ .allocator = allocator }); return str; } @@ -137,11 +135,6 @@ pub fn nameIterator(self: Object) js.ValueIterator { }; } -pub fn constructorName(self: Object, allocator: Allocator) ![]const u8 { - const str = try self.js_obj.getConstructorName(); - return js.StringToZig(allocator, str, self.context.isolate); -} - pub fn toZig(self: Object, comptime Struct: type, comptime name: []const u8, comptime T: type) !T { const named_function = comptime Caller.NamedFunction.init(Struct, name); return self.context.jsValueToZig(named_function, T, self.js_obj.toValue()); diff --git a/src/browser/js/This.zig b/src/browser/js/This.zig index 30233aea2..c04134c7d 100644 --- a/src/browser/js/This.zig +++ b/src/browser/js/This.zig @@ -23,7 +23,3 @@ pub fn setIndex(self: This, index: u32, value: anytype, opts: js.Object.SetOpts) pub fn set(self: This, key: []const u8, value: anytype, opts: js.Object.SetOpts) !void { return self.obj.set(key, value, opts); } - -pub fn constructorName(self: This, allocator: Allocator) ![]const u8 { - return try self.obj.constructorName(allocator); -} diff --git a/src/browser/js/TryCatch.zig b/src/browser/js/TryCatch.zig index a40e29a4a..141a57795 100644 --- a/src/browser/js/TryCatch.zig +++ b/src/browser/js/TryCatch.zig @@ -21,15 +21,14 @@ pub fn hasCaught(self: TryCatch) bool { // the caller needs to deinit the string returned pub fn exception(self: TryCatch, allocator: Allocator) !?[]const u8 { const msg = self.inner.getException() orelse return null; - const context = self.context; - return try js.valueToString(allocator, msg, context.isolate, context.v8_context); + return try self.context.valueToString(msg, .{ .allocator = allocator }); } // the caller needs to deinit the string returned pub fn stack(self: TryCatch, allocator: Allocator) !?[]const u8 { const context = self.context; const s = self.inner.getStackTrace(context.v8_context) orelse return null; - return try js.valueToString(allocator, s, context.isolate, context.v8_context); + return try context.valueToString(s, .{ .allocator = allocator }); } // the caller needs to deinit the string returned @@ -37,7 +36,7 @@ pub fn sourceLine(self: TryCatch, allocator: Allocator) !?[]const u8 { const context = self.context; const msg = self.inner.getMessage() orelse return null; const sl = msg.getSourceLine(context.v8_context) orelse return null; - return try js.stringToZig(allocator, sl, context.isolate); + return try context.jsStringToZig(sl, .{ .allocator = allocator }); } pub fn sourceLineNumber(self: TryCatch) ?u32 { diff --git a/src/browser/js/js.zig b/src/browser/js/js.zig index 12dcc1493..31cf54425 100644 --- a/src/browser/js/js.zig +++ b/src/browser/js/js.zig @@ -17,7 +17,6 @@ // along with this program. If not, see . const std = @import("std"); -const builtin = @import("builtin"); pub const v8 = @import("v8"); const types = @import("types.zig"); @@ -64,9 +63,7 @@ pub const PromiseResolver = struct { resolver: v8.PromiseResolver, pub fn promise(self: PromiseResolver) Promise { - return .{ - .promise = self.resolver.getPromise(), - }; + return self.resolver.getPromise(); } pub fn resolve(self: PromiseResolver, value: anytype) !void { @@ -101,9 +98,7 @@ pub const PersistentPromiseResolver = struct { } pub fn promise(self: PersistentPromiseResolver) Promise { - return .{ - .promise = self.resolver.castToPromiseResolver().getPromise(), - }; + return self.resolver.castToPromiseResolver().getPromise(); } pub fn resolve(self: PersistentPromiseResolver, value: anytype) !void { @@ -129,9 +124,7 @@ pub const PersistentPromiseResolver = struct { } }; -pub const Promise = struct { - promise: v8.Promise, -}; +pub const Promise = v8.Promise; // When doing jsValueToZig, string ([]const u8) are managed by the // call_arena. That means that if the API wants to persist the string @@ -148,8 +141,7 @@ pub const Exception = struct { // the caller needs to deinit the string returned pub fn exception(self: Exception, allocator: Allocator) ![]const u8 { - const context = self.context; - return try valueToString(allocator, self.inner, context.isolate, context.v8_context); + return self.context.valueToString(self.inner, .{ .allocator = allocator }); } }; @@ -159,8 +151,7 @@ pub const Value = struct { // the caller needs to deinit the string returned pub fn toString(self: Value, allocator: Allocator) ![]const u8 { - const context = self.context; - return valueToString(allocator, self.value, context.isolate, context.v8_context); + return self.context.valueToString(self.value, .{ .allocator = allocator }); } pub fn fromJson(ctx: *Context, json: []const u8) !Value { @@ -476,93 +467,6 @@ pub const TaggedAnyOpaque = struct { subtype: ?types.Sub, }; -pub fn valueToDetailString(arena: Allocator, value: v8.Value, isolate: v8.Isolate, v8_context: v8.Context) ![]u8 { - var str: ?v8.String = null; - if (value.isObject() and !value.isFunction()) blk: { - str = v8.Json.stringify(v8_context, value, null) catch break :blk; - - if (str.?.lenUtf8(isolate) == 2) { - // {} isn't useful, null this so that we can get the toDetailString - // (which might also be useless, but maybe not) - str = null; - } - } - - if (str == null) { - str = try value.toDetailString(v8_context); - } - - const s = try stringToZig(arena, str.?, isolate); - if (comptime builtin.mode == .Debug) { - if (std.mem.eql(u8, s, "[object Object]")) { - if (debugValueToString(arena, value.castTo(v8.Object), isolate, v8_context)) |ds| { - return ds; - } else |err| { - log.err(.js, "debug serialize value", .{ .err = err }); - } - } - } - return s; -} - -pub fn valueToString(allocator: Allocator, value: v8.Value, isolate: v8.Isolate, v8_context: v8.Context) ![]u8 { - if (value.isSymbol()) { - // symbol's can't be converted to a string - return allocator.dupe(u8, "$Symbol"); - } - const str = try value.toString(v8_context); - return stringToZig(allocator, str, isolate); -} - -pub fn valueToStringZ(allocator: Allocator, value: v8.Value, isolate: v8.Isolate, v8_context: v8.Context) ![:0]u8 { - const str = try value.toString(v8_context); - const len = str.lenUtf8(isolate); - const buf = try allocator.allocSentinel(u8, len, 0); - const n = str.writeUtf8(isolate, buf); - std.debug.assert(n == len); - return buf; -} - -pub fn stringToZig(allocator: Allocator, str: v8.String, isolate: v8.Isolate) ![]u8 { - const len = str.lenUtf8(isolate); - const buf = try allocator.alloc(u8, len); - const n = str.writeUtf8(isolate, buf); - std.debug.assert(n == len); - return buf; -} - -fn debugValueToString(arena: Allocator, js_obj: v8.Object, isolate: v8.Isolate, v8_context: v8.Context) ![]u8 { - if (comptime builtin.mode != .Debug) { - @compileError("debugValue can only be called in debug mode"); - } - - var arr: std.ArrayListUnmanaged(u8) = .empty; - var writer = arr.writer(arena); - - const names_arr = js_obj.getOwnPropertyNames(v8_context); - const names_obj = names_arr.castTo(v8.Object); - const len = names_arr.length(); - - try writer.writeAll("(JSON.stringify failed, dumping top-level fields)\n"); - for (0..len) |i| { - const field_name = try names_obj.getAtIndex(v8_context, @intCast(i)); - const field_value = try js_obj.getValue(v8_context, field_name); - const name = try valueToString(arena, field_name, isolate, v8_context); - const value = try valueToString(arena, field_value, isolate, v8_context); - try writer.writeAll(name); - try writer.writeAll(": "); - if (std.mem.indexOfAny(u8, value, &std.ascii.whitespace) == null) { - try writer.writeAll(value); - } else { - try writer.writeByte('"'); - try writer.writeAll(value); - try writer.writeByte('"'); - } - try writer.writeByte(' '); - } - return arr.items; -} - // These are here, and not in Inspector.zig, because Inspector.zig isn't always // included (e.g. in the wpt build). diff --git a/src/browser/page.zig b/src/browser/page.zig index 9c5871d08..f0576d4b5 100644 --- a/src/browser/page.zig +++ b/src/browser/page.zig @@ -74,7 +74,7 @@ pub const Page = struct { // Our JavaScript context for this specific page. This is what we use to // execute any JavaScript - main_context: *js.Context, + js: *js.Context, // indicates intention to navigate to another page on the next loop execution. delayed_navigation: bool = false, @@ -140,11 +140,11 @@ pub const Page = struct { .scheduler = Scheduler.init(arena), .keydown_event_node = .{ .func = keydownCallback }, .window_clicked_event_node = .{ .func = windowClicked }, - .main_context = undefined, + .js = undefined, }; - self.main_context = try session.executor.createContext(&self.window, self, &self.script_manager, true, js.GlobalMissingCallback.init(&self.polyfill_loader)); - try polyfill.preload(self.arena, self.main_context); + self.js = try session.executor.createContext(self, true, js.GlobalMissingCallback.init(&self.polyfill_loader)); + try polyfill.preload(self.arena, self.js); try self.scheduler.add(self, runMicrotasks, 5, .{ .name = "page.microtasks" }); // message loop must run only non-test env @@ -277,7 +277,7 @@ pub const Page = struct { var ms_remaining = wait_ms; var try_catch: js.TryCatch = undefined; - try_catch.init(self.main_context); + try_catch.init(self.js); defer try_catch.deinit(); var scheduler = &self.scheduler; @@ -1116,7 +1116,7 @@ pub const Page = struct { pub fn stackTrace(self: *Page) !?[]const u8 { if (comptime builtin.mode == .Debug) { - return self.main_context.stackTrace(); + return self.js.stackTrace(); } return null; } diff --git a/src/browser/streams/ReadableStream.zig b/src/browser/streams/ReadableStream.zig index 9b071b337..077f93204 100644 --- a/src/browser/streams/ReadableStream.zig +++ b/src/browser/streams/ReadableStream.zig @@ -105,8 +105,8 @@ const QueueingStrategy = struct { pub fn constructor(underlying: ?UnderlyingSource, _strategy: ?QueueingStrategy, page: *Page) !*ReadableStream { const strategy: QueueingStrategy = _strategy orelse .{}; - const cancel_resolver = try page.main_context.createPersistentPromiseResolver(.self); - const closed_resolver = try page.main_context.createPersistentPromiseResolver(.self); + const cancel_resolver = try page.js.createPromiseResolver(.self); + const closed_resolver = try page.js.createPromiseResolver(.self); const stream = try page.arena.create(ReadableStream); stream.* = ReadableStream{ diff --git a/src/browser/streams/ReadableStreamDefaultReader.zig b/src/browser/streams/ReadableStreamDefaultReader.zig index 25c86f728..e08dc9082 100644 --- a/src/browser/streams/ReadableStreamDefaultReader.zig +++ b/src/browser/streams/ReadableStreamDefaultReader.zig @@ -47,43 +47,26 @@ pub fn _read(self: *const ReadableStreamDefaultReader, page: *Page) !js.Promise .readable => { if (stream.queue.items.len > 0) { const data = self.stream.queue.orderedRemove(0); - const resolver = page.main_context.createPromiseResolver(); - - try resolver.resolve(ReadableStreamReadResult.init(data, false)); + const promise = page.js.resolvePromise(ReadableStreamReadResult.init(data, false)); try self.stream.pullIf(); - return resolver.promise(); - } else { - if (self.stream.reader_resolver) |rr| { - return rr.promise(); - } else { - const persistent_resolver = try page.main_context.createPersistentPromiseResolver(.page); - self.stream.reader_resolver = persistent_resolver; - return persistent_resolver.promise(); - } + return promise; + } + if (self.stream.reader_resolver) |rr| { + return rr.promise(); } + const persistent_resolver = try page.js.createPromiseResolver(.page); + self.stream.reader_resolver = persistent_resolver; + return persistent_resolver.promise(); }, .closed => |_| { - const resolver = page.main_context.createPromiseResolver(); - if (stream.queue.items.len > 0) { const data = self.stream.queue.orderedRemove(0); - try resolver.resolve(ReadableStreamReadResult.init(data, false)); - } else { - try resolver.resolve(ReadableStreamReadResult{ .done = true }); + return page.js.resolvePromise(ReadableStreamReadResult.init(data, false)); } - - return resolver.promise(); - }, - .cancelled => |_| { - const resolver = page.main_context.createPromiseResolver(); - try resolver.resolve(ReadableStreamReadResult{ .value = .empty, .done = true }); - return resolver.promise(); - }, - .errored => |err| { - const resolver = page.main_context.createPromiseResolver(); - try resolver.reject(err); - return resolver.promise(); + return page.js.resolvePromise(ReadableStreamReadResult{ .done = true }); }, + .cancelled => |_| return page.js.resolvePromise(ReadableStreamReadResult{ .value = .empty, .done = true }), + .errored => |err| return page.js.rejectPromise(err), } } diff --git a/src/cdp/cdp.zig b/src/cdp/cdp.zig index a27262f1e..0169ee55f 100644 --- a/src/cdp/cdp.zig +++ b/src/cdp/cdp.zig @@ -690,9 +690,7 @@ const IsolatedWorld = struct { return; } _ = try self.executor.createContext( - &page.window, page, - null, false, js.GlobalMissingCallback.init(&self.polyfill_loader), ); diff --git a/src/cdp/domains/dom.zig b/src/cdp/domains/dom.zig index 972f0c895..0f0ff8f8b 100644 --- a/src/cdp/domains/dom.zig +++ b/src/cdp/domains/dom.zig @@ -274,7 +274,7 @@ fn resolveNode(cmd: anytype) !void { const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded; const page = bc.session.currentPage() orelse return error.PageNotLoaded; - var js_context = page.main_context; + var js_context = page.js; if (params.executionContextId) |context_id| { if (js_context.v8_context.debugContextId() != context_id) { for (bc.isolated_worlds.items) |*isolated_world| { diff --git a/src/cdp/domains/page.zig b/src/cdp/domains/page.zig index 3872ce3cc..1f6b720aa 100644 --- a/src/cdp/domains/page.zig +++ b/src/cdp/domains/page.zig @@ -251,7 +251,7 @@ pub fn pageNavigate(arena: Allocator, bc: anytype, event: *const Notification.Pa const page = bc.session.currentPage() orelse return error.PageNotLoaded; const aux_data = try std.fmt.allocPrint(arena, "{{\"isDefault\":true,\"type\":\"default\",\"frameId\":\"{s}\"}}", .{target_id}); bc.inspector.contextCreated( - page.main_context, + page.js, "", try page.origin(arena), aux_data, diff --git a/src/cdp/domains/target.zig b/src/cdp/domains/target.zig index b3b839ad6..26f4cfbe3 100644 --- a/src/cdp/domains/target.zig +++ b/src/cdp/domains/target.zig @@ -147,7 +147,7 @@ fn createTarget(cmd: anytype) !void { { const aux_data = try std.fmt.allocPrint(cmd.arena, "{{\"isDefault\":true,\"type\":\"default\",\"frameId\":\"{s}\"}}", .{target_id}); bc.inspector.contextCreated( - page.main_context, + page.js, "", try page.origin(cmd.arena), aux_data, diff --git a/src/main_wpt.zig b/src/main_wpt.zig index 10f2e7e68..ddda29c5e 100644 --- a/src/main_wpt.zig +++ b/src/main_wpt.zig @@ -123,7 +123,7 @@ fn run( _ = page.wait(2000); - const js_context = page.main_context; + const js_context = page.js; var try_catch: js.TryCatch = undefined; try_catch.init(js_context); defer try_catch.deinit(); diff --git a/src/testing.zig b/src/testing.zig index 7439343a7..93c1abad9 100644 --- a/src/testing.zig +++ b/src/testing.zig @@ -395,7 +395,7 @@ pub fn htmlRunner(file: []const u8) !void { page.arena = @import("root").tracking_allocator; - const js_context = page.main_context; + const js_context = page.js; var try_catch: js.TryCatch = undefined; try_catch.init(js_context); defer try_catch.deinit(); @@ -409,7 +409,7 @@ pub fn htmlRunner(file: []const u8) !void { page.session.browser.runMessageLoop(); const needs_second_wait = try js_context.exec("testing._onPageWait.length > 0", "check_onPageWait"); - if (needs_second_wait.value.toBool(page.main_context.isolate)) { + if (needs_second_wait.value.toBool(page.js.isolate)) { // sets the isSecondWait flag in testing. _ = js_context.exec("testing._isSecondWait = true", "set_second_wait_flag") catch {}; _ = page.wait(2000);