History: We started with 1 context and thus only had 1 identity map. Frames
were added, and we tried to stick with 1 identity map per context. That didn't
work - it breaks cross-frame scripting. We introduced "Origin" so that all
frames on the same origin share the same objects. That almost worked, by
the v8::Inspector isn't bound by a Context's SecurityToken. So we tried 1 global
identity map. But that doesn't work. CDP IsolateWorlds do, in fact, need some
isolation. They need new v8::Objects created in their context, even if the
object already exists in the main context.

In the end, you end up with something like this: A page (and all its frames)
needs 1 view of the data. And each IsolateWorld needs it own view. This commit
introduces a js.Identity which is referenced by the context. The Session has a
js.Identity (used by all pages), and each IsolateWorld has its own js.Identity.

As a bonus, the arena pool memory-leak detection has been moved out of the
session and into the ArenaPool. This means _all_ arena pool access is audited
(in debug mode). This seems superfluous, but it's actually necessary since
IsolateWorlds (which now own their own identity) can outlive the Page so there's
no clear place to "check" for leaks - except on ArenaPool deinit.
This commit is contained in:
Karl Seguin
2026-03-19 18:46:35 +08:00
parent 38e9f86088
commit f70865e174
14 changed files with 290 additions and 212 deletions

View File

@@ -202,21 +202,20 @@ pub fn compileAndRun(self: *const Local, src: []const u8, name: ?[]const u8) !js
// we can just grab it from the identity_map)
pub fn mapZigInstanceToJs(self: *const Local, js_obj_handle: ?*const v8.Object, value: anytype) !js.Object {
const ctx = self.ctx;
const session = ctx.session;
const page_arena = session.page_arena;
const context_arena = ctx.arena;
const T = @TypeOf(value);
switch (@typeInfo(T)) {
.@"struct" => {
// Struct, has to be placed on the heap
const heap = try page_arena.create(T);
const heap = try context_arena.create(T);
heap.* = value;
return self.mapZigInstanceToJs(js_obj_handle, heap);
},
.pointer => |ptr| {
const resolved = resolveValue(value);
const gop = try session.addIdentity(@intFromPtr(resolved.ptr));
const gop = try ctx.addIdentity(@intFromPtr(resolved.ptr));
if (gop.found_existing) {
// we've seen this instance before, return the same object
return (js.Object.Global{ .handle = gop.value_ptr.* }).local(self);
@@ -245,7 +244,7 @@ pub fn mapZigInstanceToJs(self: *const Local, js_obj_handle: ?*const v8.Object,
// The TAO contains the pointer to our Zig instance as
// well as any meta data we'll need to use it later.
// See the TaggedOpaque struct for more details.
const tao = try page_arena.create(TaggedOpaque);
const tao = try context_arena.create(TaggedOpaque);
tao.* = .{
.value = resolved.ptr,
.prototype_chain = resolved.prototype_chain.ptr,
@@ -277,10 +276,10 @@ pub fn mapZigInstanceToJs(self: *const Local, js_obj_handle: ?*const v8.Object,
// Instead, we check if the base has finalizer. The assumption
// here is that if a resolve type has a finalizer, then the base
// should have a finalizer too.
const fc = try session.createFinalizerCallback(gop.value_ptr.*, resolved.ptr, resolved.finalizer_from_zig.?);
const fc = try ctx.createFinalizerCallback(gop.value_ptr.*, resolved.ptr, resolved.finalizer_from_zig.?);
{
errdefer fc.deinit();
try session.finalizer_callbacks.put(page_arena, @intFromPtr(resolved.ptr), fc);
try ctx.identity.finalizer_callbacks.put(ctx.identity_arena, @intFromPtr(resolved.ptr), fc);
}
conditionallyReference(value);