494 lines
18 KiB
Zig
494 lines
18 KiB
Zig
const std = @import("std");
|
|
const mem = std.mem;
|
|
const trait = std.meta.trait;
|
|
|
|
const assert = std.debug.assert;
|
|
const expect = std.testing.expect;
|
|
const expectEqual = std.testing.expectEqual;
|
|
|
|
pub const SelfType = opaque{};
|
|
|
|
fn makeSelfPtr(ptr: anytype) *SelfType {
|
|
if (comptime !trait.isSingleItemPtr(@TypeOf(ptr))) {
|
|
@compileError("SelfType pointer initialization expects pointer parameter.");
|
|
}
|
|
|
|
const T = std.meta.Child(@TypeOf(ptr));
|
|
|
|
if (@sizeOf(T) > 0) {
|
|
return @ptrCast(*SelfType, ptr);
|
|
} else {
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
fn selfPtrAs(self: *SelfType, comptime T: type) *T {
|
|
if (@sizeOf(T) > 0) {
|
|
return @alignCast(@alignOf(T), @ptrCast(*align(1) T, self));
|
|
} else {
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
fn constSelfPtrAs(self: *const SelfType, comptime T: type) *const T {
|
|
if (@sizeOf(T) > 0) {
|
|
return @alignCast(@alignOf(T), @ptrCast(*align(1) const T, self));
|
|
} else {
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
pub const Storage = struct {
|
|
pub const Comptime = struct {
|
|
erased_ptr: *SelfType,
|
|
ImplType: type,
|
|
|
|
fn makeInit(comptime TInterface: type) type {
|
|
return struct {
|
|
fn init(obj: anytype) !TInterface {
|
|
const ImplType = PtrChildOrSelf(@TypeOf(obj));
|
|
|
|
comptime var obj_holder = obj;
|
|
|
|
return TInterface{
|
|
.vtable_ptr = &comptime makeVTable(TInterface.VTable, ImplType),
|
|
.storage = Comptime{
|
|
.erased_ptr = makeSelfPtr(&obj_holder),
|
|
.ImplType = @TypeOf(obj),
|
|
},
|
|
};
|
|
}
|
|
};
|
|
}
|
|
|
|
pub fn getSelfPtr(comptime self: *Comptime) *SelfType {
|
|
return self.erased_ptr;
|
|
}
|
|
|
|
pub fn deinit(comptime self: Comptime) void {}
|
|
};
|
|
|
|
pub const NonOwning = struct {
|
|
erased_ptr: *SelfType,
|
|
|
|
fn makeInit(comptime TInterface: type) type {
|
|
return struct {
|
|
fn init(ptr: anytype) !TInterface {
|
|
return TInterface{
|
|
.vtable_ptr = &comptime makeVTable(TInterface.VTable, PtrChildOrSelf(@TypeOf(ptr))),
|
|
.storage = NonOwning{
|
|
.erased_ptr = makeSelfPtr(ptr),
|
|
},
|
|
};
|
|
}
|
|
};
|
|
}
|
|
|
|
pub fn getSelfPtr(self: NonOwning) *SelfType {
|
|
return self.erased_ptr;
|
|
}
|
|
|
|
pub fn deinit(self: NonOwning) void {}
|
|
};
|
|
|
|
pub const Owning = struct {
|
|
allocator: *mem.Allocator,
|
|
mem: []u8,
|
|
|
|
fn makeInit(comptime TInterface: type) type {
|
|
return struct {
|
|
fn init(obj: anytype, allocator: *std.mem.Allocator) !TInterface {
|
|
const AllocT = @TypeOf(obj);
|
|
|
|
var ptr = try allocator.create(AllocT);
|
|
ptr.* = obj;
|
|
|
|
return TInterface{
|
|
.vtable_ptr = &comptime makeVTable(TInterface.VTable, PtrChildOrSelf(AllocT)),
|
|
.storage = Owning{
|
|
.allocator = allocator,
|
|
.mem = std.mem.asBytes(ptr)[0..],
|
|
},
|
|
};
|
|
}
|
|
};
|
|
}
|
|
|
|
pub fn getSelfPtr(self: Owning) *SelfType {
|
|
return makeSelfPtr(&self.mem[0]);
|
|
}
|
|
|
|
pub fn deinit(self: Owning) void {
|
|
const result = self.allocator.shrinkBytes(self.mem, 0, 0, 0, 0);
|
|
assert(result == 0);
|
|
}
|
|
};
|
|
|
|
pub fn Inline(comptime size: usize) type {
|
|
return struct {
|
|
const Self = @This();
|
|
|
|
mem: [size]u8,
|
|
|
|
fn makeInit(comptime TInterface: type) type {
|
|
return struct {
|
|
fn init(value: anytype) !TInterface {
|
|
const ImplSize = @sizeOf(@TypeOf(value));
|
|
|
|
if (ImplSize > size) {
|
|
@compileError("Type does not fit in inline storage.");
|
|
}
|
|
|
|
var self = Self{
|
|
.mem = undefined,
|
|
};
|
|
if (ImplSize > 0) {
|
|
std.mem.copy(u8, self.mem[0..], @ptrCast([*]const u8, &args[0])[0..ImplSize]);
|
|
}
|
|
|
|
return TInterface{
|
|
.vtable_ptr = &comptime makeVTable(TInterface.VTable, PtrChildOrSelf(@TypeOf(value))),
|
|
.storage = self,
|
|
};
|
|
}
|
|
};
|
|
}
|
|
|
|
pub fn getSelfPtr(self: *Self) *SelfType {
|
|
return makeSelfPtr(&self.mem[0]);
|
|
}
|
|
|
|
pub fn deinit(self: Self) void {}
|
|
};
|
|
}
|
|
|
|
pub fn InlineOrOwning(comptime size: usize) type {
|
|
return struct {
|
|
const Self = @This();
|
|
|
|
data: union(enum) {
|
|
Inline: Inline(size),
|
|
Owning: Owning,
|
|
},
|
|
|
|
pub fn init(args: anytype) !Self {
|
|
if (args.len != 2) {
|
|
@compileError("InlineOrOwning storage expected a 2-tuple in initialization.");
|
|
}
|
|
|
|
const ImplSize = @sizeOf(@TypeOf(args[0]));
|
|
|
|
if (ImplSize > size) {
|
|
return Self{
|
|
.data = .{
|
|
.Owning = try Owning.init(args),
|
|
},
|
|
};
|
|
} else {
|
|
return Self{
|
|
.data = .{
|
|
.Inline = try Inline(size).init(.{args[0]}),
|
|
},
|
|
};
|
|
}
|
|
}
|
|
|
|
pub fn getSelfPtr(self: *Self) *SelfType {
|
|
return switch (self.data) {
|
|
.Inline => |*i| i.getSelfPtr(),
|
|
.Owning => |*o| o.getSelfPtr(),
|
|
};
|
|
}
|
|
|
|
pub fn deinit(self: Self) void {
|
|
switch (self.data) {
|
|
.Inline => |i| i.deinit(),
|
|
.Owning => |o| o.deinit(),
|
|
}
|
|
}
|
|
};
|
|
}
|
|
};
|
|
|
|
fn PtrChildOrSelf(comptime T: type) type {
|
|
if (comptime trait.isSingleItemPtr(T)) {
|
|
return std.meta.Child(T);
|
|
}
|
|
|
|
return T;
|
|
}
|
|
|
|
const GenCallType = enum {
|
|
BothAsync,
|
|
BothBlocking,
|
|
AsyncCallsBlocking,
|
|
BlockingCallsAsync,
|
|
};
|
|
|
|
fn makeCall(
|
|
comptime name: []const u8,
|
|
comptime CurrSelfType: type,
|
|
comptime Return: type,
|
|
comptime ImplT: type,
|
|
comptime call_type: GenCallType,
|
|
self_ptr: CurrSelfType,
|
|
args: anytype,
|
|
) Return {
|
|
const is_const = CurrSelfType == *const SelfType;
|
|
const self = if (is_const) constSelfPtrAs(self_ptr, ImplT) else selfPtrAs(self_ptr, ImplT);
|
|
const fptr = @field(ImplT, name);
|
|
const first_arg_ptr = comptime std.meta.trait.is(.Pointer)(@typeInfo(@TypeOf(fptr)).Fn.args[0].arg_type.?);
|
|
const self_arg = if (first_arg_ptr) .{self} else .{self.*};
|
|
|
|
return switch (call_type) {
|
|
.BothBlocking => @call(.{ .modifier = .always_inline }, fptr, self_arg ++ args),
|
|
.AsyncCallsBlocking, .BothAsync => await @call(.{ .modifier = .async_kw }, fptr, self_arg ++ args),
|
|
.BlockingCallsAsync => @compileError("Trying to implement blocking virtual function " ++ name ++ " with async implementation."),
|
|
};
|
|
}
|
|
|
|
fn getFunctionFromImpl(comptime name: []const u8, comptime FnT: type, comptime ImplT: type) ?FnT {
|
|
const our_cc = @typeInfo(FnT).Fn.calling_convention;
|
|
|
|
// Find the candidate in the implementation type.
|
|
for (std.meta.declarations(ImplT)) |decl| {
|
|
if (std.mem.eql(u8, name, decl.name)) {
|
|
switch (decl.data) {
|
|
.Fn => |fn_decl| {
|
|
const args = @typeInfo(fn_decl.fn_type).Fn.args;
|
|
|
|
if (args.len == 0) {
|
|
return @field(ImplT, name);
|
|
}
|
|
|
|
if (args.len > 0) {
|
|
const arg0_type = args[0].arg_type.?;
|
|
const is_method = arg0_type == ImplT or arg0_type == *ImplT or arg0_type == *const ImplT;
|
|
|
|
const candidate_cc = @typeInfo(fn_decl.fn_type).Fn.calling_convention;
|
|
switch (candidate_cc) {
|
|
.Async, .Unspecified => {},
|
|
else => return null,
|
|
}
|
|
|
|
const Return = @typeInfo(FnT).Fn.return_type orelse noreturn;
|
|
const CurrSelfType = @typeInfo(FnT).Fn.args[0].arg_type.?;
|
|
|
|
const call_type: GenCallType = switch (our_cc) {
|
|
.Async => if (candidate_cc == .Async) .BothAsync else .AsyncCallsBlocking,
|
|
.Unspecified => if (candidate_cc == .Unspecified) .BothBlocking else .BlockingCallsAsync,
|
|
else => unreachable,
|
|
};
|
|
|
|
if (!is_method) {
|
|
return @field(ImplT, name);
|
|
}
|
|
|
|
// TODO: Make this less hacky somehow?
|
|
// We need some new feature to do so unfortunately.
|
|
return switch (args.len) {
|
|
1 => struct {
|
|
fn impl(self_ptr: CurrSelfType) callconv(our_cc) Return {
|
|
return @call(.{ .modifier = .always_inline }, makeCall, .{ name, CurrSelfType, Return, ImplT, call_type, self_ptr, .{} });
|
|
}
|
|
}.impl,
|
|
2 => struct {
|
|
fn impl(self_ptr: CurrSelfType, arg: args[1].arg_type.?) callconv(our_cc) Return {
|
|
return @call(.{ .modifier = .always_inline }, makeCall, .{ name, CurrSelfType, Return, ImplT, call_type, self_ptr, .{arg} });
|
|
}
|
|
}.impl,
|
|
3 => struct {
|
|
fn impl(self_ptr: CurrSelfType, arg1: args[1].arg_type.?, arg2: args[2].arg_type.?) callconv(our_cc) Return {
|
|
return @call(.{ .modifier = .always_inline }, makeCall, .{ name, CurrSelfType, Return, ImplT, call_type, self_ptr, .{ arg1, arg2 } });
|
|
}
|
|
}.impl,
|
|
4 => struct {
|
|
fn impl(self_ptr: CurrSelfType, arg1: args[1].arg_type.?, arg2: args[2].arg_type.?, arg3: args[3].arg_type.?) callconv(our_cc) Return {
|
|
return @call(.{ .modifier = .always_inline }, makeCall, .{ name, CurrSelfType, Return, ImplT, call_type, self_ptr, .{ arg1, arg2, arg3 } });
|
|
}
|
|
}.impl,
|
|
5 => struct {
|
|
fn impl(self_ptr: CurrSelfType, arg1: args[1].arg_type.?, arg2: args[2].arg_type.?, arg3: args[3].arg_type.?, arg4: args[4].arg_type.?) callconv(our_cc) Return {
|
|
return @call(.{ .modifier = .always_inline }, makeCall, .{ name, CurrSelfType, Return, ImplT, call_type, self_ptr, .{ arg1, arg2, arg3, arg4 } });
|
|
}
|
|
}.impl,
|
|
6 => struct {
|
|
fn impl(self_ptr: CurrSelfType, arg1: args[1].arg_type.?, arg2: args[2].arg_type.?, arg3: args[3].arg_type.?, arg4: args[4].arg_type.?, arg5: args[5].arg_type.?) callconv(our_cc) Return {
|
|
return @call(.{ .modifier = .always_inline }, makeCall, .{ name, CurrSelfType, Return, ImplT, call_type, self_ptr, .{ arg1, arg2, arg3, arg4, arg5 } });
|
|
}
|
|
}.impl,
|
|
else => @compileError("Unsupported number of arguments, please provide a manually written vtable."),
|
|
};
|
|
}
|
|
},
|
|
else => return null,
|
|
}
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
fn makeVTable(comptime VTableT: type, comptime ImplT: type) VTableT {
|
|
if (comptime !trait.isContainer(ImplT)) {
|
|
@compileError("Type '" ++ @typeName(ImplT) ++ "' must be a container to implement interface.");
|
|
}
|
|
var vtable: VTableT = undefined;
|
|
|
|
for (std.meta.fields(VTableT)) |field| {
|
|
var fn_type = field.field_type;
|
|
const is_optional = trait.is(.Optional)(fn_type);
|
|
if (is_optional) {
|
|
fn_type = std.meta.Child(fn_type);
|
|
}
|
|
|
|
const candidate = comptime getFunctionFromImpl(field.name, fn_type, ImplT);
|
|
if (candidate == null and !is_optional) {
|
|
@compileError("Type '" ++ @typeName(ImplT) ++ "' does not implement non optional function '" ++ field.name ++ "'.");
|
|
} else if (!is_optional) {
|
|
@field(vtable, field.name) = candidate.?;
|
|
} else {
|
|
@field(vtable, field.name) = candidate;
|
|
}
|
|
}
|
|
|
|
return vtable;
|
|
}
|
|
|
|
fn checkVtableType(comptime VTableT: type) void {
|
|
if (comptime !trait.is(.Struct)(VTableT)) {
|
|
@compileError("VTable type " ++ @typeName(VTableT) ++ " must be a struct.");
|
|
}
|
|
|
|
for (std.meta.declarations(VTableT)) |decl| {
|
|
switch (decl.data) {
|
|
.Fn => @compileError("VTable type defines method '" ++ decl.name ++ "'."),
|
|
.Type, .Var => {},
|
|
}
|
|
}
|
|
|
|
for (std.meta.fields(VTableT)) |field| {
|
|
var field_type = field.field_type;
|
|
|
|
if (trait.is(.Optional)(field_type)) {
|
|
field_type = std.meta.Child(field_type);
|
|
}
|
|
|
|
if (!trait.is(.Fn)(field_type)) {
|
|
@compileError("VTable type defines non function field '" ++ field.name ++ "'.");
|
|
}
|
|
|
|
const type_info = @typeInfo(field_type);
|
|
|
|
if (type_info.Fn.is_generic) {
|
|
@compileError("Virtual function '" ++ field.name ++ "' cannot be generic.");
|
|
}
|
|
|
|
switch (type_info.Fn.calling_convention) {
|
|
.Unspecified, .Async => {},
|
|
else => @compileError("Virtual function's '" ++ field.name ++ "' calling convention is not default or async."),
|
|
}
|
|
}
|
|
}
|
|
|
|
fn vtableHasMethod(comptime VTableT: type, comptime name: []const u8, is_optional: *bool, is_async: *bool, is_method: *bool) bool {
|
|
for (std.meta.fields(VTableT)) |field| {
|
|
if (std.mem.eql(u8, name, field.name)) {
|
|
is_optional.* = trait.is(.Optional)(field.field_type);
|
|
const fn_typeinfo = @typeInfo(if (is_optional.*) std.meta.Child(field.field_type) else field.field_type).Fn;
|
|
is_async.* = fn_typeinfo.calling_convention == .Async;
|
|
is_method.* = fn_typeinfo.args.len > 0 and blk: {
|
|
const first_arg_type = fn_typeinfo.args[0].arg_type.?;
|
|
break :blk first_arg_type == *SelfType or first_arg_type == *const SelfType;
|
|
};
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
fn VTableReturnType(comptime VTableT: type, comptime name: []const u8) type {
|
|
for (std.meta.fields(VTableT)) |field| {
|
|
if (std.mem.eql(u8, name, field.name)) {
|
|
const is_optional = trait.is(.Optional)(field.field_type);
|
|
|
|
var fn_ret_type = (if (is_optional)
|
|
@typeInfo(std.meta.Child(field.field_type)).Fn.return_type
|
|
else
|
|
@typeInfo(field.field_type).Fn.return_type) orelse noreturn;
|
|
|
|
if (is_optional) {
|
|
return ?fn_ret_type;
|
|
}
|
|
|
|
return fn_ret_type;
|
|
}
|
|
}
|
|
|
|
@compileError("VTable type '" ++ @typeName(VTableT) ++ "' has no virtual function '" ++ name ++ "'.");
|
|
}
|
|
|
|
pub fn Interface(comptime VTableT: type, comptime StorageT: type) type {
|
|
comptime checkVtableType(VTableT);
|
|
|
|
const stack_size: usize = if (@hasDecl(VTableT, "async_call_stack_size"))
|
|
VTableT.async_call_stack_size
|
|
else
|
|
1 * 1024 * 1024;
|
|
|
|
return struct {
|
|
vtable_ptr: *const VTableT,
|
|
storage: StorageT,
|
|
|
|
const Self = @This();
|
|
const VTable = VTableT;
|
|
const Storage = StorageT;
|
|
|
|
pub const init = StorageT.makeInit(Self).init;
|
|
|
|
pub fn initWithVTable(vtable_ptr: *const VTableT, args: anytype) !Self {
|
|
return .{
|
|
.vtable_ptr = vtable_ptr,
|
|
.storage = try init(args),
|
|
};
|
|
}
|
|
|
|
pub fn call(self: anytype, comptime name: []const u8, args: anytype) VTableReturnType(VTableT, name) {
|
|
comptime var is_optional = true;
|
|
comptime var is_async = true;
|
|
comptime var is_method = true;
|
|
comptime assert(vtableHasMethod(VTableT, name, &is_optional, &is_async, &is_method));
|
|
|
|
const fn_ptr = if (is_optional) blk: {
|
|
const val = @field(self.vtable_ptr, name);
|
|
if (val) |v| break :blk v;
|
|
return null;
|
|
} else @field(self.vtable_ptr, name);
|
|
|
|
if (is_method) {
|
|
const self_ptr = self.storage.getSelfPtr();
|
|
const new_args = .{self_ptr};
|
|
|
|
if (!is_async) {
|
|
return @call(.{}, fn_ptr, new_args ++ args);
|
|
} else {
|
|
var stack_frame: [stack_size]u8 align(std.Target.stack_align) = undefined;
|
|
return await @asyncCall(&stack_frame, {}, fn_ptr, new_args ++ args);
|
|
}
|
|
} else {
|
|
if (!is_async) {
|
|
return @call(.{}, fn_ptr, args);
|
|
} else {
|
|
var stack_frame: [stack_size]u8 align(std.Target.stack_align) = undefined;
|
|
return await @asyncCall(&stack_frame, {}, fn_ptr, args);
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn deinit(self: Self) void {
|
|
self.storage.deinit();
|
|
}
|
|
};
|
|
}
|