diff --git a/src/browser/dom/html_collection.zig b/src/browser/dom/html_collection.zig index 3a7aa8fa..812b6931 100644 --- a/src/browser/dom/html_collection.zig +++ b/src/browser/dom/html_collection.zig @@ -24,7 +24,7 @@ const utils = @import("utils.z"); const Element = @import("element.zig").Element; const Union = @import("element.zig").Union; -const JsObject = @import("../env.zig").JsObject; +const JsThis = @import("../env.zig").JsThis; const Walker = @import("walker.zig").Walker; const WalkerDepthFirst = @import("walker.zig").WalkerDepthFirst; @@ -443,15 +443,15 @@ pub const HTMLCollection = struct { return null; } - pub fn postAttach(self: *HTMLCollection, js_obj: JsObject) !void { + pub fn postAttach(self: *HTMLCollection, js_this: JsThis) !void { const len = try self.get_length(); for (0..len) |i| { const node = try self.item(@intCast(i)) orelse unreachable; const e = @as(*parser.Element, @ptrCast(node)); - try js_obj.setIndex(@intCast(i), e); + try js_this.setIndex(@intCast(i), e); if (try item_name(e)) |name| { - try js_obj.set(name, e); + try js_this.set(name, e); } } } diff --git a/src/browser/dom/mutation_observer.zig b/src/browser/dom/mutation_observer.zig index 0324399e..d9b9bc09 100644 --- a/src/browser/dom/mutation_observer.zig +++ b/src/browser/dom/mutation_observer.zig @@ -22,7 +22,7 @@ const parser = @import("../netsurf.zig"); const SessionState = @import("../env.zig").SessionState; const Env = @import("../env.zig").Env; -const JsObject = @import("../env.zig").JsObject; +const JsThis = @import("../env.zig").JsThis; const NodeList = @import("nodelist.zig").NodeList; pub const Interfaces = .{ @@ -184,9 +184,9 @@ pub const MutationRecords = struct { return null; }; } - pub fn postAttach(self: *const MutationRecords, js_obj: JsObject) !void { + pub fn postAttach(self: *const MutationRecords, js_this: JsThis) !void { if (self.first) |mr| { - try js_obj.set("0", mr); + try js_this.set("0", mr); } } }; diff --git a/src/browser/dom/nodelist.zig b/src/browser/dom/nodelist.zig index 2d27f4d0..37ef3b31 100644 --- a/src/browser/dom/nodelist.zig +++ b/src/browser/dom/nodelist.zig @@ -20,7 +20,7 @@ const std = @import("std"); const parser = @import("../netsurf.zig"); -const JsObject = @import("../env.zig").JsObject; +const JsThis = @import("../env.zig").JsThis; const Callback = @import("../env.zig").Callback; const SessionState = @import("../env.zig").SessionState; @@ -177,11 +177,11 @@ pub const NodeList = struct { } // TODO entries() https://developer.mozilla.org/en-US/docs/Web/API/NodeList/entries - pub fn postAttach(self: *NodeList, js_obj: JsObject) !void { + pub fn postAttach(self: *NodeList, js_this: JsThis) !void { const len = self.get_length(); for (0..len) |i| { const node = try self._item(@intCast(i)) orelse unreachable; - try js_obj.setIndex(i, node); + try js_this.setIndex(i, node); } } }; diff --git a/src/browser/dom/token_list.zig b/src/browser/dom/token_list.zig index 361ef047..538c4156 100644 --- a/src/browser/dom/token_list.zig +++ b/src/browser/dom/token_list.zig @@ -22,6 +22,7 @@ const parser = @import("../netsurf.zig"); const iterator = @import("../iterator/iterator.zig"); const Callback = @import("../env.zig").Callback; +const JsObject = @import("../env.zig").JsObject; const SessionState = @import("../env.zig").SessionState; const DOMException = @import("exceptions.zig").DOMException; @@ -138,11 +139,11 @@ pub const DOMTokenList = struct { } // TODO handle thisArg - pub fn _forEach(self: *parser.TokenList, cbk: Callback) !void { + pub fn _forEach(self: *parser.TokenList, cbk: Callback, this_arg: JsObject) !void { var entries = _entries(self); while (try entries._next()) |entry| { var result: Callback.Result = undefined; - cbk.tryCall(.{ entry.@"1", entry.@"0", self }, &result) catch { + cbk.tryCallWithThis(this_arg, .{ entry.@"1", entry.@"0", self }, &result) catch { log.err("callback error: {s}", .{result.exception}); log.debug("stack:\n{s}", .{result.stack orelse "???"}); }; diff --git a/src/browser/env.zig b/src/browser/env.zig index 0fd4370d..67e7207e 100644 --- a/src/browser/env.zig +++ b/src/browser/env.zig @@ -21,6 +21,7 @@ const Interfaces = generate.Tuple(.{ @import("xmlserializer/xmlserializer.zig").Interfaces, }); +pub const JsThis = Env.JsThis; pub const JsObject = Env.JsObject; pub const Callback = Env.Callback; pub const Env = js.Env(*SessionState, Interfaces{}); diff --git a/src/runtime/js.zig b/src/runtime/js.zig index 327cd579..0387eace 100644 --- a/src/runtime/js.zig +++ b/src/runtime/js.zig @@ -902,6 +902,27 @@ pub fn Env(comptime S: type, comptime types: anytype) type { _ = self.scope_arena.reset(.{ .retain_with_limit = 1024 * 64 }); } + // Given an anytype, turns it into a v8.Object. The anytype could be: + // 1 - A V8.object already + // 2 - Our this 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 Executor, value: anytype) !v8.Object { + if (@TypeOf(value) == v8.Object) { + return value; + } + + if (@TypeOf(value) == JsObject) { + return value.js_obj; + } + + const persistent_object = self.scope.?.identity_map.get(@intFromPtr(value)) orelse { + return error.InvalidThisForCallback; + }; + + return persistent_object.castToObject(); + } + // Wrap a v8.Value, largely so that we can provide a convenient // toString function fn createValue(self: *const Executor, value: v8.Value) Value { @@ -1003,7 +1024,7 @@ pub fn Env(comptime S: type, comptime types: anytype) type { gop.value_ptr.* = js_persistent; if (@hasDecl(ptr.child, "postAttach")) { - const obj_wrap = JsObject{ .js_obj = js_obj, .executor = self }; + const obj_wrap = JsThis{ .obj = .{ .js_obj = js_obj, .executor = self } }; switch (@typeInfo(@TypeOf(ptr.child.postAttach)).@"fn".params.len) { 2 => try value.postAttach(obj_wrap), 3 => try value.postAttach(self.state, obj_wrap), @@ -1094,7 +1115,7 @@ pub fn Env(comptime S: type, comptime types: anytype) type { pub const Callback = struct { id: usize, executor: *Executor, - this: ?v8.Object = null, + _this: ?v8.Object = null, func: PersistentFunction, // We use this when mapping a JS value to a Zig object. We can't @@ -1110,22 +1131,23 @@ pub fn Env(comptime S: type, comptime types: anytype) type { }; pub fn setThis(self: *Callback, value: anytype) !void { - const persistent_object = self.executor.scope.?.identity_map.get(@intFromPtr(value)) orelse { - return error.InvalidThisForCallback; - }; - self.this = persistent_object.castToObject(); + self._this = try self.executor.valueToExistingObject(value); } pub fn call(self: *const Callback, args: anytype) !void { - return self.callWithThis(self.this orelse self.executor.context.getGlobal(), args); + return self.callWithThis(self.getThis(), args); } pub fn tryCall(self: *const Callback, args: anytype, result: *Result) !void { + return self.tryCallWithThis(self.getThis(), args, result); + } + + pub fn tryCallWithThis(self: *const Callback, this: anytype, args: anytype, result: *Result) !void { var try_catch: TryCatch = undefined; try_catch.init(self.executor); defer try_catch.deinit(); - self.call(args) catch |err| { + self.callWithThis(this, args) catch |err| { if (try_catch.hasCaught()) { const allocator = self.executor.scope.?.call_arena; result.stack = try_catch.stack(allocator) catch null; @@ -1138,9 +1160,11 @@ pub fn Env(comptime S: type, comptime types: anytype) type { }; } - fn callWithThis(self: *const @This(), js_this: v8.Object, args: anytype) !void { + pub fn callWithThis(self: *const Callback, this: anytype, args: anytype) !void { const executor = self.executor; + const js_this = try executor.valueToExistingObject(this); + const aargs = if (comptime @typeInfo(@TypeOf(args)) == .null) struct {}{} else args; const fields = @typeInfo(@TypeOf(aargs)).@"struct".fields; var js_args: [fields.len]v8.Value = undefined; @@ -1154,8 +1178,12 @@ pub fn Env(comptime S: type, comptime types: anytype) type { } } + fn getThis(self: *const Callback) v8.Object { + return self._this orelse self.executor.context.getGlobal(); + } + // debug/helper to print the source of the JS callback - fn printFunc(self: *const @This()) !void { + fn printFunc(self: Callback) !void { const executor = self.executor; const value = self.func.castToFunction().toValue(); const src = try valueToString(executor.call_arena.allocator(), value, executor.isolate, executor.context); @@ -1198,6 +1226,28 @@ pub fn Env(comptime S: type, comptime types: anytype) type { } }; + // This only exists so that we know whether a function wants the opaque + // JS argument (JsObject), or if it wants the receiver as an opaque + // value. + // JsObject is normally used when a method wants an opaque JS object + // that it'll pass into a callback. + // JsThis is used when the function wants to do advanced manipulation + // of the v8.Object bound to the instance. For example, postAttach is an + // example of using JsThis. + pub const JsThis = struct { + obj: JsObject, + + const _JSTHIS_ID_KLUDGE = true; + + pub fn setIndex(self: JsThis, index: usize, value: anytype) !void { + return self.obj.setIndex(index, value); + } + + pub fn set(self: JsThis, key: []const u8, value: anytype) !void { + return self.obj.set(key, value); + } + }; + pub const TryCatch = struct { inner: v8.TryCatch, executor: *const Executor, @@ -1761,14 +1811,14 @@ fn Caller(comptime E: type) type { break :blk params[0 .. params.len - 1]; } - // If the last parameter is a JsObject, set it, and exclude it + // If the last parameter is a special JsThis, set it, and exclude it // from our params slice, because we don't want to bind it to // a JS argument - if (comptime isJsObject(params[params.len - 1].type.?)) { - @field(args, std.fmt.comptimePrint("{d}", .{params.len - 1 + offset})) = .{ - .handle = info.getThis(), + if (comptime isJsThis(params[params.len - 1].type.?)) { + @field(args, std.fmt.comptimePrint("{d}", .{params.len - 1 + offset})) = .{ .obj = .{ + .js_obj = info.getThis(), .executor = self.executor, - }; + } }; // AND the 2nd last parameter is state if (params.len > 1 and comptime isState(params[params.len - 2].type.?)) { @@ -1827,9 +1877,9 @@ fn Caller(comptime E: type) type { } if (comptime isState(param.type.?)) { - @compileError("State must be the last parameter (or 2nd last if there's a JsObject): " ++ named_function.full_name); - } else if (comptime isJsObject(param.type.?)) { - @compileError("JsObject must be the last parameter: " ++ named_function.full_name); + @compileError("State must be the last parameter (or 2nd last if there's a JsThis): " ++ named_function.full_name); + } else if (comptime isJsThis(param.type.?)) { + @compileError("JsThis must be the last parameter: " ++ named_function.full_name); } else if (i >= js_parameter_count) { if (@typeInfo(param.type.?) != .optional) { return error.InvalidArgument; @@ -1929,9 +1979,20 @@ fn Caller(comptime E: type) type { if (!js_value.isObject()) { return error.InvalidArgument; } + + const js_obj = js_value.castTo(v8.Object); + + if (comptime isJsObject(T)) { + // Caller wants an opaque JsObject. Probably a parameter + // that it needs to pass back into a callback + return E.JsObject{ + .js_obj = js_obj, + .executor = self.executor, + }; + } + const context = self.context; const isolate = self.isolate; - const js_obj = js_value.castTo(v8.Object); var value: T = undefined; inline for (s.fields) |field| { @@ -2017,6 +2078,10 @@ fn Caller(comptime E: type) type { fn isJsObject(comptime T: type) bool { return @typeInfo(T) == .@"struct" and @hasDecl(T, "_JSOBJECT_ID_KLUDGE"); } + + fn isJsThis(comptime T: type) bool { + return @typeInfo(T) == .@"struct" and @hasDecl(T, "_JSTHIS_ID_KLUDGE"); + } }; }