pkg/objc: extract to zig-objc repo

This commit is contained in:
Mitchell Hashimoto
2023-01-02 21:35:32 -08:00
parent f1e62cfba4
commit e9bf2a5680
12 changed files with 5 additions and 435 deletions

3
.gitmodules vendored
View File

@ -37,3 +37,6 @@
[submodule "vendor/zig-js"]
path = vendor/zig-js
url = https://github.com/mitchellh/zig-js.git
[submodule "vendor/zig-objc"]
path = vendor/zig-objc
url = https://github.com/mitchellh/zig-objc.git

View File

@ -12,7 +12,7 @@ const libxml2 = @import("vendor/zig-libxml2/libxml2.zig");
const libuv = @import("pkg/libuv/build.zig");
const libpng = @import("pkg/libpng/build.zig");
const macos = @import("pkg/macos/build.zig");
const objc = @import("pkg/objc/build.zig");
const objc = @import("vendor/zig-objc/build.zig");
const pixman = @import("pkg/pixman/build.zig");
const stb_image_resize = @import("pkg/stb_image_resize/build.zig");
const utf8proc = @import("pkg/utf8proc/build.zig");

View File

@ -1,16 +0,0 @@
const std = @import("std");
pub const AutoreleasePool = opaque {
pub inline fn init() *AutoreleasePool {
return @ptrCast(*AutoreleasePool, objc_autoreleasePoolPush().?);
}
pub inline fn deinit(self: *AutoreleasePool) void {
objc_autoreleasePoolPop(self);
}
};
// I'm not sure if these are internal or not... they aren't in any headers,
// but its how autorelease pools are implemented.
extern "c" fn objc_autoreleasePoolPush() ?*anyopaque;
extern "c" fn objc_autoreleasePoolPop(?*anyopaque) void;

View File

@ -1,10 +0,0 @@
const std = @import("std");
pub const pkg = std.build.Pkg{
.name = "objc",
.source = .{ .path = thisDir() ++ "/main.zig" },
};
fn thisDir() []const u8 {
return std.fs.path.dirname(@src().file) orelse ".";
}

View File

@ -1,4 +0,0 @@
pub usingnamespace @cImport({
@cInclude("objc/runtime.h");
@cInclude("objc/message.h");
});

View File

@ -1,74 +0,0 @@
const std = @import("std");
const c = @import("c.zig");
const objc = @import("main.zig");
const MsgSend = @import("msg_send.zig").MsgSend;
pub const Class = struct {
value: c.Class,
pub usingnamespace MsgSend(Class);
/// Returns the class definition of a specified class.
pub fn getClass(name: [:0]const u8) ?Class {
return Class{
.value = c.objc_getClass(name.ptr) orelse return null,
};
}
/// Returns a property with a given name of a given class.
pub fn getProperty(self: Class, name: [:0]const u8) ?objc.Property {
return objc.Property{
.value = c.class_getProperty(self.value, name.ptr) orelse return null,
};
}
/// Describes the properties declared by a class. This must be freed.
pub fn copyPropertyList(self: Class) []objc.Property {
var count: c_uint = undefined;
const list = @ptrCast([*c]objc.Property, c.class_copyPropertyList(self.value, &count));
if (count == 0) return list[0..0];
return list[0..count];
}
};
test "getClass" {
const testing = std.testing;
const NSObject = Class.getClass("NSObject");
try testing.expect(NSObject != null);
try testing.expect(Class.getClass("NoWay") == null);
}
test "msgSend" {
const testing = std.testing;
const NSObject = Class.getClass("NSObject").?;
// Should work with primitives
const id = NSObject.msgSend(c.id, objc.Sel.registerName("alloc"), .{});
try testing.expect(id != null);
{
const obj: objc.Object = .{ .value = id };
obj.msgSend(void, objc.sel("dealloc"), .{});
}
// Should work with our wrappers
const obj = NSObject.msgSend(objc.Object, objc.Sel.registerName("alloc"), .{});
try testing.expect(obj.value != null);
obj.msgSend(void, objc.sel("dealloc"), .{});
}
test "getProperty" {
const testing = std.testing;
const NSObject = Class.getClass("NSObject").?;
try testing.expect(NSObject.getProperty("className") != null);
try testing.expect(NSObject.getProperty("nope") == null);
}
test "copyProperyList" {
const testing = std.testing;
const NSObject = Class.getClass("NSObject").?;
const list = NSObject.copyPropertyList();
defer objc.free(list);
try testing.expect(list.len > 20);
}

View File

@ -1,18 +0,0 @@
const std = @import("std");
pub const c = @import("c.zig");
pub usingnamespace @import("autorelease.zig");
pub usingnamespace @import("class.zig");
pub usingnamespace @import("object.zig");
pub usingnamespace @import("property.zig");
pub usingnamespace @import("sel.zig");
/// This just calls the C allocator free. Some things need to be freed
/// and this is how they can be freed for objc.
pub inline fn free(ptr: anytype) void {
std.heap.c_allocator.free(ptr);
}
test {
std.testing.refAllDecls(@This());
}

View File

@ -1,177 +0,0 @@
const std = @import("std");
const builtin = @import("builtin");
const assert = std.debug.assert;
const c = @import("c.zig");
const objc = @import("main.zig");
/// Returns a struct that implements the msgSend function for type T.
/// This is meant to be used with `usingnamespace` to add dispatch
/// capability to a type that supports it.
pub fn MsgSend(comptime T: type) type {
// 1. T should be a struct
// 2. T should have a field "value" that can be an "id" (same size)
return struct {
/// Invoke a selector on the target, i.e. an instance method on an
/// object or a class method on a class. The args should be a tuple.
pub fn msgSend(
target: T,
comptime Return: type,
sel: objc.Sel,
args: anytype,
) Return {
// Our one special-case: If the return type is our own Object
// type then we wrap it.
const is_object = Return == objc.Object;
// Our actual return value is an "id" if we are using one of
// our built-in types (see above). Otherwise, we trust the caller.
const RealReturn = if (is_object) c.id else Return;
// See objc/message.h. The high-level is that depending on the
// target architecture and return type, we must use a different
// objc_msgSend function.
const msg_send_fn = switch (builtin.target.cpu.arch) {
// Aarch64 uses objc_msgSend for everything. Hurray!
.aarch64 => &c.objc_msgSend,
// x86_64 depends on the return type...
.x86_64 => switch (@typeInfo(RealReturn)) {
// Most types use objc_msgSend
inline .Int, .Bool, .Pointer, .Void => &c.objc_msgSend,
.Optional => |opt| opt: {
assert(@typeInfo(opt.child) == .Pointer);
break :opt &c.objc_msgSend;
},
// Structs must use objc_msgSend_stret.
// NOTE: This is probably WAY more complicated... we only
// call this if the struct is NOT returned as a register.
// And that depends on the size of the struct. But I don't
// know what the breakpoint actually is for that. This SO
// answer says 16 bytes so I'm going to use that but I have
// no idea...
.Struct => if (@sizeOf(Return) > 16)
&c.objc_msgSend_stret
else
&c.objc_msgSend,
// Floats use objc_msgSend_fpret for f64 on x86_64,
// but normal msgSend for other bit sizes. i386 has
// more complex rules but we don't support i386 at the time
// of this comment and probably never will since all i386
// Apple models are discontinued at this point.
.Float => |float| switch (float.bits) {
64 => &c.objc_msgSend_fpret,
else => &c.objc_msgSend,
},
// Otherwise we log in case we need to add a new case above
else => {
@compileLog(@typeInfo(RealReturn));
@compileError("unsupported return type for objc runtime on x86_64");
},
},
else => @compileError("unsupported objc architecture"),
};
// Build our function type and call it
const Fn = MsgSendFn(RealReturn, @TypeOf(target.value), @TypeOf(args));
// Due to this stage2 Zig issue[1], this must be var for now.
// [1]: https://github.com/ziglang/zig/issues/13598
var msg_send_ptr = @ptrCast(*const Fn, msg_send_fn);
const result = @call(.auto, msg_send_ptr, .{ target.value, sel.value } ++ args);
if (!is_object) return result;
return .{ .value = result };
}
};
}
/// This returns a function body type for `obj_msgSend` that matches
/// the given return type, target type, and arguments tuple type.
///
/// obj_msgSend is a really interesting function, because it doesn't act
/// like a typical function. You have to call it with the C ABI as if you're
/// calling the true target function, not as a varargs C function. Therefore
/// you have to cast obj_msgSend to a function pointer type of the final
/// destination function, then call that.
///
/// Example: you have an ObjC function like this:
///
/// @implementation Foo
/// - (void)log: (float)x { /* stuff */ }
///
/// If you call it like this, it won't work (you'll get garbage):
///
/// objc_msgSend(obj, @selector(log:), (float)PI);
///
/// You have to call it like this:
///
/// ((void (*)(id, SEL, float))objc_msgSend)(obj, @selector(log:), M_PI);
///
/// This comptime function returns the function body type that can be used
/// to cast and call for the proper C ABI behavior.
fn MsgSendFn(
comptime Return: type,
comptime Target: type,
comptime Args: type,
) type {
const argsInfo = @typeInfo(Args).Struct;
assert(argsInfo.is_tuple);
// Target must always be an "id". Lots of types (Class, Object, etc.)
// are an "id" so we just make sure the sizes match for ABI reasons.
assert(@sizeOf(Target) == @sizeOf(c.id));
// Build up our argument types.
const Fn = std.builtin.Type.Fn;
const params: []Fn.Param = params: {
var acc: [argsInfo.fields.len + 2]Fn.Param = undefined;
// First argument is always the target and selector.
acc[0] = .{ .type = Target, .is_generic = false, .is_noalias = false };
acc[1] = .{ .type = c.SEL, .is_generic = false, .is_noalias = false };
// Remaining arguments depend on the args given, in the order given
for (argsInfo.fields) |field, i| {
acc[i + 2] = .{
.type = field.type,
.is_generic = false,
.is_noalias = false,
};
}
break :params &acc;
};
// Copy the alignment of a normal function type so equality works
// (mainly for tests, I don't think this has any consequence otherwise)
const alignment = @typeInfo(fn () callconv(.C) void).Fn.alignment;
return @Type(.{
.Fn = .{
.calling_convention = .C,
.alignment = alignment,
.is_generic = false,
.is_var_args = false,
.return_type = Return,
.params = params,
},
});
}
test {
// https://github.com/ziglang/zig/issues/12360
if (true) return error.SkipZigTest;
const testing = std.testing;
try testing.expectEqual(fn (
u8,
objc.Sel,
) callconv(.C) u64, MsgSendFn(u64, u8, @TypeOf(.{})));
try testing.expectEqual(fn (u8, objc.Sel, u16, u32) callconv(.C) u64, MsgSendFn(u64, u8, @TypeOf(.{
@as(u16, 0),
@as(u32, 0),
})));
}

View File

@ -1,70 +0,0 @@
const std = @import("std");
const c = @import("c.zig");
const objc = @import("main.zig");
const MsgSend = @import("msg_send.zig").MsgSend;
pub const Object = struct {
value: c.id,
pub usingnamespace MsgSend(Object);
pub fn fromId(id: anytype) Object {
return .{ .value = @ptrCast(c.id, @alignCast(@alignOf(c.id), id)) };
}
/// Returns the class of an object.
pub fn getClass(self: Object) ?objc.Class {
return objc.Class{
.value = c.object_getClass(self.value) orelse return null,
};
}
/// Returns the class name of a given object.
pub fn getClassName(self: Object) [:0]const u8 {
return std.mem.sliceTo(c.object_getClassName(self.value), 0);
}
/// Set a property. This is a helper around getProperty and is
/// strictly less performant than doing it manually. Consider doing
/// this manually if performance is critical.
pub fn setProperty(self: Object, comptime n: [:0]const u8, v: anytype) void {
const Class = self.getClass().?;
const prop = Class.getProperty(n).?;
const setter = if (prop.copyAttributeValue("S")) |val| setter: {
defer objc.free(val);
break :setter objc.sel(val);
} else objc.sel(
"set" ++
[1]u8{std.ascii.toUpper(n[0])} ++
n[1..n.len] ++
":",
);
self.msgSend(void, setter, .{v});
}
/// Get a property. This is a helper around Class.getProperty and is
/// strictly less performant than doing it manually. Consider doing
/// this manually if performance is critical.
pub fn getProperty(self: Object, comptime T: type, comptime n: [:0]const u8) T {
const Class = self.getClass().?;
const prop = Class.getProperty(n).?;
const getter = if (prop.copyAttributeValue("G")) |val| getter: {
defer objc.free(val);
break :getter objc.sel(val);
} else objc.sel(n);
return self.msgSend(T, getter, .{});
}
};
test {
const testing = std.testing;
const NSObject = objc.Class.getClass("NSObject").?;
// Should work with our wrappers
const obj = NSObject.msgSend(objc.Object, objc.Sel.registerName("alloc"), .{});
try testing.expect(obj.value != null);
try testing.expectEqualStrings("NSObject", obj.getClassName());
obj.msgSend(void, objc.sel("dealloc"), .{});
}

View File

@ -1,35 +0,0 @@
const std = @import("std");
const c = @import("c.zig");
const objc = @import("main.zig");
pub const Property = extern struct {
value: c.objc_property_t,
/// Returns the name of a property.
pub fn getName(self: Property) [:0]const u8 {
return std.mem.sliceTo(c.property_getName(self.value), 0);
}
/// Returns the value of a property attribute given the attribute name.
pub fn copyAttributeValue(self: Property, attr: [:0]const u8) ?[:0]u8 {
return std.mem.sliceTo(
c.property_copyAttributeValue(self.value, attr.ptr) orelse return null,
0,
);
}
};
test {
// Critical properties because we ptrCast C pointers to this.
const testing = std.testing;
try testing.expect(@sizeOf(Property) == @sizeOf(c.objc_property_t));
try testing.expect(@alignOf(Property) == @alignOf(c.objc_property_t));
}
test {
const testing = std.testing;
const NSObject = objc.Class.getClass("NSObject").?;
const prop = NSObject.getProperty("className").?;
try testing.expectEqualStrings("className", prop.getName());
}

View File

@ -1,30 +0,0 @@
const std = @import("std");
const c = @import("c.zig");
// Shorthand, equivalent to Sel.registerName
pub inline fn sel(name: [:0]const u8) Sel {
return Sel.registerName(name);
}
pub const Sel = struct {
value: c.SEL,
/// Registers a method with the Objective-C runtime system, maps the
/// method name to a selector, and returns the selector value.
pub fn registerName(name: [:0]const u8) Sel {
return Sel{
.value = c.sel_registerName(name.ptr),
};
}
/// Returns the name of the method specified by a given selector.
pub fn getName(self: Sel) [:0]const u8 {
return std.mem.sliceTo(c.sel_getName(self.value), 0);
}
};
test {
const testing = std.testing;
const s = Sel.registerName("yo");
try testing.expectEqualStrings("yo", s.getName());
}

1
vendor/zig-objc vendored Submodule

@ -0,0 +1 @@
Subproject commit 4f5eda096c327a6943c834762cead4d1c9db2366