From e8c63c795e4f343bb0936cf1c12faba4f5d2ce68 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 2 Jan 2023 14:48:21 -0800 Subject: [PATCH] initial import --- .envrc | 5 ++ .gitignore | 2 + .gitmodules | 3 + build.zig | 28 +++++++ flake.lock | 111 +++++++++++++++++++++++++++ flake.nix | 46 ++++++++++++ shell.nix | 12 +++ src/autorelease.zig | 16 ++++ src/c.zig | 4 + src/class.zig | 74 ++++++++++++++++++ src/main.zig | 18 +++++ src/msg_send.zig | 177 ++++++++++++++++++++++++++++++++++++++++++++ src/object.zig | 70 ++++++++++++++++++ src/property.zig | 35 +++++++++ src/sel.zig | 30 ++++++++ vendor/mach | 1 + 16 files changed, 632 insertions(+) create mode 100644 .envrc create mode 100644 .gitignore create mode 100644 .gitmodules create mode 100644 build.zig create mode 100644 flake.lock create mode 100644 flake.nix create mode 100644 shell.nix create mode 100644 src/autorelease.zig create mode 100644 src/c.zig create mode 100644 src/class.zig create mode 100644 src/main.zig create mode 100644 src/msg_send.zig create mode 100644 src/object.zig create mode 100644 src/property.zig create mode 100644 src/sel.zig create mode 160000 vendor/mach diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..24e67d2 --- /dev/null +++ b/.envrc @@ -0,0 +1,5 @@ +# If we are a computer with nix-shell available, then use that to setup +# the build environment with exactly what we need. +if has nix; then + use nix +fi diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4c82b07 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +zig-cache +zig-out diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..1c9e43f --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "vendor/mach"] + path = vendor/mach + url = https://github.com/hexops/mach.git diff --git a/build.zig b/build.zig new file mode 100644 index 0000000..0784fc2 --- /dev/null +++ b/build.zig @@ -0,0 +1,28 @@ +const std = @import("std"); +const system_sdk = @import("vendor/mach/libs/glfw/system_sdk.zig"); + +/// Use this with addPackage in your project. +pub const pkg = std.build.Pkg{ + .name = "objc", + .source = .{ .path = thisDir() ++ "/src/main.zig" }, +}; + +pub fn build(b: *std.build.Builder) !void { + const target = b.standardTargetOptions(.{}); + const mode = b.standardReleaseOptions(); + + const tests = b.addTestExe("objc-test", "src/main.zig"); + tests.setBuildMode(mode); + tests.setTarget(target); + tests.linkSystemLibrary("objc"); + system_sdk.include(b, tests, .{}); + tests.install(); + + const test_step = b.step("test", "Run tests"); + const tests_run = tests.run(); + test_step.dependOn(&tests_run.step); +} + +fn thisDir() []const u8 { + return std.fs.path.dirname(@src().file) orelse "."; +} diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..4f265ac --- /dev/null +++ b/flake.lock @@ -0,0 +1,111 @@ +{ + "nodes": { + "flake-compat": { + "flake": false, + "locked": { + "lastModified": 1668681692, + "narHash": "sha256-Ht91NGdewz8IQLtWZ9LCeNXMSXHUss+9COoqu6JLmXU=", + "owner": "edolstra", + "repo": "flake-compat", + "rev": "009399224d5e398d03b22badca40a37ac85412a1", + "type": "github" + }, + "original": { + "owner": "edolstra", + "repo": "flake-compat", + "type": "github" + } + }, + "flake-utils": { + "locked": { + "lastModified": 1667395993, + "narHash": "sha256-nuEHfE/LcWyuSWnS8t12N1wc105Qtau+/OdUAjtQ0rA=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "5aed5285a952e0b949eb3ba02c12fa4fcfef535f", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "flake-utils_2": { + "locked": { + "lastModified": 1659877975, + "narHash": "sha256-zllb8aq3YO3h8B/U0/J1WBgAL8EX5yWf5pMj3G0NAmc=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "c0e246b9b83f637f4681389ecabcb2681b4f3af0", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1672580127, + "narHash": "sha256-3lW3xZslREhJogoOkjeZtlBtvFMyxHku7I/9IVehhT8=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "0874168639713f547c05947c76124f78441ea46c", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "release-22.05", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs_2": { + "locked": { + "lastModified": 1661151577, + "narHash": "sha256-++S0TuJtuz9IpqP8rKktWyHZKpgdyrzDFUXVY07MTRI=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "54060e816971276da05970a983487a25810c38a7", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-compat": "flake-compat", + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs", + "zig": "zig" + } + }, + "zig": { + "inputs": { + "flake-utils": "flake-utils_2", + "nixpkgs": "nixpkgs_2" + }, + "locked": { + "lastModified": 1672661306, + "narHash": "sha256-PsGj6ynMs4r5BMsPSi9feJe4OxufH0OqJtuHJwT9NBY=", + "owner": "mitchellh", + "repo": "zig-overlay", + "rev": "13d033cc9439685eeb0ea8ef535582ab2302db29", + "type": "github" + }, + "original": { + "owner": "mitchellh", + "repo": "zig-overlay", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..db48258 --- /dev/null +++ b/flake.nix @@ -0,0 +1,46 @@ +{ + description = "Objective-C runtime bindings for Zig"; + + inputs = { + nixpkgs.url = "github:nixos/nixpkgs/release-22.05"; + flake-utils.url = "github:numtide/flake-utils"; + zig.url = "github:mitchellh/zig-overlay"; + + # Used for shell.nix + flake-compat = { + url = github:edolstra/flake-compat; + flake = false; + }; + }; + + outputs = { + self, + nixpkgs, + flake-utils, + ... + } @ inputs: let + overlays = [ + # Other overlays + (final: prev: { + zigpkgs = inputs.zig.packages.${prev.system}; + }) + ]; + + # Our supported systems are the same supported systems as the Zig binaries + systems = builtins.attrNames inputs.zig.packages; + in + flake-utils.lib.eachSystem systems ( + system: let + pkgs = import nixpkgs {inherit overlays system;}; + in rec { + devShells.default = pkgs.mkShell { + nativeBuildInputs = with pkgs; [ + zigpkgs.master + ]; + }; + + # For compatibility with older versions of the `nix` binary + devShell = self.devShells.${system}.default; + } + ); +} diff --git a/shell.nix b/shell.nix new file mode 100644 index 0000000..a15057a --- /dev/null +++ b/shell.nix @@ -0,0 +1,12 @@ +(import + ( + let + flake-compat = (builtins.fromJSON (builtins.readFile ./flake.lock)).nodes.flake-compat; + in + fetchTarball { + url = "https://github.com/edolstra/flake-compat/archive/${flake-compat.locked.rev}.tar.gz"; + sha256 = flake-compat.locked.narHash; + } + ) + {src = ./.;}) +.shellNix diff --git a/src/autorelease.zig b/src/autorelease.zig new file mode 100644 index 0000000..18adf14 --- /dev/null +++ b/src/autorelease.zig @@ -0,0 +1,16 @@ +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; diff --git a/src/c.zig b/src/c.zig new file mode 100644 index 0000000..622ea4b --- /dev/null +++ b/src/c.zig @@ -0,0 +1,4 @@ +pub usingnamespace @cImport({ + @cInclude("objc/runtime.h"); + @cInclude("objc/message.h"); +}); diff --git a/src/class.zig b/src/class.zig new file mode 100644 index 0000000..c6a3103 --- /dev/null +++ b/src/class.zig @@ -0,0 +1,74 @@ +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); +} diff --git a/src/main.zig b/src/main.zig new file mode 100644 index 0000000..fe18b06 --- /dev/null +++ b/src/main.zig @@ -0,0 +1,18 @@ +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()); +} diff --git a/src/msg_send.zig b/src/msg_send.zig new file mode 100644 index 0000000..ce0d049 --- /dev/null +++ b/src/msg_send.zig @@ -0,0 +1,177 @@ +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), + }))); +} diff --git a/src/object.zig b/src/object.zig new file mode 100644 index 0000000..b69b0ee --- /dev/null +++ b/src/object.zig @@ -0,0 +1,70 @@ +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"), .{}); +} diff --git a/src/property.zig b/src/property.zig new file mode 100644 index 0000000..9adcb3c --- /dev/null +++ b/src/property.zig @@ -0,0 +1,35 @@ +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()); +} diff --git a/src/sel.zig b/src/sel.zig new file mode 100644 index 0000000..23c629f --- /dev/null +++ b/src/sel.zig @@ -0,0 +1,30 @@ +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()); +} diff --git a/vendor/mach b/vendor/mach new file mode 160000 index 0000000..2271c78 --- /dev/null +++ b/vendor/mach @@ -0,0 +1 @@ +Subproject commit 2271c78fd6066fd031ef70ff4900de339dd71629