From dd8fde52d9d62f322df8cb12a0e1adc361baf506 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 25 Oct 2022 16:21:11 -0700 Subject: [PATCH 1/6] pkg/objc starting --- build.zig | 2 ++ pkg/objc/build.zig | 10 ++++++++++ pkg/objc/c.zig | 4 ++++ pkg/objc/class.zig | 20 ++++++++++++++++++++ pkg/objc/main.zig | 6 ++++++ 5 files changed, 42 insertions(+) create mode 100644 pkg/objc/build.zig create mode 100644 pkg/objc/c.zig create mode 100644 pkg/objc/class.zig create mode 100644 pkg/objc/main.zig diff --git a/build.zig b/build.zig index efeeaa189..8bade4c10 100644 --- a/build.zig +++ b/build.zig @@ -11,6 +11,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 stb_image_resize = @import("pkg/stb_image_resize/build.zig"); const utf8proc = @import("pkg/utf8proc/build.zig"); const zlib = @import("pkg/zlib/build.zig"); @@ -194,6 +195,7 @@ fn addDeps( step.addPackage(imgui.pkg); step.addPackage(glfw.pkg); step.addPackage(libuv.pkg); + step.addPackage(objc.pkg); step.addPackage(stb_image_resize.pkg); step.addPackage(utf8proc.pkg); diff --git a/pkg/objc/build.zig b/pkg/objc/build.zig new file mode 100644 index 000000000..a29d90daf --- /dev/null +++ b/pkg/objc/build.zig @@ -0,0 +1,10 @@ +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 "."; +} diff --git a/pkg/objc/c.zig b/pkg/objc/c.zig new file mode 100644 index 000000000..622ea4bce --- /dev/null +++ b/pkg/objc/c.zig @@ -0,0 +1,4 @@ +pub usingnamespace @cImport({ + @cInclude("objc/runtime.h"); + @cInclude("objc/message.h"); +}); diff --git a/pkg/objc/class.zig b/pkg/objc/class.zig new file mode 100644 index 000000000..2f6d66920 --- /dev/null +++ b/pkg/objc/class.zig @@ -0,0 +1,20 @@ +const std = @import("std"); +const c = @import("c.zig"); + +pub const Class = struct { + value: c.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, + }; + } +}; + +test { + const testing = std.testing; + const NSObject = Class.getClass("NSObject"); + try testing.expect(NSObject != null); + try testing.expect(Class.getClass("NoWay") == null); +} diff --git a/pkg/objc/main.zig b/pkg/objc/main.zig new file mode 100644 index 000000000..d83e205b2 --- /dev/null +++ b/pkg/objc/main.zig @@ -0,0 +1,6 @@ +pub const c = @import("c.zig"); +pub usingnamespace @import("class.zig"); + +test { + @import("std").testing.refAllDecls(@This()); +} From 10ee05b435319dd1a1d29bf1c98104f026ff1ae7 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 25 Oct 2022 16:33:45 -0700 Subject: [PATCH 2/6] pkg/objc: selectors --- build.zig | 2 +- pkg/objc/main.zig | 1 + pkg/objc/sel.zig | 25 +++++++++++++++++++++++++ 3 files changed, 27 insertions(+), 1 deletion(-) create mode 100644 pkg/objc/sel.zig diff --git a/build.zig b/build.zig index 8bade4c10..882b04a79 100644 --- a/build.zig +++ b/build.zig @@ -195,12 +195,12 @@ fn addDeps( step.addPackage(imgui.pkg); step.addPackage(glfw.pkg); step.addPackage(libuv.pkg); - step.addPackage(objc.pkg); step.addPackage(stb_image_resize.pkg); step.addPackage(utf8proc.pkg); // Mac Stuff if (step.target.isDarwin()) { + step.addPackage(objc.pkg); step.addPackage(macos.pkg); _ = try macos.link(b, step, .{}); } diff --git a/pkg/objc/main.zig b/pkg/objc/main.zig index d83e205b2..1a74413a5 100644 --- a/pkg/objc/main.zig +++ b/pkg/objc/main.zig @@ -1,5 +1,6 @@ pub const c = @import("c.zig"); pub usingnamespace @import("class.zig"); +pub usingnamespace @import("sel.zig"); test { @import("std").testing.refAllDecls(@This()); diff --git a/pkg/objc/sel.zig b/pkg/objc/sel.zig new file mode 100644 index 000000000..4c4c62f7a --- /dev/null +++ b/pkg/objc/sel.zig @@ -0,0 +1,25 @@ +const std = @import("std"); +const c = @import("c.zig"); + +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 sel = Sel.registerName("yo"); + try testing.expectEqualStrings("yo", sel.getName()); +} From 7d48e564b5b44561239d89cc19f3d14ec246d0f8 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 25 Oct 2022 20:30:42 -0700 Subject: [PATCH 3/6] pkg/objc: message send --- pkg/objc/class.zig | 13 ++++- pkg/objc/main.zig | 3 ++ pkg/objc/msg_send.zig | 115 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 130 insertions(+), 1 deletion(-) create mode 100644 pkg/objc/msg_send.zig diff --git a/pkg/objc/class.zig b/pkg/objc/class.zig index 2f6d66920..95dddb5af 100644 --- a/pkg/objc/class.zig +++ b/pkg/objc/class.zig @@ -1,9 +1,13 @@ 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{ @@ -12,9 +16,16 @@ pub const Class = struct { } }; -test { +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").?; + const id = NSObject.msgSend(c.id, objc.Sel.registerName("alloc"), .{}); + try testing.expect(id != null); +} diff --git a/pkg/objc/main.zig b/pkg/objc/main.zig index 1a74413a5..6c6270d4c 100644 --- a/pkg/objc/main.zig +++ b/pkg/objc/main.zig @@ -4,4 +4,7 @@ pub usingnamespace @import("sel.zig"); test { @import("std").testing.refAllDecls(@This()); + + // TODO: remove once we integrate this + _ = @import("msg_send.zig"); } diff --git a/pkg/objc/msg_send.zig b/pkg/objc/msg_send.zig new file mode 100644 index 000000000..6f276b593 --- /dev/null +++ b/pkg/objc/msg_send.zig @@ -0,0 +1,115 @@ +const std = @import("std"); +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 { + const Fn = MsgSendFn(Return, @TypeOf(target.value), @TypeOf(args)); + const msg_send_ptr = @ptrCast(std.meta.FnPtr(Fn), &c.objc_msgSend); + return @call(.{}, msg_send_ptr, .{ target.value, sel } ++ args); + } + }; +} + +/// 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 args: []Fn.Param = args: { + var acc: [argsInfo.fields.len + 2]Fn.Param = undefined; + + // First argument is always the target and selector. + acc[0] = .{ .arg_type = Target, .is_generic = false, .is_noalias = false }; + acc[1] = .{ .arg_type = objc.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] = .{ + .arg_type = field.field_type, + .is_generic = false, + .is_noalias = false, + }; + } + + break :args &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, + .args = args, + }, + }); +} + +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), + }))); +} From f587b222e775f948f9bd349c71d5c825c38b1b01 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 25 Oct 2022 20:55:34 -0700 Subject: [PATCH 4/6] don't need fstage1 --- README.md | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/README.md b/README.md index 75a5a3a56..ab52a8ab6 100644 --- a/README.md +++ b/README.md @@ -75,16 +75,12 @@ on the [Zig downloads page](https://ziglang.org/download/). With Zig installed, a binary can be built using `zig build`: ```shell-session -$ zig build -fstage1 +$ zig build ... $ zig-out/bin/ghostty ``` -**Important: you must specify the `-fstage1` flag.** Ghostty can't yet be -built with the self-hosted Zig backend, so we have to use "stage1" (the -C++ LLVM backend). - This will build a binary for the currently running system (if supported). You can cross compile by setting `-Dtarget=`. For example, `zig build -Dtarget=aarch64-macos` will build for Apple Silicon macOS. Note From aaaae38fa1e3399a1b9bf0468edaf6d5e3bd9edc Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 25 Oct 2022 20:55:41 -0700 Subject: [PATCH 5/6] pkg/objc: more message send stuff --- pkg/objc/class.zig | 11 +++++++++++ pkg/objc/main.zig | 1 + pkg/objc/msg_send.zig | 19 ++++++++++++++++++- pkg/objc/object.zig | 10 ++++++++++ pkg/objc/sel.zig | 9 +++++++-- 5 files changed, 47 insertions(+), 3 deletions(-) create mode 100644 pkg/objc/object.zig diff --git a/pkg/objc/class.zig b/pkg/objc/class.zig index 95dddb5af..f655c82eb 100644 --- a/pkg/objc/class.zig +++ b/pkg/objc/class.zig @@ -26,6 +26,17 @@ test "getClass" { 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"), .{}); } diff --git a/pkg/objc/main.zig b/pkg/objc/main.zig index 6c6270d4c..6eefc51fe 100644 --- a/pkg/objc/main.zig +++ b/pkg/objc/main.zig @@ -1,5 +1,6 @@ pub const c = @import("c.zig"); pub usingnamespace @import("class.zig"); +pub usingnamespace @import("object.zig"); pub usingnamespace @import("sel.zig"); test { diff --git a/pkg/objc/msg_send.zig b/pkg/objc/msg_send.zig index 6f276b593..4f2eaf699 100644 --- a/pkg/objc/msg_send.zig +++ b/pkg/objc/msg_send.zig @@ -19,9 +19,26 @@ pub fn MsgSend(comptime T: type) type { sel: objc.Sel, args: anytype, ) Return { + // Build our function type and call it const Fn = MsgSendFn(Return, @TypeOf(target.value), @TypeOf(args)); const msg_send_ptr = @ptrCast(std.meta.FnPtr(Fn), &c.objc_msgSend); - return @call(.{}, msg_send_ptr, .{ target.value, sel } ++ args); + const result = @call(.{}, msg_send_ptr, .{ target.value, sel } ++ args); + + // This is a special nicety: if the return type is one of our + // public structs then we wrap the msgSend id result with it. + // This lets msgSend magically work with Object and so on. + const is_pkg_struct = comptime is_pkg_struct: { + for (@typeInfo(objc).Struct.decls) |decl| { + if (decl.is_pub and Return == @field(objc, decl.name)) { + break :is_pkg_struct true; + } + } + + break :is_pkg_struct false; + }; + + if (!is_pkg_struct) return result; + return .{ .value = result }; } }; } diff --git a/pkg/objc/object.zig b/pkg/objc/object.zig new file mode 100644 index 000000000..41c779eeb --- /dev/null +++ b/pkg/objc/object.zig @@ -0,0 +1,10 @@ +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); +}; diff --git a/pkg/objc/sel.zig b/pkg/objc/sel.zig index 4c4c62f7a..23c629f48 100644 --- a/pkg/objc/sel.zig +++ b/pkg/objc/sel.zig @@ -1,6 +1,11 @@ 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, @@ -20,6 +25,6 @@ pub const Sel = struct { test { const testing = std.testing; - const sel = Sel.registerName("yo"); - try testing.expectEqualStrings("yo", sel.getName()); + const s = Sel.registerName("yo"); + try testing.expectEqualStrings("yo", s.getName()); } From 59cb774cdd4fb0263aa18cd0c5e2cfbe4681a5ee Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 25 Oct 2022 21:25:07 -0700 Subject: [PATCH 6/6] pkg/objc: properties --- pkg/objc/class.zig | 32 ++++++++++++++++++++++++++++++++ pkg/objc/main.zig | 16 +++++++++++----- pkg/objc/msg_send.zig | 5 ++++- pkg/objc/object.zig | 10 ++++++++++ pkg/objc/property.zig | 27 +++++++++++++++++++++++++++ 5 files changed, 84 insertions(+), 6 deletions(-) create mode 100644 pkg/objc/property.zig diff --git a/pkg/objc/class.zig b/pkg/objc/class.zig index f655c82eb..c6a310307 100644 --- a/pkg/objc/class.zig +++ b/pkg/objc/class.zig @@ -14,6 +14,21 @@ pub const Class = struct { .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" { @@ -40,3 +55,20 @@ test "msgSend" { 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); +} diff --git a/pkg/objc/main.zig b/pkg/objc/main.zig index 6eefc51fe..9d1da6847 100644 --- a/pkg/objc/main.zig +++ b/pkg/objc/main.zig @@ -1,11 +1,17 @@ +const std = @import("std"); + pub const c = @import("c.zig"); pub usingnamespace @import("class.zig"); pub usingnamespace @import("object.zig"); +pub usingnamespace @import("property.zig"); pub usingnamespace @import("sel.zig"); -test { - @import("std").testing.refAllDecls(@This()); - - // TODO: remove once we integrate this - _ = @import("msg_send.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()); } diff --git a/pkg/objc/msg_send.zig b/pkg/objc/msg_send.zig index 4f2eaf699..06edf397d 100644 --- a/pkg/objc/msg_send.zig +++ b/pkg/objc/msg_send.zig @@ -29,7 +29,10 @@ pub fn MsgSend(comptime T: type) type { // This lets msgSend magically work with Object and so on. const is_pkg_struct = comptime is_pkg_struct: { for (@typeInfo(objc).Struct.decls) |decl| { - if (decl.is_pub and Return == @field(objc, decl.name)) { + if (decl.is_pub and + @TypeOf(@field(objc, decl.name)) == type and + Return == @field(objc, decl.name)) + { break :is_pkg_struct true; } } diff --git a/pkg/objc/object.zig b/pkg/objc/object.zig index 41c779eeb..f6ef6e427 100644 --- a/pkg/objc/object.zig +++ b/pkg/objc/object.zig @@ -8,3 +8,13 @@ pub const Object = struct { pub usingnamespace MsgSend(Object); }; + +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); + obj.msgSend(void, objc.sel("dealloc"), .{}); +} diff --git a/pkg/objc/property.zig b/pkg/objc/property.zig new file mode 100644 index 000000000..110516b0e --- /dev/null +++ b/pkg/objc/property.zig @@ -0,0 +1,27 @@ +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); + } +}; + +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()); +}