In this post we’ll take a brief look at the experience of using Zig to work with system APIs in C. We’ll work on a bit of code to list windows in Darwin (OS X).
All code is online at: github.com/aalbacetef/x => blog/osx-windows
Note: if there’s demand I’m happy to write similar posts for Windows and Linux!
The vast majority of our interaction with C will be using the CoreGraphics
framework. Zig makes this very easy by the use of @cImport
.
const C = @cImport({
@cInclude("CoreGraphics/CoreGraphics.h");
});
We now have all symbols available via C
.
@cImport
lets you use that expression block to do all kinds of C defines and includes using @cDefine
, @cUndef
, and @cInclude
.
Zig recommends having only one @cImport
block in your application, barring a few edge cases (read more here).
When building we’ll need to include the -framework
switch or update our build.zig
. Personally, I prefer using build.zig
so we need to add calls to linkFramework
:
const exe = b.addExecutable(.{
.name = "osx-windows",
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
});
exe.linkFramework("CoreFoundation");
exe.linkFramework("CoreGraphics");
We will obviously need to make sure that we free all memory we allocate, but there are also the CoreFoundation objects that need to be passed to CFRelease
. I’ve heard some arguments here and there that short-lived programs don’t need to worry about freeing memory, but it is good practice to develop proper memory management habits plus you never know when the user’s OS might not actually free all resources, be it due to a bug or by design.
Now, one of the benefits of Zig is that the GeneralPurposeAllocator
will yell at you if there are leaks (std.testing.allocator does as well). For example:
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
const allocator = gpa.allocator();
defer {
const check = gpa.deinit();
switch (check) {
.ok => {},
.leak => std.debug.print("leaked\n", .{}),
}
}
Something to bear in mind is to be careful that we don’t over-release or, to use the technical term, double free. This can happen when calling CFRelease
on something that we don’t own, or gets released as part of a collection.
There are a few rules that help us determine if we need to call CFRelease
:
CFRelease
on itFor more in depth information, check out the docs:
To give you the overview, we are going to:
The main method to do this will be CGWindowListCopyWindowInfo
. This method takes two arguments:
CGWindowListOption
CGWindowID
In our case, we want all windows so we use:
C.CGWindowListCopyWindowInfo(
C.kCGWindowListOptionAll,
C.kCGNullWindowID,
);
This will return a CFArray
, where each entry is a CFDictionary
corresponding to a window.
CFShow
to print out structures. It’s a really useful function when you’re poking around data. The very first thing we need to do is get the length of the array:
const index = C.CFArrayGetCount(self.ref);
const n: usize = @intCast(index);
We then access the elements of the array with CFArrayGetValueAtIndex
:
for (0..n) |k| {
const idx: c_long = @intCast(k);
const v = C.CFArrayGetValueAtIndex(self.ref, idx);
if (v) |dict_ref| {
// do something with dict_ref
}
}
A CFDictionary is a set of key-value pairs. The window information dictionaries we get have the following props we’re interested in:
The full set of props is available at:
As can be seen from the docs, we have the following mapping of constants:
name | constant |
---|---|
id | kCGWindowNumber |
alpha | kCGWindowAlpha |
owner pid | kCGWindowOwnerPID |
bounds | kCGWindowBounds |
owner name | kCGWindowOwnerName |
name | kCGWindowName |
We use CFDictionaryGetValue(dict, key)
.
An example where we get the ID:
const raw = C.CFDictionaryGetValue(dict, C.kCGWindowNumber);
if(raw) |v| {
const num_ref: C.CFNumberRef = @ptrCast(v);
var id: u64 = 0;
if(C.CFNumberGetValue(num_ref, C.kCFNumberLongLongType, &id) == 0) {
return ConversionError.NotSuccessful;
}
// ...
// do something with num
// ...
}
A few things to note:
We do something similar for strings:
const raw_val = C.CFDictionaryGetValue(dict, C.kCGWindowOwnerName);
const encoding = C.kCFStringEncodingUTF8;
if (raw_val) |v| {
const raw: C.CFStringRef = @ptrCast(v);
const chars = C.CFStringGetLength(raw);
const len = C.CFStringGetMaximumSizeForEncoding(chars, encoding) + 1;
const n: usize = @intCast(len);
const s: []u8 = try alloc.alloc(u8, n);
const _ptr: [*]u8 = @ptrCast(s);
if (C.CFStringGetCString(raw, _ptr, len, encoding) == 0) {
return ConvError.UTF8EncodingFailed;
}
// ...
// do something with s
// ...
The approach I’ve taken is to declare two structs, one to interface with the Window Info List (CFArray
) and one to store the info of each Window Info entry (CFDictionary
).
I won’t put the whole code here, you can read it at: github.com/aalbacetef/x => blog/osx-windows But I will show how the main function looks.
pub fn main() !void {
const windowList = try darwin.WindowList.init();
defer windowList.deinit();
const n = windowList.count();
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
const allocator = gpa.allocator();
defer {
const check = gpa.deinit();
switch (check) {
.ok => {},
.leak => std.debug.print("leaked\n", .{}),
}
}
var windows: []darwin.Window = undefined;
windows = try allocator.alloc(darwin.Window, n);
defer {
for (windows) |w| {
w.deinit() catch {
std.debug.print("error deinit\n", .{});
};
}
allocator.free(windows);
}
try windowList.makeWindows(allocator, windows, n);
for (0..n) |k| {
const w = windows[k];
w.print();
}
}
Overall, Zig’s interop with C is fantastic and it shows! C functions are typed and the experiences of having the LSP suggest matching types and functions as you write means you don’t have to refer back to the docs all the time.