lib/sys/src/apple/objc.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2 const builtin = @import("builtin");
3
4 pub const Class = *opaque {};
5 pub const Ivar = *opaque {};
6 pub const SEL = *opaque {};
7 pub const Id = *anyopaque;
8 pub const BOOL = boolType(builtin.cpu.arch);
9
10 pub const nil: ?Id = null;
11
12 extern fn objc_getClass(name: [*:0]const u8) ?Class;
13 extern fn objc_allocateClassPair(
14 superclass: Class,
15 name: [*:0]const u8,
16 extra_bytes: usize,
17 ) ?Class;
18 extern fn objc_registerClassPair(class_value: Class) void;
19 extern fn objc_disposeClassPair(class_value: Class) void;
20 extern fn class_addIvar(
21 class_value: Class,
22 name: [*:0]const u8,
23 size: usize,
24 alignment: u8,
25 types: ?[*:0]const u8,
26 ) BOOL;
27 extern fn class_addMethod(
28 class_value: Class,
29 operation: SEL,
30 implementation: *const anyopaque,
31 types: [*:0]const u8,
32 ) BOOL;
33 extern fn object_setInstanceVariable(
34 object: Id,
35 name: [*:0]const u8,
36 value: ?*anyopaque,
37 ) ?Ivar;
38 extern fn object_getInstanceVariable(
39 object: Id,
40 name: [*:0]const u8,
41 value: *?*anyopaque,
42 ) ?Ivar;
43 extern fn sel_registerName(name: [*:0]const u8) SEL;
44 extern fn objc_msgSend() void;
45 extern fn objc_msgSend_stret() void;
46 extern fn objc_autoreleasePoolPush() *anyopaque;
47 extern fn objc_autoreleasePoolPop(context: *anyopaque) void;
48
49 pub const AutoreleasePool = struct {
50 context: *anyopaque,
51
52 pub fn init() AutoreleasePool {
53 return .{ .context = objc_autoreleasePoolPush() };
54 }
55
56 pub fn deinit(self: *AutoreleasePool) void {
57 objc_autoreleasePoolPop(self.context);
58 self.* = undefined;
59 }
60 };
61
62 pub fn lookupClass(name: [*:0]const u8) ?Class {
63 return objc_getClass(name);
64 }
65
66 pub fn class(name: [*:0]const u8) ?Id {
67 return @ptrCast(lookupClass(name) orelse return null);
68 }
69
70 pub fn allocateClassPair(superclass: Class, name: [*:0]const u8) ?Class {
71 return objc_allocateClassPair(superclass, name, 0);
72 }
73
74 pub fn registerClassPair(class_value: Class) void {
75 objc_registerClassPair(class_value);
76 }
77
78 pub fn disposeClassPair(class_value: Class) void {
79 objc_disposeClassPair(class_value);
80 }
81
82 pub fn addPointerIvar(class_value: Class, name: [*:0]const u8) bool {
83 const pointer_alignment: u8 = @intCast(@ctz(@as(usize, @alignOf(*anyopaque))));
84 return isTrue(class_addIvar(
85 class_value,
86 name,
87 @sizeOf(*anyopaque),
88 pointer_alignment,
89 "^v",
90 ));
91 }
92
93 pub fn addMethod(
94 class_value: Class,
95 operation: SEL,
96 implementation: *const anyopaque,
97 types: [*:0]const u8,
98 ) bool {
99 return isTrue(class_addMethod(class_value, operation, implementation, types));
100 }
101
102 pub fn setInstancePointer(
103 object: Id,
104 name: [*:0]const u8,
105 value: ?*anyopaque,
106 ) bool {
107 return object_setInstanceVariable(object, name, value) != null;
108 }
109
110 pub fn instancePointer(object: Id, name: [*:0]const u8) ?*anyopaque {
111 var value: ?*anyopaque = null;
112 _ = object_getInstanceVariable(object, name, &value) orelse return null;
113 return value;
114 }
115
116 pub fn selector(name: [*:0]const u8) SEL {
117 return sel_registerName(name);
118 }
119
120 pub fn boolean(value: bool) BOOL {
121 return booleanFor(builtin.cpu.arch, value);
122 }
123
124 pub fn isTrue(value: BOOL) bool {
125 return isTrueFor(builtin.cpu.arch, value);
126 }
127
128 pub fn retain(target: anytype) @TypeOf(target) {
129 const retained = send(Id, @ptrCast(target), selector("retain"), .{});
130 return @ptrCast(retained);
131 }
132
133 pub fn release(target: anytype) void {
134 send(void, @ptrCast(target), selector("release"), .{});
135 }
136
137 /// Whether `target` implements `operation`, so that optional API can be probed before it is sent.
138 pub fn respondsTo(target: Id, operation: SEL) bool {
139 return isTrue(send(BOOL, target, selector("respondsToSelector:"), .{operation}));
140 }
141
142 pub fn send(comptime Return: type, target: Id, operation: SEL, arguments: anytype) Return {
143 return dispatch(Return, entryForReturn(Return, builtin.cpu.arch), target, operation, arguments);
144 }
145
146 const Entry = enum {
147 message,
148 structure,
149 };
150
151 fn dispatch(
152 comptime Return: type,
153 comptime entry_kind: Entry,
154 target: Id,
155 operation: SEL,
156 arguments: anytype,
157 ) Return {
158 const entry = if (comptime entry_kind == .structure)
159 &objc_msgSend_stret
160 else
161 &objc_msgSend;
162 const implementation: *const message(Return, @TypeOf(arguments)) = @ptrCast(entry);
163 return @call(.auto, implementation, .{ target, operation } ++ arguments);
164 }
165
166 fn boolType(comptime arch: std.Target.Cpu.Arch) type {
167 return switch (arch) {
168 .aarch64 => bool,
169 else => i8,
170 };
171 }
172
173 fn booleanFor(comptime arch: std.Target.Cpu.Arch, value: bool) boolType(arch) {
174 return switch (arch) {
175 .aarch64 => value,
176 else => @intFromBool(value),
177 };
178 }
179
180 fn isTrueFor(comptime arch: std.Target.Cpu.Arch, value: boolType(arch)) bool {
181 return switch (arch) {
182 .aarch64 => value,
183 else => value != 0,
184 };
185 }
186
187 fn entryForReturn(comptime Return: type, comptime arch: std.Target.Cpu.Arch) Entry {
188 return switch (@typeInfo(Return)) {
189 .@"struct" => if (comptime supportedAggregateReturn(Return))
190 if (arch == .x86_64 and @sizeOf(Return) > 16) .structure else .message
191 else
192 @compileError("Objective-C aggregate returns support only non-empty extern f64 structures"),
193 else => .message,
194 };
195 }
196
197 fn supportedAggregateReturn(comptime Aggregate: type) bool {
198 const aggregate = switch (@typeInfo(Aggregate)) {
199 .@"struct" => |info| info,
200 else => return false,
201 };
202 if (aggregate.layout != .@"extern" or aggregate.field_types.len == 0) return false;
203 inline for (aggregate.field_types) |field_type| {
204 switch (@typeInfo(field_type)) {
205 .float => if (field_type != f64) return false,
206 .@"struct" => if (!supportedAggregateReturn(field_type)) return false,
207 else => return false,
208 }
209 }
210 return true;
211 }
212
213 /// The most explicit arguments one message takes. Metal's longest selectors take nine.
214 const max_message_arguments = 12;
215
216 /// The C signature of a message that takes `Arguments`, a tuple of explicit argument types, after
217 /// the receiver and selector.
218 fn message(comptime Return: type, comptime Arguments: type) type {
219 const argument_types = @typeInfo(Arguments).@"struct".field_types;
220 if (argument_types.len > max_message_arguments) {
221 @compileError("Objective-C messages support at most twelve explicit arguments");
222 }
223 var parameter_types: [argument_types.len + 2]type = undefined;
224 parameter_types[0] = Id;
225 parameter_types[1] = SEL;
226 for (argument_types, parameter_types[2..]) |argument_type, *parameter_type| {
227 parameter_type.* = argument_type;
228 }
229 return @Fn(¶meter_types, &@splat(.{}), Return, .{ .@"callconv" = .c });
230 }
231
232 test "Objective-C ABI scalar and pointer layouts" {
233 try std.testing.expectEqual(@as(usize, 1), @sizeOf(BOOL));
234 try std.testing.expect(boolType(.aarch64) == bool);
235 try std.testing.expect(boolType(.x86_64) == i8);
236 try std.testing.expectEqual(@sizeOf(usize), @sizeOf(Class));
237 try std.testing.expectEqual(@sizeOf(usize), @sizeOf(Ivar));
238 try std.testing.expectEqual(@sizeOf(usize), @sizeOf(SEL));
239 try std.testing.expectEqual(@sizeOf(usize), @sizeOf(Id));
240 try std.testing.expectEqual(true, booleanFor(.aarch64, true));
241 try std.testing.expectEqual(false, booleanFor(.aarch64, false));
242 try std.testing.expectEqual(@as(i8, 1), booleanFor(.x86_64, true));
243 try std.testing.expectEqual(@as(i8, 0), booleanFor(.x86_64, false));
244 try std.testing.expect(isTrueFor(.aarch64, true));
245 try std.testing.expect(isTrueFor(.x86_64, -1));
246 try std.testing.expect(isTrue(boolean(true)));
247 try std.testing.expect(!isTrue(boolean(false)));
248 }
249
250 test "Objective-C message signatures preserve exact C ABI types" {
251 const Signature = message(?Id, struct { u64, BOOL });
252 const info = @typeInfo(Signature).@"fn";
253 try std.testing.expectEqual(std.builtin.CallingConvention.c, info.attrs.@"callconv");
254 try std.testing.expect(info.return_type.? == ?Id);
255 try std.testing.expect(info.param_types[0].? == Id);
256 try std.testing.expect(info.param_types[1].? == SEL);
257 try std.testing.expect(info.param_types[2].? == u64);
258 try std.testing.expect(info.param_types[3].? == BOOL);
259 }
260
261 test "Objective-C dispatch exposes one message entry API" {
262 try std.testing.expect(!@hasDecl(@This(), "sendStret"));
263 }
264
265 test "Objective-C autorelease pool has an explicit poll lifetime" {
266 if (comptime builtin.os.tag != .macos) return error.SkipZigTest;
267 var pool = AutoreleasePool.init();
268 pool.deinit();
269 }
270
271 const ObjectiveCPointFixture = extern struct {
272 x: f64,
273 y: f64,
274 };
275
276 const ObjectiveCRectFixture = extern struct {
277 origin: ObjectiveCPointFixture,
278 size: ObjectiveCPointFixture,
279 };
280
281 const ObjectiveCIntegerPairFixture = extern struct {
282 x: u64,
283 y: u64,
284 };
285
286 const ObjectiveCNativePointFixture = struct {
287 x: f64,
288 y: f64,
289 };
290
291 test "Objective-C aggregate entry follows the macOS architecture ABI" {
292 try std.testing.expect(supportedAggregateReturn(ObjectiveCPointFixture));
293 try std.testing.expect(supportedAggregateReturn(ObjectiveCRectFixture));
294 try std.testing.expect(!supportedAggregateReturn(ObjectiveCIntegerPairFixture));
295 try std.testing.expect(!supportedAggregateReturn(ObjectiveCNativePointFixture));
296 try std.testing.expectEqual(Entry.message, entryForReturn(ObjectiveCPointFixture, .aarch64));
297 try std.testing.expectEqual(Entry.message, entryForReturn(ObjectiveCRectFixture, .aarch64));
298 try std.testing.expectEqual(Entry.message, entryForReturn(ObjectiveCPointFixture, .x86_64));
299 try std.testing.expectEqual(Entry.structure, entryForReturn(ObjectiveCRectFixture, .x86_64));
300 }