discrete type for fast functions

This commit is contained in:
Halil Durak committed 2026-10-16 15:44:41 +03:00
1 parent eac4af243a
commit 3d0b4eabae
3 files changed
+233 -55

No files matched your search

+163
View File
@@ -1024,6 +1024,169 @@ pub const Function = struct {
};
};
/// V8 Fast API integration.
pub const FastFunction = struct {
pub const Options = struct {
static: bool,
exposed: Function.Opts.Exposed,
};
inline fn isPointer(comptime T: type) bool {
const Inner = if (@typeInfo(T) == .optional) @typeInfo(T).optional.child else T;
return @typeInfo(Inner) == .pointer;
}
/// Only primitives and V8 values are allowed.
fn Arg(comptime T: type) type {
// Treat pointers as V8 values; we can change this in the future.
if (comptime isPointer(T)) {
return ?*const v8.Value;
}
// Primitives.
return switch (comptime T) {
void, bool, i32, u32, i64, u64, f32, f64 => T,
else => @compileError("type '" ++ @typeName(T) ++ "' is not allowed in Fast Functions"),
};
}
fn toCTypeEnum(comptime T: type) v8.CTypeInfo {
// Treat pointers as V8 values; we can change this in the future.
if (comptime isPointer(T)) {
return .{ .type = v8.kCTypeInfoType_V8Value };
}
// Primitives.
return switch (T) {
void => .{ .type = v8.kCTypeInfoType_Void },
bool => .{ .type = v8.kCTypeInfoType_Bool },
u8 => unreachable, // u8 return crashes TurboFan.
i32 => .{ .type = v8.kCTypeInfoType_Int32 },
u32 => .{ .type = v8.kCTypeInfoType_Uint32 },
i64 => .{ .type = v8.kCTypeInfoType_Int64 },
u64 => .{ .type = v8.kCTypeInfoType_Uint64 },
f32 => .{ .type = v8.kCTypeInfoType_Float32 },
f64 => .{ .type = v8.kCTypeInfoType_Float64 },
else => unreachable,
};
}
/// Mirrors `.optional` / `.pointer` branches from `Local.jsValueToZig`.
fn toZigObject(comptime T: type, handle: ?*const v8.Value) error{InvalidArgument}!T {
const value = handle.?;
if (@typeInfo(T) == .optional) {
if (v8.v8__Value__IsNullOrUndefined(value)) return null;
}
if (!v8.v8__Value__IsObject(value)) return error.InvalidArgument;
const Pointer = if (@typeInfo(T) == .optional) @typeInfo(T).optional.child else T;
return TaggedOpaque.fromJS(Pointer, @ptrCast(value));
}
fn canReturnErr(comptime T: type) struct { type, bool } {
return switch (@typeInfo(T)) {
.error_union => |info| .{ info.payload, true },
.error_set => unreachable,
else => .{ T, false },
};
}
/// Creates a constant `v8.CFunction`.
pub fn create(comptime function: anytype, comptime options: Options) *const v8.CFunction {
// Bunch of comptime-known stuff.
const Fn = @TypeOf(function);
const info = @typeInfo(Fn).@"fn";
const P = info.param_types;
// TODO: Check if last argument is `*js.Execution` or `*const js.Execution`.
if (comptime !options.static and P.len == 0) {
@compileError("non-static fast functions need a receiver parameter");
}
// Parameters bound from JS arguments; a non-static function's `self`
// comes from the receiver instead.
const offset = if (options.static) 0 else 1;
const JsP = P[offset..];
const R, const can_return_err = canReturnErr(info.return_type orelse {
@compileError("fast functions cannot return generic types");
});
// TODO: Support this in the future.
if (comptime can_return_err) {
@compileError("fast functions cannot return errors currently");
}
const Opts = *const v8.FastApiCallbackOptions;
const type_safe_call = struct {
inline fn wrap(c_args: anytype, opts: Opts) R {
var args: ParameterTypes(Fn) = undefined;
if (comptime !options.static) {
// Cannot fail.
args[0] = TaggedOpaque.fromJS(P[0].?, @ptrCast(c_args[0].?)) catch unreachable;
}
inline for (JsP, 0..) |param_type, i| {
const c_arg = c_args[i + 1];
args[i + offset] = blk: {
const ParamType = param_type.?;
if (comptime isPointer(ParamType)) {
break :blk toZigObject(ParamType, c_arg) catch {
// On failure, we get into handle scope, this
// should only be done with `opts.isolate`.
const isolate_handle = opts.isolate.?;
const isolate: js.Isolate = .{ .handle = isolate_handle };
var scope: js.HandleScope = undefined;
scope.initWithIsolateHandle(isolate_handle);
defer scope.deinit();
_ = isolate.throwException(isolate.createTypeError("invalid argument"));
return std.mem.zeroes(R);
};
}
// Primitive.
break :blk c_arg;
};
}
return @call(.auto, function, args);
}
}.wrap;
// Zig doesn't allow us to build parameter lists in comptime. So we
// switch on the JS argument count and prefer a wrapper depending on it.
const func = switch (comptime JsP.len) {
0 => struct {
fn wrap(recv: ?*const v8.Value, opts: Opts) callconv(.c) R {
return type_safe_call(.{recv}, opts);
}
}.wrap,
1 => struct {
fn wrap(recv: ?*const v8.Value, a0: Arg(JsP[0].?), opts: Opts) callconv(.c) R {
return type_safe_call(.{ recv, a0 }, opts);
}
}.wrap,
2 => struct {
fn wrap(recv: ?*const v8.Value, a0: Arg(JsP[0].?), a1: Arg(JsP[1].?), opts: Opts) callconv(.c) R {
return type_safe_call(.{ recv, a0, a1 }, opts);
}
}.wrap,
3 => struct {
fn wrap(recv: ?*const v8.Value, a0: Arg(JsP[0].?), a1: Arg(JsP[1].?), a2: Arg(JsP[2].?), opts: Opts) callconv(.c) R {
return type_safe_call(.{ recv, a0, a1, a2 }, opts);
}
}.wrap,
else => @compileError("exceeded argument limit for fast functions"),
};
const args = comptime blk: {
// [receiver] + [JS arguments] + [v8.FastApiCallbackOptions].
var list: [JsP.len + 2]v8.CTypeInfo = undefined;
list[0] = .{ .type = v8.kCTypeInfoType_V8Value };
for (JsP, 1..) |t, i| list[i] = toCTypeEnum(t.?);
list[list.len - 1] = .{ .type = v8.kCTypeInfoType_CallbackOptions };
break :blk list;
};
const func_info: *const v8.CFunctionInfo = &.{
.return_info = toCTypeEnum(R),
.arg_count = args.len,
.arg_info = &args,
};
return &.{ .address = @ptrCast(&func), .type_info = func_info };
}
};
// If we call a method in javascript: cat.lives('nine');
//
// Then we'd expect a Zig function with 2 parameters: a self and the string.
+32 -2
View File
@@ -462,6 +462,9 @@ fn countExternalReferences() comptime_int {
continue;
}
count += 1;
} else if (T == bridge.FastFunction) {
// slow callback + v8.CFunction
count += 2;
} else if (T == bridge.Iterator) {
count += 1;
} else if (T == bridge.Indexed) {
@@ -546,7 +549,6 @@ fn collectExternalReferences() *const ExternalReferences {
references[idx] = @bitCast(@intFromPtr(value.getter));
idx += 1;
if (value.fast_getter) |fast_getter| {
// V8 stores a pointer to the CFunction struct itself, not to fast_method_fn.
references[idx] = @bitCast(@intFromPtr(fast_getter));
idx += 1;
}
@@ -560,6 +562,13 @@ fn collectExternalReferences() *const ExternalReferences {
}
references[idx] = @bitCast(@intFromPtr(value.func));
idx += 1;
} else if (T == bridge.FastFunction) {
references[idx] = @bitCast(@intFromPtr(value.slow));
idx += 1;
// V8 stores a pointer to the v8.CFunction itself (not its
// .address) as a Foreign in the template's C function overloads.
references[idx] = @bitCast(@intFromPtr(value.fast));
idx += 1;
} else if (T == bridge.Iterator) {
references[idx] = @bitCast(@intFromPtr(value.func));
idx += 1;
@@ -880,6 +889,25 @@ fn attachClass(comptime JsApi: type, comptime flatten: bool, isolate: *v8.Isolat
v8.v8__Template__Set(@ptrCast(define_on orelse member_template), js_name, @ptrCast(function_template), v8.None);
}
},
bridge.FastFunction => {
const func_signature = if (value.static) null else signature;
const function_template = v8.v8__FunctionTemplate__New__CFunction(isolate, &.{
.callback = value.slow,
.signature = func_signature,
.length = @intCast(value.arity()),
.behavior = v8.kConstructorBehavior_Throw,
.side_effect_type = v8.kSideEffectType_HasSideEffect,
}, value.fast, 0, 0, 0).?;
const js_name = v8.v8__String__NewFromUtf8(isolate, name.ptr, v8.kNormal, @intCast(name.len));
v8.v8__FunctionTemplate__SetClassName(function_template, js_name);
if (value.static and !own_properties) {
v8.v8__Template__Set(@ptrCast(template), js_name, @ptrCast(function_template), v8.None);
} else {
// Web IDL: operations on the interface prototype object
// (and mirrored onto [Global] instances) are enumerable.
v8.v8__Template__Set(@ptrCast(define_on orelse member_template), js_name, @ptrCast(function_template), v8.None);
}
},
bridge.Indexed => {
var configuration: v8.IndexedPropertyHandlerConfiguration = .{
.getter = value.getter,
@@ -1033,9 +1061,11 @@ fn attachAccessorProperty(comptime name: [:0]const u8, value: bridge.Accessor, i
const getter_callback: *const v8.FunctionTemplate = blk: {
if (value.fast_getter) |fast_getter| {
// You can pass `data` through this constructor:
// https://v8docs.nodesource.com/node-25.0/d5/dfd/structv8_1_1_fast_api_callback_options.html#ad3be03826a4a0cfbf72eab7eac9cc3db
// For our use cases, this make no sense though.
break :blk v8.v8__FunctionTemplate__New__CFunction(isolate, &.{
.callback = value.getter,
.data = null,
.signature = getter_signature,
.length = 0,
.behavior = v8.kConstructorBehavior_Throw, // Required with a fast function.
+38 -53
View File
@@ -46,6 +46,10 @@ pub fn Builder(comptime T: type) type {
return Function.init(T, func, opts);
}
pub fn fastFunction(comptime func: anytype, comptime opts: Caller.FastFunction.Options) FastFunction {
return FastFunction.init(T, func, opts);
}
pub fn indexed(comptime getter_func: anytype, comptime enumerator_func: anytype, comptime opts: Indexed.Opts) Indexed {
return Indexed.init(T, getter_func, null, null, null, null, enumerator_func, opts);
}
@@ -241,7 +245,40 @@ pub const Function = struct {
}
};
const TaggedOpaque = @import("TaggedOpaque.zig");
pub const FastFunction = struct {
static: bool,
exposed: Caller.Function.Opts.Exposed,
/// V8 still requires us to create a slow variant.
slow: *const fn (?*const v8.FunctionCallbackInfo) callconv(.c) void,
fast: *const v8.CFunction,
fn init(
comptime T: type,
comptime func: anytype,
comptime opts: Caller.FastFunction.Options,
) FastFunction {
return .{
.static = opts.static,
.exposed = opts.exposed,
.slow = struct {
fn wrap(handle: ?*const v8.FunctionCallbackInfo) callconv(.c) void {
return Caller.Function.call(T, handle.?, func, .{
.static = opts.static,
.exposed = opts.exposed,
});
}
}.wrap,
.fast = Caller.FastFunction.create(func, .{
.static = opts.static,
.exposed = opts.exposed,
}),
};
}
pub inline fn arity(self: FastFunction) c_uint {
return self.fast.type_info.*.arg_count - 2;
}
};
pub const Accessor = struct {
static: bool = false,
@@ -281,58 +318,6 @@ pub const Accessor = struct {
Caller.Function.call(T, handle.?, getter, getter_opts);
}
}.wrap;
const can_be_fast = blk: {
switch (@typeInfo(@TypeOf(getter))) {
.@"fn" => |info| {
const param_types = info.param_types;
break :blk param_types.len == 1 and
opts.static == false and
opts.cache == null and
opts.noop == false and
opts.embedded_receiver == false;
},
.null => false,
else => unreachable,
}
};
fast_getter: {
if (comptime can_be_fast) {
const R = @typeInfo(@TypeOf(getter)).@"fn".return_type.?;
const host_to_v8_type = switch (comptime R) {
void => v8.kCTypeInfoType_Void,
bool => v8.kCTypeInfoType_Bool,
u8 => break :fast_getter, // u8 return crashes TurboFan.
i32 => v8.kCTypeInfoType_Int32,
u32 => v8.kCTypeInfoType_Uint32,
i64 => v8.kCTypeInfoType_Int64,
u64 => v8.kCTypeInfoType_Uint64,
f32 => v8.kCTypeInfoType_Float32,
f64 => v8.kCTypeInfoType_Float64,
else => break :fast_getter,
};
const fast_fn = struct {
fn wrap(handle: ?*const v8.Value) callconv(.c) R {
const self = TaggedOpaque.fromJS(*T, handle.?) catch unreachable;
return @call(.auto, getter, .{self});
}
}.wrap;
const fast_method_info: *const v8.CFunctionInfo = &.{
.return_info = .{ .type = host_to_v8_type },
.arg_count = fast_method_args.len,
.arg_info = &fast_method_args,
};
accessor.fast_getter = &.{
.address = @ptrCast(&fast_fn),
.type_info = fast_method_info,
};
}
}
}
if (@typeInfo(@TypeOf(setter)) != .null) {