commit e8c63c795e4f343bb0936cf1c12faba4f5d2ce68 Author: Mitchell Hashimoto Date: Mon Jan 2 14:48:21 2023 -0800 initial import 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