Table of Contents

Introduction

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!

How to include C

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");

About Managing Memory

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", .{}),
	}
}

Over-releasing and the Create/Get rules

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:

  • if you own the object, you are responsible for calling CFRelease on it
  • you don’t own objects returned by functions with Get in the name
  • you own objects which are returned by functions with Create, Copy, New, or Alloc in the name

For more in depth information, check out the docs:

Listing windows

To give you the overview, we are going to:

  • get a list of window information objects
  • iterate over them
  • copy over the information/properties to our own custom window objects
  • print what we got

The main method to do this will be CGWindowListCopyWindowInfo. This method takes two arguments:

  • a CGWindowListOption
  • a 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.

hint Use CFShow to print out structures. It’s a really useful function when you’re poking around data.

Working with a CFArray

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
	}
}

Working with a CFDictionary

A CFDictionary is a set of key-value pairs. The window information dictionaries we get have the following props we’re interested in:

  • id
  • alpha
  • owner pid
  • bounds
  • owner name (optional)
  • name (optional)

The full set of props is available at:

As can be seen from the docs, we have the following mapping of constants:

nameconstant
idkCGWindowNumber
alphakCGWindowAlpha
owner pidkCGWindowOwnerPID
boundskCGWindowBounds
owner namekCGWindowOwnerName
namekCGWindowName

Getting the key’s value

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:

  • CFDictionaryGetValue will return a possibly null pointer, so we need to check it with the optional-if
  • the dictionary value we get needs to be decoded into the appropriate type
  • OS X follows the standard pattern for return values where success=true failure=false (we get back integer values though). Windows follows a similar pattern: success=non-zero, failure=0, see for example EnumWindows

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
	// ...

Putting it all together

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();
    }
}

Thoughts

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.