mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-16 16:56:09 +03:00
Objective-C Runtime (Low-level ObjC interfacing)
This adds a new homemade package `pkg/objc` that wraps the Objective-C runtime so that Zig can interface with ObjC. This doesn't have 100% API coverage but all the hard problems (mainly `objc_msgSend`) are done. We can merge this now, start working on ObjC stuff, and expand APIs as we need them. The `objc` package is of course only available to Mac builds.
This commit is contained in:
@ -75,16 +75,12 @@ on the [Zig downloads page](https://ziglang.org/download/).
|
|||||||
With Zig installed, a binary can be built using `zig build`:
|
With Zig installed, a binary can be built using `zig build`:
|
||||||
|
|
||||||
```shell-session
|
```shell-session
|
||||||
$ zig build -fstage1
|
$ zig build
|
||||||
...
|
...
|
||||||
|
|
||||||
$ zig-out/bin/ghostty
|
$ 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).
|
This will build a binary for the currently running system (if supported).
|
||||||
You can cross compile by setting `-Dtarget=<target-triple>`. For example,
|
You can cross compile by setting `-Dtarget=<target-triple>`. For example,
|
||||||
`zig build -Dtarget=aarch64-macos` will build for Apple Silicon macOS. Note
|
`zig build -Dtarget=aarch64-macos` will build for Apple Silicon macOS. Note
|
||||||
|
@ -11,6 +11,7 @@ const libxml2 = @import("vendor/zig-libxml2/libxml2.zig");
|
|||||||
const libuv = @import("pkg/libuv/build.zig");
|
const libuv = @import("pkg/libuv/build.zig");
|
||||||
const libpng = @import("pkg/libpng/build.zig");
|
const libpng = @import("pkg/libpng/build.zig");
|
||||||
const macos = @import("pkg/macos/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 stb_image_resize = @import("pkg/stb_image_resize/build.zig");
|
||||||
const utf8proc = @import("pkg/utf8proc/build.zig");
|
const utf8proc = @import("pkg/utf8proc/build.zig");
|
||||||
const zlib = @import("pkg/zlib/build.zig");
|
const zlib = @import("pkg/zlib/build.zig");
|
||||||
@ -199,6 +200,7 @@ fn addDeps(
|
|||||||
|
|
||||||
// Mac Stuff
|
// Mac Stuff
|
||||||
if (step.target.isDarwin()) {
|
if (step.target.isDarwin()) {
|
||||||
|
step.addPackage(objc.pkg);
|
||||||
step.addPackage(macos.pkg);
|
step.addPackage(macos.pkg);
|
||||||
_ = try macos.link(b, step, .{});
|
_ = try macos.link(b, step, .{});
|
||||||
}
|
}
|
||||||
|
10
pkg/objc/build.zig
Normal file
10
pkg/objc/build.zig
Normal file
@ -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 ".";
|
||||||
|
}
|
4
pkg/objc/c.zig
Normal file
4
pkg/objc/c.zig
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
pub usingnamespace @cImport({
|
||||||
|
@cInclude("objc/runtime.h");
|
||||||
|
@cInclude("objc/message.h");
|
||||||
|
});
|
74
pkg/objc/class.zig
Normal file
74
pkg/objc/class.zig
Normal file
@ -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);
|
||||||
|
}
|
17
pkg/objc/main.zig
Normal file
17
pkg/objc/main.zig
Normal file
@ -0,0 +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");
|
||||||
|
|
||||||
|
/// 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());
|
||||||
|
}
|
135
pkg/objc/msg_send.zig
Normal file
135
pkg/objc/msg_send.zig
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
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 {
|
||||||
|
// 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);
|
||||||
|
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
|
||||||
|
@TypeOf(@field(objc, decl.name)) == type 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 };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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),
|
||||||
|
})));
|
||||||
|
}
|
20
pkg/objc/object.zig
Normal file
20
pkg/objc/object.zig
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
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);
|
||||||
|
};
|
||||||
|
|
||||||
|
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"), .{});
|
||||||
|
}
|
27
pkg/objc/property.zig
Normal file
27
pkg/objc/property.zig
Normal file
@ -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());
|
||||||
|
}
|
30
pkg/objc/sel.zig
Normal file
30
pkg/objc/sel.zig
Normal file
@ -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());
|
||||||
|
}
|
Reference in New Issue
Block a user