port Platform and Inspector to use v8's C handles/functions directly

This commit is contained in:
Karl Seguin
2025-12-31 12:39:25 +08:00
parent 8aaef674fe
commit 6ecf52cc03
7 changed files with 404 additions and 62 deletions

View File

@@ -119,8 +119,10 @@ pub fn deinit(self: *Env) void {
self.allocator.free(self.templates);
}
pub fn newInspector(self: *Env, arena: Allocator, ctx: anytype) !Inspector {
return Inspector.init(arena, self.isolate, ctx);
pub fn newInspector(self: *Env, arena: Allocator, ctx: anytype) !*Inspector {
const inspector = try arena.create(Inspector);
try Inspector.init(inspector, self.isolate.handle, ctx);
return inspector;
}
pub fn runMicrotasks(self: *const Env) void {
@@ -128,11 +130,11 @@ pub fn runMicrotasks(self: *const Env) void {
}
pub fn pumpMessageLoop(self: *const Env) bool {
return self.platform.inner.pumpMessageLoop(self.isolate, false);
return v8.c.v8__Platform__PumpMessageLoop(self.platform.handle, self.isolate.handle, false);
}
pub fn runIdleTasks(self: *const Env) void {
return self.platform.inner.runIdleTasks(self.isolate, 1);
v8.c.v8__Platform__RunIdleTasks(self.platform.handle, self.isolate.handle, 1);
}
pub fn newExecutionWorld(self: *Env) !ExecutionWorld {
return .{

View File

@@ -37,16 +37,6 @@ pub fn id(self: *const Function) u32 {
return @as(u32, @bitCast(v8.c.v8__Object__GetIdentityHash(@ptrCast(self.handle))));
}
pub fn getName(self: *const Function, allocator: Allocator) ![]const u8 {
const name = v8.c.v8__Function__GetName(self.handle).?;
return self.context.valueToString(name, .{ .allocator = allocator });
}
pub fn setName(self: *const Function, name: []const u8) void {
const v8_name = v8.String.initUtf8(self.context.isolate, name);
self.func.castToFunction().setName(v8_name);
}
pub fn withThis(self: *const Function, value: anytype) !Function {
const this_obj = if (@TypeOf(value) == js.Object)
value.js_obj
@@ -181,8 +171,7 @@ fn getThis(self: *const Function) v8.Object {
}
pub fn src(self: *const Function) ![]const u8 {
const value = self.func.castToFunction().toValue();
return self.context.valueToString(value, .{});
return self.context.valueToString(.{ .handle = @ptrCast(self.handle) }, .{});
}
pub fn getPropertyValue(self: *const Function, name: []const u8) !?js.Value {

View File

@@ -23,43 +23,76 @@ const v8 = js.v8;
const Context = @import("Context.zig");
const Allocator = std.mem.Allocator;
const RndGen = std.Random.DefaultPrng;
const CONTEST_GROUP_ID = 1;
const CLIENT_TRUST_LEVEL = 1;
const Inspector = @This();
pub const RemoteObject = v8.RemoteObject;
isolate: v8.Isolate,
inner: *v8.Inspector,
session: v8.InspectorSession,
handle: *v8.c.Inspector,
isolate: *v8.c.Isolate,
client: Client,
channel: Channel,
session: Session,
rnd: RndGen = RndGen.init(0),
ctx_handle: ?*const v8.c.Context = null,
// We expect allocator to be an arena
pub fn init(allocator: Allocator, isolate: v8.Isolate, ctx: anytype) !Inspector {
// Note: This initializes the pre-allocated inspector in-place
pub fn init(self: *Inspector, isolate: *v8.c.Isolate, ctx: anytype) !void {
const ContextT = @TypeOf(ctx);
const InspectorContainer = switch (@typeInfo(ContextT)) {
const Container = switch (@typeInfo(ContextT)) {
.@"struct" => ContextT,
.pointer => |ptr| ptr.child,
.void => NoopInspector,
else => @compileError("invalid context type"),
};
// If necessary, turn a void context into something we can safely ptrCast
const safe_context: *anyopaque = if (ContextT == void) @ptrCast(@constCast(&{})) else ctx;
const channel = v8.InspectorChannel.init(
// Initialize the fields that callbacks need first
self.* = .{
.handle = undefined,
.isolate = isolate,
.client = undefined,
.channel = undefined,
.rnd = RndGen.init(0),
.ctx_handle = null,
.session = undefined,
};
// Create client and set inspector data BEFORE creating the inspector
// because V8 will call generateUniqueId during inspector creation
const client = Client.init();
self.client = client;
client.setInspector(self);
// Now create the inspector - generateUniqueId will work because data is set
const handle = v8.c.v8_inspector__Inspector__Create(isolate, client.handle).?;
self.handle = handle;
// Create the channel
const channel = Channel.init(
safe_context,
InspectorContainer.onInspectorResponse,
InspectorContainer.onInspectorEvent,
InspectorContainer.onRunMessageLoopOnPause,
InspectorContainer.onQuitMessageLoopOnPause,
Container.onInspectorResponse,
Container.onInspectorEvent,
Container.onRunMessageLoopOnPause,
Container.onQuitMessageLoopOnPause,
isolate,
);
self.channel = channel;
channel.setInspector(self);
const client = v8.InspectorClient.init();
const inner = try allocator.create(v8.Inspector);
v8.Inspector.init(inner, client, channel, isolate);
return .{ .inner = inner, .isolate = isolate, .session = inner.connect() };
// Create the session
const session_handle = v8.c.v8_inspector__Inspector__Connect(
handle,
CONTEST_GROUP_ID,
channel.handle,
CLIENT_TRUST_LEVEL,
).?;
self.session = .{ .handle = session_handle };
}
pub fn deinit(self: *const Inspector) void {
@@ -68,7 +101,9 @@ pub fn deinit(self: *const Inspector) void {
defer temp_scope.deinit();
self.session.deinit();
self.inner.deinit();
self.client.deinit();
self.channel.deinit();
v8.c.v8_inspector__Inspector__DELETE(self.handle);
}
pub fn send(self: *const Inspector, msg: []const u8) void {
@@ -76,9 +111,9 @@ pub fn send(self: *const Inspector, msg: []const u8) void {
// available when doing this. Pages (and thus the HandleScope)
// comes and goes, but CDP can keep sending messages.
const isolate = self.isolate;
var temp_scope: v8.HandleScope = undefined;
v8.HandleScope.init(&temp_scope, isolate);
defer temp_scope.deinit();
var temp_scope: v8.c.HandleScope = undefined;
v8.c.v8__HandleScope__CONSTRUCT(&temp_scope, isolate);
defer v8.c.v8__HandleScope__DESTRUCT(&temp_scope);
self.session.dispatchProtocolMessage(isolate, msg);
}
@@ -99,7 +134,27 @@ pub fn contextCreated(
aux_data: ?[]const u8,
is_default_context: bool,
) void {
self.inner.contextCreated(context.v8_context, name, origin, aux_data, is_default_context);
_ = is_default_context; // TODO: Should this be passed to the C API?
var auxData_ptr: [*c]const u8 = undefined;
var auxData_len: usize = undefined;
if (aux_data) |data| {
auxData_ptr = data.ptr;
auxData_len = data.len;
} else {
auxData_ptr = null;
auxData_len = 0;
}
v8.c.v8_inspector__Inspector__ContextCreated(
self.handle,
name.ptr,
name.len,
origin.ptr,
origin.len,
auxData_ptr,
auxData_len,
CONTEST_GROUP_ID,
context.v8_context.handle,
);
}
// Retrieves the RemoteObject for a given value.
@@ -118,9 +173,9 @@ pub fn getRemoteObject(
// We do not want to expose this as a parameter for now
const generate_preview = false;
return self.session.wrapObject(
context.isolate,
context.v8_context,
js_value,
context.isolate.handle,
context.v8_context.handle,
js_value.handle,
group,
generate_preview,
);
@@ -136,15 +191,210 @@ pub fn getNodePtr(self: *const Inspector, allocator: Allocator, object_id: []con
const unwrapped = try self.session.unwrapObject(allocator, object_id);
// The values context and groupId are not used here
const js_val = unwrapped.value;
if (js_val.isObject() == false) {
if (!v8.c.v8__Value__IsObject(js_val)) {
return error.ObjectIdIsNotANode;
}
const Node = @import("../webapi/Node.zig");
return Context.typeTaggedAnyOpaque(*Node, js_val.castTo(v8.Object)) catch {
// Wrap the C handle in a v8.Object for typeTaggedAnyOpaque
const js_obj = v8.Object{ .handle = js_val };
return Context.typeTaggedAnyOpaque(*Node, js_obj) catch {
return error.ObjectIdIsNotANode;
};
}
pub const RemoteObject = struct {
handle: *v8.c.RemoteObject,
pub fn deinit(self: RemoteObject) void {
v8.c.v8_inspector__RemoteObject__DELETE(self.handle);
}
pub fn getType(self: RemoteObject, allocator: Allocator) ![]const u8 {
var ctype_: v8.c.CZigString = .{ .ptr = null, .len = 0 };
if (!v8.c.v8_inspector__RemoteObject__getType(self.handle, &allocator, &ctype_)) return error.V8AllocFailed;
return cZigStringToString(ctype_) orelse return error.InvalidType;
}
pub fn getSubtype(self: RemoteObject, allocator: Allocator) !?[]const u8 {
if (!v8.c.v8_inspector__RemoteObject__hasSubtype(self.handle)) return null;
var csubtype: v8.c.CZigString = .{ .ptr = null, .len = 0 };
if (!v8.c.v8_inspector__RemoteObject__getSubtype(self.handle, &allocator, &csubtype)) return error.V8AllocFailed;
return cZigStringToString(csubtype);
}
pub fn getClassName(self: RemoteObject, allocator: Allocator) !?[]const u8 {
if (!v8.c.v8_inspector__RemoteObject__hasClassName(self.handle)) return null;
var cclass_name: v8.c.CZigString = .{ .ptr = null, .len = 0 };
if (!v8.c.v8_inspector__RemoteObject__getClassName(self.handle, &allocator, &cclass_name)) return error.V8AllocFailed;
return cZigStringToString(cclass_name);
}
pub fn getDescription(self: RemoteObject, allocator: Allocator) !?[]const u8 {
if (!v8.c.v8_inspector__RemoteObject__hasDescription(self.handle)) return null;
var description: v8.c.CZigString = .{ .ptr = null, .len = 0 };
if (!v8.c.v8_inspector__RemoteObject__getDescription(self.handle, &allocator, &description)) return error.V8AllocFailed;
return cZigStringToString(description);
}
pub fn getObjectId(self: RemoteObject, allocator: Allocator) !?[]const u8 {
if (!v8.c.v8_inspector__RemoteObject__hasObjectId(self.handle)) return null;
var cobject_id: v8.c.CZigString = .{ .ptr = null, .len = 0 };
if (!v8.c.v8_inspector__RemoteObject__getObjectId(self.handle, &allocator, &cobject_id)) return error.V8AllocFailed;
return cZigStringToString(cobject_id);
}
};
const Session = struct {
handle: *v8.c.InspectorSession,
fn deinit(self: Session) void {
v8.c.v8_inspector__Session__DELETE(self.handle);
}
fn dispatchProtocolMessage(self: Session, isolate: *v8.c.Isolate, msg: []const u8) void {
v8.c.v8_inspector__Session__dispatchProtocolMessage(
self.handle,
isolate,
msg.ptr,
msg.len,
);
}
fn wrapObject(
self: Session,
isolate: *v8.c.Isolate,
ctx: *const v8.c.Context,
val: *const v8.c.Value,
grpname: []const u8,
generatepreview: bool,
) !RemoteObject {
const remote_object = v8.c.v8_inspector__Session__wrapObject(
self.handle,
isolate,
ctx,
val,
grpname.ptr,
grpname.len,
generatepreview,
).?;
return .{ .handle = remote_object };
}
fn unwrapObject(
self: Session,
allocator: Allocator,
object_id: []const u8,
) !UnwrappedObject {
const in_object_id = v8.c.CZigString{
.ptr = object_id.ptr,
.len = object_id.len,
};
var out_error: v8.c.CZigString = .{ .ptr = null, .len = 0 };
var out_value_handle: ?*v8.c.Value = null;
var out_context_handle: ?*v8.c.Context = null;
var out_object_group: v8.c.CZigString = .{ .ptr = null, .len = 0 };
const result = v8.c.v8_inspector__Session__unwrapObject(
self.handle,
&allocator,
&out_error,
in_object_id,
&out_value_handle,
&out_context_handle,
&out_object_group,
);
if (!result) {
const error_str = cZigStringToString(out_error) orelse return error.UnwrapFailed;
std.log.err("unwrapObject failed: {s}", .{error_str});
return error.UnwrapFailed;
}
return .{
.value = out_value_handle.?,
.context = out_context_handle.?,
.object_group = cZigStringToString(out_object_group),
};
}
};
const UnwrappedObject = struct {
value: *const v8.c.Value,
context: *const v8.c.Context,
object_group: ?[]const u8,
};
const Channel = struct {
handle: *v8.c.InspectorChannelImpl,
// callbacks
ctx: *anyopaque,
onNotif: onNotifFn = undefined,
onResp: onRespFn = undefined,
onRunMessageLoopOnPause: onRunMessageLoopOnPauseFn = undefined,
onQuitMessageLoopOnPause: onQuitMessageLoopOnPauseFn = undefined,
pub const onNotifFn = *const fn (ctx: *anyopaque, msg: []const u8) void;
pub const onRespFn = *const fn (ctx: *anyopaque, call_id: u32, msg: []const u8) void;
pub const onRunMessageLoopOnPauseFn = *const fn (ctx: *anyopaque, context_group_id: u32) void;
pub const onQuitMessageLoopOnPauseFn = *const fn (ctx: *anyopaque) void;
fn init(
ctx: *anyopaque,
onResp: onRespFn,
onNotif: onNotifFn,
onRunMessageLoopOnPause: onRunMessageLoopOnPauseFn,
onQuitMessageLoopOnPause: onQuitMessageLoopOnPauseFn,
isolate: *v8.c.Isolate,
) Channel {
const handle = v8.c.v8_inspector__Channel__IMPL__CREATE(isolate);
return .{
.handle = handle,
.ctx = ctx,
.onResp = onResp,
.onNotif = onNotif,
.onRunMessageLoopOnPause = onRunMessageLoopOnPause,
.onQuitMessageLoopOnPause = onQuitMessageLoopOnPause,
};
}
fn deinit(self: Channel) void {
v8.c.v8_inspector__Channel__IMPL__DELETE(self.handle);
}
fn setInspector(self: Channel, inspector: *anyopaque) void {
v8.c.v8_inspector__Channel__IMPL__SET_DATA(self.handle, inspector);
}
fn resp(self: Channel, call_id: u32, msg: []const u8) void {
self.onResp(self.ctx, call_id, msg);
}
fn notif(self: Channel, msg: []const u8) void {
self.onNotif(self.ctx, msg);
}
};
const Client = struct {
handle: *v8.c.InspectorClientImpl,
fn init() Client {
return .{ .handle = v8.c.v8_inspector__Client__IMPL__CREATE() };
}
fn deinit(self: Client) void {
v8.c.v8_inspector__Client__IMPL__DELETE(self.handle);
}
fn setInspector(self: Client, inspector: *anyopaque) void {
v8.c.v8_inspector__Client__IMPL__SET_DATA(self.handle, inspector);
}
};
const NoopInspector = struct {
pub fn onInspectorResponse(_: *anyopaque, _: u32, _: []const u8) void {}
pub fn onInspectorEvent(_: *anyopaque, _: []const u8) void {}
@@ -152,15 +402,107 @@ const NoopInspector = struct {
pub fn onQuitMessageLoopOnPause(_: *anyopaque) void {}
};
pub fn getTaggedAnyOpaque(value: v8.Value) ?*js.TaggedAnyOpaque {
if (value.isObject() == false) {
fn fromData(data: *anyopaque) *Inspector {
return @ptrCast(@alignCast(data));
}
pub fn getTaggedAnyOpaque(value: *const v8.c.Value) ?*js.TaggedAnyOpaque {
if (!v8.c.v8__Value__IsObject(value)) {
return null;
}
const obj = value.castTo(v8.Object);
if (obj.internalFieldCount() == 0) {
const internal_field_count = v8.c.v8__Object__InternalFieldCount(value);
if (internal_field_count == 0) {
return null;
}
const external_data = obj.getInternalField(0).castTo(v8.External).get().?;
const external_value = v8.c.v8__Object__GetInternalField(value, 0).?;
const external_data = v8.c.v8__External__Value(external_value).?;
return @ptrCast(@alignCast(external_data));
}
fn cZigStringToString(s: v8.c.CZigString) ?[]const u8 {
if (s.ptr == null) return null;
return s.ptr[0..s.len];
}
// C export functions for Inspector callbacks
pub export fn v8_inspector__Client__IMPL__generateUniqueId(
_: *v8.c.InspectorClientImpl,
data: *anyopaque,
) callconv(.c) i64 {
const inspector: *Inspector = @ptrCast(@alignCast(data));
return inspector.rnd.random().int(i64);
}
pub export fn v8_inspector__Client__IMPL__runMessageLoopOnPause(
_: *v8.c.InspectorClientImpl,
data: *anyopaque,
ctx_group_id: c_int,
) callconv(.c) void {
const inspector: *Inspector = @ptrCast(@alignCast(data));
inspector.channel.onRunMessageLoopOnPause(inspector.channel.ctx, @intCast(ctx_group_id));
}
pub export fn v8_inspector__Client__IMPL__quitMessageLoopOnPause(
_: *v8.c.InspectorClientImpl,
data: *anyopaque,
) callconv(.c) void {
const inspector: *Inspector = @ptrCast(@alignCast(data));
inspector.channel.onQuitMessageLoopOnPause(inspector.channel.ctx);
}
pub export fn v8_inspector__Client__IMPL__runIfWaitingForDebugger(
_: *v8.c.InspectorClientImpl,
_: *anyopaque,
_: c_int,
) callconv(.c) void {
// TODO
}
pub export fn v8_inspector__Client__IMPL__consoleAPIMessage(
_: *v8.c.InspectorClientImpl,
_: *anyopaque,
_: c_int,
_: v8.c.MessageErrorLevel,
_: *v8.c.StringView,
_: *v8.c.StringView,
_: c_uint,
_: c_uint,
_: *v8.c.StackTrace,
) callconv(.c) void {}
pub export fn v8_inspector__Client__IMPL__ensureDefaultContextInGroup(
_: *v8.c.InspectorClientImpl,
data: *anyopaque,
) callconv(.c) ?*const v8.c.Context {
const inspector: *Inspector = @ptrCast(@alignCast(data));
return inspector.ctx_handle;
}
pub export fn v8_inspector__Channel__IMPL__sendResponse(
_: *v8.c.InspectorChannelImpl,
data: *anyopaque,
call_id: c_int,
msg: [*c]u8,
length: usize,
) callconv(.c) void {
const inspector: *Inspector = @ptrCast(@alignCast(data));
inspector.channel.resp(@as(u32, @intCast(call_id)), msg[0..length]);
}
pub export fn v8_inspector__Channel__IMPL__sendNotification(
_: *v8.c.InspectorChannelImpl,
data: *anyopaque,
msg: [*c]u8,
length: usize,
) callconv(.c) void {
const inspector: *Inspector = @ptrCast(@alignCast(data));
inspector.channel.notif(msg[0..length]);
}
pub export fn v8_inspector__Channel__IMPL__flushProtocolNotifications(
_: *v8.c.InspectorChannelImpl,
_: *anyopaque,
) callconv(.c) void {
// TODO
}

View File

@@ -20,20 +20,22 @@ const js = @import("js.zig");
const v8 = js.v8;
const Platform = @This();
inner: v8.Platform,
handle: *v8.c.Platform,
pub fn init() !Platform {
if (v8.initV8ICU() == false) {
if (v8.c.v8__V8__InitializeICU() == false) {
return error.FailedToInitializeICU;
}
const platform = v8.Platform.initDefault(0, true);
v8.initV8Platform(platform);
v8.initV8();
return .{ .inner = platform };
// 0 - threadpool size, 0 == let v8 decide
// 1 - idle_task_support, 1 == enabled
const handle = v8.c.v8__Platform__NewDefaultPlatform(0, 1).?;
v8.c.v8__V8__InitializePlatform(handle);
v8.c.v8__V8__Initialize();
return .{ .handle = handle };
}
pub fn deinit(self: Platform) void {
_ = v8.deinitV8();
v8.deinitV8Platform();
self.inner.deinit();
_ = v8.c.v8__V8__Dispose();
v8.c.v8__V8__DisposePlatform();
v8.c.v8__Platform__DELETE(self.handle);
}

View File

@@ -481,7 +481,7 @@ pub export fn v8_inspector__Client__IMPL__valueSubtype(
_: *v8.c.InspectorClientImpl,
c_value: *const v8.C_Value,
) callconv(.c) [*c]const u8 {
const external_entry = Inspector.getTaggedAnyOpaque(.{ .handle = c_value }) orelse return null;
const external_entry = Inspector.getTaggedAnyOpaque(c_value) orelse return null;
return if (external_entry.subtype) |st| @tagName(st) else null;
}
@@ -498,10 +498,17 @@ pub export fn v8_inspector__Client__IMPL__descriptionForValueSubtype(
// We _must_ include a non-null description in order for the subtype value
// to be included. Besides that, I don't know if the value has any meaning
const external_entry = Inspector.getTaggedAnyOpaque(.{ .handle = c_value }) orelse return null;
const external_entry = Inspector.getTaggedAnyOpaque(c_value) orelse return null;
return if (external_entry.subtype == null) null else "";
}
/// Enables C to allocate using the given Zig allocator
pub export fn zigAlloc(self: *anyopaque, bytes: usize) callconv(.c) ?[*]u8 {
const allocator: *Allocator = @ptrCast(@alignCast(self));
const allocated_bytes = allocator.alloc(u8, bytes) catch return null;
return allocated_bytes.ptr;
}
test "TaggedAnyOpaque" {
// If we grow this, fine, but it should be a conscious decision
try std.testing.expectEqual(24, @sizeOf(TaggedAnyOpaque));

View File

@@ -331,7 +331,7 @@ pub fn BrowserContext(comptime CDP_T: type) type {
node_registry: Node.Registry,
node_search_list: Node.Search.List,
inspector: js.Inspector,
inspector: *js.Inspector,
isolated_worlds: std.ArrayListUnmanaged(IsolatedWorld),
http_proxy_changed: bool = false,

View File

@@ -135,7 +135,7 @@ fn run(
return err;
};
return value.toString(arena);
return value.toString(.{ .allocator = arena });
}
const Writer = struct {