320 lines
12 KiB
Zig
320 lines
12 KiB
Zig
![]() |
const std = @import("std");
|
||
|
const objc = @import("main.zig");
|
||
|
|
||
|
const NSConcreteStackBlock = @extern(*anyopaque, .{
|
||
|
.name = "_NSConcreteStackBlock",
|
||
|
});
|
||
|
|
||
|
extern "C" fn _Block_object_assign(dst: *anyopaque, src: *const anyopaque, flag: c_int) void;
|
||
|
extern "C" fn _Block_object_dispose(src: *const anyopaque, flag: c_int) void;
|
||
|
|
||
|
/// captures is either a struct type or a struct literal
|
||
|
// whose fields will be added to the block
|
||
|
// captures should not be a tuple
|
||
|
// blockFn is either a function type or an actual function
|
||
|
pub fn Block(comptime captures: anytype, comptime blockFn: anytype) type {
|
||
|
const Captures = @TypeOf(captures);
|
||
|
const BlockFn = @TypeOf(blockFn);
|
||
|
const captures_info = @typeInfo(Captures);
|
||
|
const blockfn_info = @typeInfo(BlockFn);
|
||
|
const real_captures_info = switch (captures_info) {
|
||
|
.Type => @typeInfo(captures).Struct,
|
||
|
.Struct => |s| s,
|
||
|
else => @compileError("captures should be a struct type or struct literal"),
|
||
|
};
|
||
|
const real_blockfn_info = switch (blockfn_info) {
|
||
|
.Type => @typeInfo(blockFn).Fn,
|
||
|
.Fn => |f| f,
|
||
|
.Pointer => |p| @typeInfo(p.child).Fn,
|
||
|
else => @compileError("blockFn should be a function type or a function"),
|
||
|
};
|
||
|
const Return = real_blockfn_info.return_type.?;
|
||
|
const params = real_blockfn_info.params;
|
||
|
// an invoke function takes at least one argument: a block.
|
||
|
std.debug.assert(params.len > 0);
|
||
|
// an invoke function's first argument, a block, must be *anyopaque.
|
||
|
std.debug.assert(params[0].type == *anyopaque);
|
||
|
const fields: []std.builtin.Type.StructField = fields: {
|
||
|
var acc: [real_captures_info.fields.len + 5]std.builtin.Type.StructField = undefined;
|
||
|
acc[0] = .{
|
||
|
.name = "isa",
|
||
|
.type = ?*anyopaque,
|
||
|
.default_value = null,
|
||
|
.is_comptime = false,
|
||
|
.alignment = @alignOf(*anyopaque),
|
||
|
};
|
||
|
acc[1] = .{
|
||
|
.name = "flags",
|
||
|
.type = c_int,
|
||
|
.default_value = null,
|
||
|
.is_comptime = false,
|
||
|
.alignment = @alignOf(c_int),
|
||
|
};
|
||
|
acc[2] = .{
|
||
|
.name = "reserved",
|
||
|
.type = c_int,
|
||
|
.default_value = null,
|
||
|
.is_comptime = false,
|
||
|
.alignment = @alignOf(c_int),
|
||
|
};
|
||
|
acc[3] = .{
|
||
|
.name = "invoke",
|
||
|
.type = *const @Type(.{
|
||
|
.Fn = .{
|
||
|
.calling_convention = .C,
|
||
|
.alignment = @typeInfo(fn () callconv(.C) void).Fn.alignment,
|
||
|
.is_generic = false,
|
||
|
.is_var_args = false,
|
||
|
.return_type = Return,
|
||
|
.params = params,
|
||
|
},
|
||
|
}),
|
||
|
.default_value = null,
|
||
|
.is_comptime = false,
|
||
|
.alignment = @typeInfo(*const fn () callconv(.C) void).Pointer.alignment,
|
||
|
};
|
||
|
acc[4] = .{
|
||
|
.name = "descriptor",
|
||
|
.type = *Descriptor,
|
||
|
.default_value = null,
|
||
|
.is_comptime = false,
|
||
|
.alignment = @alignOf(*Descriptor),
|
||
|
};
|
||
|
std.debug.assert(!real_captures_info.is_tuple);
|
||
|
for (real_captures_info.fields, 0..) |capture, i| {
|
||
|
switch (capture.type) {
|
||
|
comptime_int => @compileError("capture should not be a comptime_int! try using @as"),
|
||
|
comptime_float => @compileError("capture should not be a comptime_float! try using @as"),
|
||
|
else => {},
|
||
|
}
|
||
|
acc[5 + i] = .{
|
||
|
.name = capture.name,
|
||
|
.type = capture.type,
|
||
|
.default_value = null,
|
||
|
.is_comptime = false,
|
||
|
.alignment = capture.alignment,
|
||
|
};
|
||
|
}
|
||
|
break :fields &acc;
|
||
|
};
|
||
|
return @Type(.{
|
||
|
.Struct = .{
|
||
|
.layout = .Extern,
|
||
|
.fields = fields,
|
||
|
.decls = &.{},
|
||
|
.is_tuple = false,
|
||
|
},
|
||
|
});
|
||
|
}
|
||
|
|
||
|
/// creates a block (on the heap, using malloc) of type T,
|
||
|
// which should be the output of Block,
|
||
|
// assigns the given captures
|
||
|
// (which should be a struct literal with correct names)
|
||
|
// and assigns the given function
|
||
|
// (which should have the correct signature and C calling convention)
|
||
|
// to the block.
|
||
|
// Objective-C will free block pointers itself,
|
||
|
// otherwise you can call free on it yourself.
|
||
|
pub fn initBlock(comptime T: type, captures: anytype, blockFn: anytype) *T {
|
||
|
const allocator = std.heap.raw_c_allocator;
|
||
|
var ret = allocator.create(T) catch @panic("OOM!");
|
||
|
const captures_info = @typeInfo(@TypeOf(captures)).Struct;
|
||
|
var fn_is_pointer = false;
|
||
|
const fn_info = switch (@typeInfo(@TypeOf(blockFn))) {
|
||
|
.Fn => |f| f,
|
||
|
.Pointer => |p| blk: {
|
||
|
fn_is_pointer = true;
|
||
|
break :blk @typeInfo(p.child).Fn;
|
||
|
},
|
||
|
else => @compileError("blockFn should be a function!"),
|
||
|
};
|
||
|
std.debug.assert(fn_info.calling_convention == .C);
|
||
|
const Return = fn_info.return_type.?;
|
||
|
const ret_type = @typeInfo(Return);
|
||
|
const flags: BlockFlags = .{
|
||
|
.stret = ret_type == .Struct,
|
||
|
};
|
||
|
@field(ret, "isa") = NSConcreteStackBlock;
|
||
|
@field(ret, "flags") = @as(c_int, @bitCast(flags));
|
||
|
@field(ret, "reserved") = undefined;
|
||
|
@field(ret, "invoke") = if (fn_is_pointer) blockFn else &blockFn;
|
||
|
const inner = struct {
|
||
|
fn copy_helper(src: *anyopaque, dst: *anyopaque) callconv(.C) void {
|
||
|
var real_src: *T = @ptrCast(@alignCast(src));
|
||
|
var real_dst: *T = @ptrCast(@alignCast(dst));
|
||
|
inline for (captures_info.fields) |field| {
|
||
|
if (field.type == objc.c.id) {
|
||
|
var dst_field = @field(real_dst, field.name);
|
||
|
const src_field = @field(real_src, field.name);
|
||
|
_Block_object_assign(dst_field, src_field, 3);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
fn dispose_helper(src: *anyopaque) callconv(.C) void {
|
||
|
const real_src: *T = @ptrCast(@alignCast(src));
|
||
|
inline for (captures_info.fields) |field| {
|
||
|
if (field.type == objc.c.id) {
|
||
|
_Block_object_dispose(@field(real_src, field.name), 3);
|
||
|
}
|
||
|
std.heap.raw_c_allocator.free(std.mem.sliceTo(@field(@field(real_src, "descriptor"), "signature").?, 0));
|
||
|
std.heap.raw_c_allocator.destroy(@field(real_src, "descriptor"));
|
||
|
}
|
||
|
}
|
||
|
};
|
||
|
const signature = encodeFn(Return, fn_info.params) catch @panic("OOM!");
|
||
|
var descriptor = allocator.create(Descriptor) catch @panic("OOM!");
|
||
|
descriptor.* = .{
|
||
|
.reserved = 0,
|
||
|
.size = @sizeOf(T),
|
||
|
.copy_helper = inner.copy_helper,
|
||
|
.dispose_helper = inner.dispose_helper,
|
||
|
.signature = signature.ptr,
|
||
|
};
|
||
|
@field(ret, "descriptor") = descriptor;
|
||
|
inline for (captures_info.fields) |field| {
|
||
|
@field(ret, field.name) = @field(captures, field.name);
|
||
|
}
|
||
|
return ret;
|
||
|
}
|
||
|
|
||
|
/// contents must be freed with 'free'
|
||
|
pub fn encodeFn(
|
||
|
comptime Return: type,
|
||
|
comptime Args: []const std.builtin.Type.Fn.Param,
|
||
|
) ![:0]const u8 {
|
||
|
var allocator = std.heap.raw_c_allocator;
|
||
|
const String = std.ArrayList(u8);
|
||
|
var list = try String.initCapacity(allocator, 1024);
|
||
|
defer list.deinit();
|
||
|
const str = try encode_inner(Return);
|
||
|
try list.appendSlice(str);
|
||
|
allocator.free(str);
|
||
|
inline for (Args) |arg| {
|
||
|
const arg_str = try encode_inner(arg.type.?);
|
||
|
defer allocator.free(arg_str);
|
||
|
try list.appendSlice(arg_str);
|
||
|
}
|
||
|
return allocator.dupeZ(u8, list.items);
|
||
|
}
|
||
|
|
||
|
fn encode_inner(comptime T: type) ![]const u8 {
|
||
|
var allocator = std.heap.raw_c_allocator;
|
||
|
const String = std.ArrayList(u8);
|
||
|
var list = try String.initCapacity(allocator, 128);
|
||
|
defer list.deinit();
|
||
|
switch (T) {
|
||
|
objc.Object, objc.c.id => try list.append('@'),
|
||
|
objc.Class, objc.c.Class => try list.append('#'),
|
||
|
objc.Sel, objc.c.SEL => try list.append(':'),
|
||
|
bool => try list.append('B'),
|
||
|
u8 => try list.append('C'),
|
||
|
i8 => try list.append('c'),
|
||
|
u16 => try list.append('S'),
|
||
|
i16 => try list.append('s'),
|
||
|
u32, c_uint => try list.append('I'),
|
||
|
i32, c_int => try list.append('i'),
|
||
|
u64, c_ulong, c_ulonglong => try list.append('Q'),
|
||
|
i64, c_long, c_longlong => try list.append('q'),
|
||
|
f32 => try list.append('f'),
|
||
|
f64 => try list.append('d'),
|
||
|
[:0]const u8, [*c]const u8, [:0]u8, [*c]u8 => try list.append('*'),
|
||
|
void => try list.append('v'),
|
||
|
else => {
|
||
|
const type_info = @typeInfo(T);
|
||
|
switch (type_info) {
|
||
|
.Struct => |s| {
|
||
|
try list.appendSlice("{?=");
|
||
|
for (s.fields) |field| {
|
||
|
const str = try encode_inner(field.type);
|
||
|
defer allocator.free(str);
|
||
|
try list.appendSlice(str);
|
||
|
}
|
||
|
try list.appendSlice("}");
|
||
|
},
|
||
|
.Union => |u| {
|
||
|
try list.appendSlice("(?=");
|
||
|
for (u.fields) |field| {
|
||
|
const str = try encode_inner(field.type);
|
||
|
defer allocator.free(str);
|
||
|
try list.appendSlice(str);
|
||
|
}
|
||
|
try list.appendSlice(")");
|
||
|
},
|
||
|
.Pointer => |p| {
|
||
|
try list.append('^');
|
||
|
const str = try encode_inner(p.child);
|
||
|
defer allocator.free(str);
|
||
|
try list.appendSlice(str);
|
||
|
},
|
||
|
.Fn => try list.append('?'),
|
||
|
.Opaque => try list.append('v'),
|
||
|
else => @compileError("unsupported type for encode(): " ++ @typeName(T)),
|
||
|
}
|
||
|
},
|
||
|
}
|
||
|
return allocator.dupe(u8, list.items);
|
||
|
}
|
||
|
|
||
|
/// contents must be freed with 'free'
|
||
|
pub fn encode(comptime T: type) ![:0]const u8 {
|
||
|
const allocator = std.heap.raw_c_allocator;
|
||
|
const str = try encode_inner(T);
|
||
|
defer allocator.free(str);
|
||
|
return allocator.dupeZ(u8, str);
|
||
|
}
|
||
|
|
||
|
const Descriptor = extern struct {
|
||
|
reserved: c_ulong = 0,
|
||
|
size: c_ulong,
|
||
|
copy_helper: *const fn (dst: *anyopaque, src: *anyopaque) callconv(.C) void,
|
||
|
dispose_helper: *const fn (src: *anyopaque) callconv(.C) void,
|
||
|
signature: ?[*:0]const u8,
|
||
|
};
|
||
|
|
||
|
const BlockFlags = packed struct {
|
||
|
_unused: u22 = 0,
|
||
|
noescape: bool = false,
|
||
|
_unused_2: bool = false,
|
||
|
copy_dispose: bool = true,
|
||
|
ctor: bool = false,
|
||
|
_unused_3: bool = false,
|
||
|
global: bool = false,
|
||
|
stret: bool,
|
||
|
signature: bool = true,
|
||
|
_unused_4: u2 = 0,
|
||
|
|
||
|
comptime {
|
||
|
std.debug.assert(@sizeOf(@This()) == @sizeOf(c_int));
|
||
|
std.debug.assert(@bitSizeOf(@This()) == @bitSizeOf(c_int));
|
||
|
}
|
||
|
};
|
||
|
|
||
|
pub fn invokeBlock(comptime T: type, comptime Return: type, block: *T, args: anytype) Return {
|
||
|
var invoke = @field(block, "invoke");
|
||
|
const ret: Return = @call(.auto, invoke, .{block} ++ args);
|
||
|
return ret;
|
||
|
}
|
||
|
|
||
|
test "Block and invokeBlock" {
|
||
|
const Captures = struct {
|
||
|
x: i32,
|
||
|
y: i32,
|
||
|
};
|
||
|
const AddBlock = Block(Captures, fn (block: *anyopaque) i32);
|
||
|
const captures = .{
|
||
|
.x = 2,
|
||
|
.y = 3,
|
||
|
};
|
||
|
const inner = struct {
|
||
|
fn addFn(block: *anyopaque) callconv(.C) i32 {
|
||
|
const realBlock: *AddBlock = @ptrCast(@alignCast(block));
|
||
|
return realBlock.x + realBlock.y;
|
||
|
}
|
||
|
};
|
||
|
var block = initBlock(AddBlock, captures, inner.addFn);
|
||
|
defer std.heap.raw_c_allocator.destroy(block);
|
||
|
const ret = invokeBlock(AddBlock, i32, block, .{});
|
||
|
try std.testing.expectEqual(@as(i32, 5), ret);
|
||
|
}
|