In the last article we learned how Zig types map to C, and how C types map to Swift. Maybe not the most exciting piece of trivia ever, but a necessary introduction to learn how to interoperate between Zig, C, and Swift.
If the last post was about types, then this post is about data because we're going to learn about a few ways to handle memory between Zig and Swift, and we're going to put that into practice by loading a QOI image from Zig and by having Swift free it at the right moment.
No management at all
The first way memory is shared between Zig and Swift requires no management at all. This is what we already saw happen in the previous articles, and it's what happens whenever the caller assigns the result of a function call to a local variable, like so:
let foo: Int = zigFunc()
let bar: MyZigExternStruct = zigCreateMyStruct()
In this case the lifetime of foo
is entirely up to Swift so you don't need to do anything. This applies to all "value types" that you can get from C: numbers, pointers, unions and structs. Note that arrays are missing from this list because C doesn't allow returning arrays from functions.
Pointers
Let's take a moment to look at how Swift represents pointer types, it will come in handy later.
struct UnsafePointer<Pointee>
struct UnsafeMutablePointer<Pointee>
struct UnsafeRawPointer
struct UnsafeMutableRawPointer
The first differentiation between these pointer types is mutability, as their name implies. The second is that "Raw" pointers refer to untyped bytes, while the others are generic with regards to the type they point to.
Slices
Swift also has some types similar to Zig slices, so pointers that also bundle a length. They mirror their pointer counterparts:
struct UnsafeBufferPointer<Element>
struct UnsafeMutableBufferPointer<Element>
struct UnsafeRawBufferPointer
struct UnsafeMutableRawBufferPointer
You can learn more about their constructors and methods in the official Swift docs.
Pointers are also special because, while the pointer itself is a value type handled directly by Swift, the memory that it references might need to be managed. In our previous article we sent a string pointer to Swift, but we didn't need to do anything else because the referenced bytes were part of the data section of our Zig library.
Let's see what to do if the bytes are allocated on the heap instead.
Wrapper class and destructors
When calling a Zig function that returns a pointer to dynamically allocated memory, you will be in charge of freeing such memory at the right moment. In simple cases, the lifetime of such memory is directly tied to the lifetime of the corresponding Swift object: once it stops being referenced anywhere and it's about to get garbage collected, that's when we'll want to free the relative memory allocated by Zig.
Swift makes this very easy by using classes. All you'll have to do is expose a function to free memory from Zig to Swift and call it in the class destructor. Here's a full example.
In this Zig code we allocate on the heap a new instance of ZigFoo
every time createFoo
is invoked. This is not how you would normally want to do that, since the struct could be passed by value with no downsides, but let's roll with it for illustrative purposes.
const std = @import("std");
const ZigFoo = extern struct {
count: u32,
};
export fn createFoo(count: u32) *ZigFoo {
var f = std.heap.c_allocator.create(ZigFoo) catch @panic("oom!");
f.* = .{ .count = count };
return f;
}
export fn freeFoo(f: *ZigFoo) void {
std.heap.c_allocator.destroy(f);
}
Bridge header definitions:
#include <stdint.h>
struct ZigFoo {
uint32_t count;
};
struct ZigFoo *createFoo(uint32_t count);
void freeFoo(struct ZigFoo *f);
Swift:
// We could just use the API directly but then we wouldn't
// be able to clean up automatically at the right moment!
// let f = createFoo(42)
class Foo {
var ptr: UnsafeMutablePointer<ZigFoo>
var count: Int {
get {
return Int(ptr.pointee.count)
}
}
init(count: UInt32) {
ptr = createFoo(count)
}
deinit {
print("about to call `freeFoo`!")
freeFoo(ptr);
}
}
This Swift code wraps the C type into a class where we override the destructor and call freeFoo
whenever Swift decides it's a good time to free the object. This is very nice because the class definition cleanly defines and encapsulates the point of contact between the two different memory management strategies. Once this work is done, you can forget everything about manual memory management because the system will always do the right thing for you.
Of course, there might be cases where the lifetime of a dynamic allocation is more complicated than that, but this simple pattern can get you very far, especially considering that in most realistic use cases you don't want (nor need) to jump between Zig and Swift too often.
A special case: Data
Swift has a type that can represent a type-erased chunk of memory that also has built-in cleanup functionality. This can be very handy when you want to ferry information from Zig that, as the name of the type suggests, you mostly consider generic data, rather than structured information that you want to manipulate directly. A perfect example of this is image data, as we'll soon see with the QOI example.
Data
has two init functions that are of particular interest to us:
func Data(bytes: UnsafeMutableRawPointer, count: Int)
func Data(bytesNoCopy: UnsafeMutableRawPointer, count: Int, deallocator: Data.Deallocator)
The first init function copies the data referenced by the UnsafeMutableRawPointer
, while the second one doesn't. The second one seems preferable as it allows us to avoid copying data and it also allows us to provide a deallocator
, which means that Data
will be able to manage the full lifecycle of that memory without requiring us to do any ulterior wrapping.
Even if zero copy is more efficient, you might want to keep in mind that a copy might drastically simplify your lifetime management in special situations. Copying the data will allow you to immediately free the memory allocated by Zig.
Decoding QOI from Zig
QOI is an image format similar to PNG but lighter and thus faster. @xq has written an implementation in Zig that we're going to use to decode an image and display it in our app. We also want to use this exercise as an excuse to flex our memory lifecycle management muscle, so we're also going to add a button that will swap the image, causing Swift to eventually garbage collect discarded memory.
First of all we need to get MasterQ32/zig-qoi
and an image in .qoi format. From that repository copy src/qoi.zig
, data/zero.qoi
, and put both of the files inside zighello/src
. Don't forget that you also need to make these files show up in Xcode, so you might want to use the Add File…
contextual menu option on src/
.
We'll need two main functions from Zig: one to decode the image and one to free the bytes that qoi.zig
will allocate on our behalf. As a shortcut we're going to embed the image as data inside our Zig library, then we're going to invoke qoi.decodeBuffer()
on it. Everything would be extremely straightforward if not for one detail: decodeBuffer
returns Image
. Look at its definition, notice anything?
/// A QOI image with RGBA pixels.
pub const Image = struct {
width: u32,
height: u32,
pixels: []Color,
colorspace: Colorspace,
// ...
pub fn deinit(self: *Image, allocator: std.mem.Allocator) void {
allocator.free(self.pixels);
self.* = undefined;
}
};
pub const Color = extern struct {
r: u8,
g: u8,
b: u8,
a: u8 = 0xFF,
fn hash(c: Color) u6 {
return @truncate(u6, c.r *% 3 +% c.g *% 5 +% c.b *% 7 +% c.a *% 11);
}
pub fn eql(a: Color, b: Color) bool {
return std.meta.eql(a, b);
}
};
Unfortunately for us, Image
is not extern
, which means that we won't be able to use it as a return value from our C interface. We now have four options in front of us:
- Change the code in
qoi.zig
to make itextern
. It's just a matter of adding that one keyword and everything will work, but we'll have modified code that we want to consider a library. - Non-extern structs can't be passed directly to C because they don't have a well-defined in-memory layout, but we could still heap-allocate the struct and give a (void) pointer to it to Swift. This works, but it's less efficient and it would still require us to write a bunch of getter functions, since Swift won't be able to directly access any of its fields.
- We could create an
extern struct
definition that maps 1:1 toImage
(i.e. has the same fields) and we return that from Zig instread. This is generally the way to go unless you plan to map some fields to different types than what Swift's automatic mapping would do. - We could unbundle the struct into discrete fields that we pass to Swift using
inout
parameters. This won't require extra dynamic allocation, nor will it require changing the library's code, but it will force us to deal more closely with how theImage
struct works, as we'll be ditching it at one point and we won't be able to use itsdeinit()
anymore, for example.
Since I know what lies ahead of us, I know that the best option in this case is #4, mostly because in my experience the easiest way to create a Swift image object starting from some bytes is to use Data
. This last point is important and non-obvious if you don't know how memory works in C. The pixels
field is a slice of Color
. Each instance of Color
basically represents a single pixel. If you look at its definition you can also see that it's basically a 32bit value in RGBA format. Defining pixels as a struct instead of just a number has two benefits: the struct is endianess-independent, and you get a nice way of addressing individual channels. That said, we won't need to worry about any of that so I'll reduce the entire chunk of memory to just an array of bytes that we'll feed to Data
.
Based on this analysis, this is the Zig code to put in main.zig
:
const std = @import("std");
const qoi = @import("qoi.zig");
const zero_bytes = @embedFile("zero.qoi");
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
export fn getZeroRawImage(width_ptr: *u32, height_ptr: *u32) [*]u8 {
const img = qoi.decodeBuffer(gpa.allocator(), zero_bytes) catch @panic("oh no");
width_ptr.* = img.width;
height_ptr.* = img.height;
return @ptrCast([*]u8, img.pixels.ptr);
}
export fn freeZeroRawImage(pixels: [*]u8, len: u32) void {
gpa.allocator().free(pixels[0..len]);
}
And these are the corresponding definitions for exports.h
:
#include <stdint.h>
uint8_t *getZeroRawImage(uint32_t *width_ptr, uint32_t *height_ptr);
void freeZeroRawImage(void *pixels, size_t len);
Finally, in Swift create a new file called ZigRawImage.swift
and place it next to ContentView.swift
. In there we'll add the code that calls into Zig and creates a nice Swift type for us, and we'll also add a bunch of boilerplate to create a UIImage
starting from pixel data.
import SwiftUI
struct ZigRawImage {
static let bytes_per_pixel = 4
var width: Int
var height: Int
var pixels: Data
init() {
var w = UInt32()
var h = UInt32()
let p = getZeroRawImage(&w, &h)
width = Int(w)
height = Int(h)
let count = width * height * ZigRawImage.bytes_per_pixel;
pixels = Data(bytesNoCopy: p!, count: count, deallocator: .custom(freeZeroRawImage))
}
}
extension UIImage {
convenience init?(pixels: Data, width: Int, height: Int) {
guard width > 0 && height > 0 else { return nil }
guard let providerRef = CGDataProvider(data: pixels as CFData)
else { return nil }
guard let cgim = CGImage(
width: width,
height: height,
bitsPerComponent: 8,
bitsPerPixel: 32,
bytesPerRow: width * ZigRawImage.bytes_per_pixel,
space: CGColorSpaceCreateDeviceRGB(),
bitmapInfo: CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedLast.rawValue),
provider: providerRef,
decode: nil,
shouldInterpolate: true,
intent: .defaultIntent)
else { return nil }
self.init(cgImage: cgim)
}
}
The first interesting bit of this code is how we instantiate Data
: as you can see we're able to pass in freeZeroRawImage
directly as the destructor function because it has the right signature, pretty neat!
Another interesting part is how we're able to just "take a pointer" from w
and h
. Don't let the syntax sugar trick you, there's a lot more going on there than what the code would let you suspect. Pointers in Swift are to be considered always unstable. The Swift runtime can and will move values around whenever it pleases, since it can fix references in Swift's code transparently. This means, among other things, that Swift can't give ownership of a piece of memory to Zig in normal situations. You can learn more from this WWDC 2020 talk.
Finally, let's wire everything into our main app. Open ContentView.swift
and change the definition to add a bit of state and a button to toggle the image.
struct ContentView: View {
@State var rawImg: ZigRawImage? = nil
var body: some View {
if let rawImg = rawImg {
Image(uiImage: UIImage(pixels: rawImg.pixels, width: rawImg.width, height: rawImg.height)!)
.resizable()
.scaledToFit()
.padding()
.transition(.opacity)
} else {
Text("the image is set to null")
.padding()
}
Button("Toggle Zero") {
withAnimation {
if (rawImg == nil) {
rawImg = ZigRawImage()
} else {
rawImg = nil
}
}
}
}
}
At this point you should be able to build the application successfully and toggle Zero by pressing the button.
How can we be sure that the memory gets freed correctly though? Xcode has a handy tool for that: from the left column click on the spray can and you will see a bunch of runtime statistics, including memory usage. Keep toggling Zero and you will see that the memory usage remains stable.
As a countertest, change how pixels
gets initialized in ZigRawImage
:
pixels = Data(bytesNoCopy: p!, count: count, deallocator: .none)
Rebuild the application and watch memory usage increase consistently at every other toggle: congratulations, we're leaking memory!
Next steps
With this last article we've taken a look at how to use Zig alongside Xcode and Swift from start to finish. If you have a reason to want to leverage a Zig codebase into an iOS application, now you've seen enough to get started. In fact, this is how many Swift libraries work, albeit with C instead of Zig, like this QOI implementation for Swift does, for example.
In the next article we're going to focus our attention towards a more pure gamedev use case and we're going to ditch both Xcode and Swift. Will Zig be able to build an iOS app all by itself? (Spoilers: yes!)
Latest comments (5)
Thanks for you effort :-) any plans to continue the series?
This series is amazing, thank you. I found a header generation system (more recent fork). Might stick to manual header writing while learning.
Yeah, Zig will definitely start producing header files again once we re-enable the feature in the compiler, in the meantime my recommendation is to do it manually as it's a good way of making a virtue (learning how C type definitions work wrt Swift) out of necessity.
spay can -> spray can
thanks, fixed!