Cover image for Mapping Types between Zig and Swift
Loris Cro
Loris Cro

Posted on • Updated on

Mapping Types between Zig and Swift

In the previous article we did the work required to integrate Zig into a Xcode project. At the end of the process we were able to call add, a function implemented in Zig, from Swift. Passing numbers around is easy, but it's a different story when it comes to complex types, like structs and memory buffers. That said, Swift offers various sugared interfaces to deal with raw pointers and manual memory management, but it still requires some effort on our part.

Today we're going to learn how to access from Swift each Zig primitive type. This topic is a bit wide and notion-heavy so I'll split it in two parts. In this first part we're going to share a string between Zig and Swift, and in the next part we're going to take a QOI-encoded image, make Zig decode it, pass it to Swift and finally display it.

Zig to C

When we have some data in Zig, we first need to make sure we express it in a way that's compatible with the C ABI to be able to hand it over to Swift, as that's the only common "language" understood by both Zig and Swift.

In the previous article I've mentioned how the self-hosted compiler will be able to generate these C definitions for us, that said you still need to be at least vaguely familiar with them if you want to be able to diagnose programming errors correctly.


Integers with standard sizes are easy to map to C. One important thing to note is that you will need to add three includes to the bridging header file (exports.h from the previous article): stddef.h, stdbool.h and stdint.h.

Zig C
bool bool
u8 uint8_t or char
i8 int8_t
u16 uint16_t
i16 int16_t
u32 uint32_t
i32 int32_t
u64 uint64_t
i64 int64_t
usize uintptr_t (usually equivalent to size_t)
isize intptr_t (usually equivalent to ssize_t)
f32 float
f64 double

As an example, the following Zig functions

export fn multiplyFloat(f: f64, by: usize) f64 {
    return f * @intToFloat(by);

export fn isOdd(n: u32) bool {
   return n % 2 == 1;
Enter fullscreen mode Exit fullscreen mode

Will have to be declared in exports.h like this:

#include <stddef.h>
#include <stdbool.h>
#include <stdint.h>

double multiplyFloat(double f, size_t by);
bool isOdd(uint32_t n);
Enter fullscreen mode Exit fullscreen mode


Zig enums normally don't require you to specify the underlying integer type, as Zig will pick it for you based on the number of members it has, but to ensure C ABI compatibility though you will need to specify a size of c_int because, in C, users can't define the size of enum types. The C compiler will start at c_int and then progressively go up in case 32 bits are not enough.

const Foo = enum(c_int) { a, b, c };
export fn bar(foo: Foo) void { ... };
Enter fullscreen mode Exit fullscreen mode
enum Foo { a, b, c };
void bar(enum Foo foo);
Enter fullscreen mode Exit fullscreen mode


Structs in Zig have no well-defined in-memory layout by default because Zig reserves the right to reorder struct fields and perform other transformations, such as adding hidden fields that add safety in debug mode. This means that if you want to ensure a struct has a memory layout compatible with C you have to explicitly mark it as extern.

const Foo = extern struct {
    a: usize,
    b: i32,
    c: Bar,

const Bar = extern struct {
    d: bool,

export fn baz(foo: Foo) void { ... };
Enter fullscreen mode Exit fullscreen mode

In exports.h this becomes:

#include <stddef.h>
#include <stdbool.h>
#include <stdint.h>

// Order of declarations is important in C!
struct Bar {
    bool d;

struct Foo {
    size_t a;
    int32_t b;
    struct Bar c;

void baz(struct Foo foo);
Enter fullscreen mode Exit fullscreen mode


Same as structs, Zig unions have no well-defined in-memory layout so you need to declare them as extern if you want C ABI compatibility.

Note also that C has no concept of tagged union. If you want to translate a Zig tagged union to C you will need to explicitly create a struct with two fields (one for the tag and one for the union).

const Foo = extern union {
    bar: u64,
    baz: bool,

export fn gux(foo: Foo) { ... };
Enter fullscreen mode Exit fullscreen mode

In exports.h this becomes:

#include <stddef.h>
#include <stdbool.h>
#include <stdint.h>

union Foo {
    uint64_t bar;
    bool baz;

void gux(union Foo foo);
Enter fullscreen mode Exit fullscreen mode


Arrays are fairly straightforward to translate to C.

const Foo = [4]u8;
export fn bar(x: [2]bool) void {...}
Enter fullscreen mode Exit fullscreen mode
#include <stddef.h>
#include <stdbool.h>
#include <stdint.h>

typedef uint8_t [4] Foo;
void bar(bool [2] x);
Enter fullscreen mode Exit fullscreen mode


Pointers also are fairly straightforward with only one caveat: C doesn't understand Zig slices so you will have to "unpack" them and pass them as two separate ptrand len arguments.

Zig has many pointer types useful to interoperate with C. I won't go over each one of them in this section so, if you want to learn more, take a look at these other posts.

First of all, make sure to read carefully the language reference

I also gave a talk on some pointer basics:

If you need more examples, check out these articles

export fn foo(str: [*:0]const u8) void {...}
export fn bar(buf: [*]u8, len: usize) void {...}
const Baz = extern struct { x: usize };
export fn touchaDaStruct(baz: *Baz) void {...}
Enter fullscreen mode Exit fullscreen mode
#include <stddef.h>
#include <stdbool.h>
#include <stdint.h>

void foo(const char *str);
void bar(char *buf, size_t len) ;
struct Baz {
    size_t x;
void touchaDaStruct(struct Baz *baz);
Enter fullscreen mode Exit fullscreen mode

C to Swift and Swift to C

Once we're done exporting our Zig types to C, we then need to load them from Swift.

I won't give you the same table I did in the previous section because many of these conversions are automatic or discoverable by interacting with the IDE. All you have to do is add the declarations in exports.h and you'll immediately be able to see how Xcode auto-completes their mentions in Swift code. I personally think this is a great way to explore this domain.

Additionally, you can see the full listing of how Swift automatically converts C types by selecting exports.h and pressing the tiny button in the top-left corner of the main section will open a contextual menu where you can select Generated Interfaces.

magic corner button

Passing a string to Swift

Now we're ready to mess around with our sample app a bit more. Let's start by implementing and exporting a new Zig function.

// main.zig
export fn helloFromZig() [*:0]const u8 {
   return "All your codebase are belong to us.";
Enter fullscreen mode Exit fullscreen mode

Then let's add it to our exports.h file.

const char *helloFromZig(void);
Enter fullscreen mode Exit fullscreen mode

Now we can invoke it from Swift. Unfortunately, we can't immediately nest the call directly inside of Text() because it expects a Swift string and the function returns a UnsafePointer<CChar> (Swift's equivalent of const char *).

That's a bit of a shame because the Zig type would have given enough information to Swift to know how to make a String out of it, because [*:0]const u8 is a constant pointer:

  • to many items, but C makes no distinction between pointers-to-one and pointers-to-many,
  • with a null byte terminator, but C doesn't encode this information in the type system despite making extensive use of null-terminated strings.

That said, Swift got us covered in this case. A quick look at the various initializers for String will show you one that looks really good: String(cString: UnsafePointer<CChar>).

Xcode autocomplete suggestions

Text(String(cString: helloFromZig()))
Enter fullscreen mode Exit fullscreen mode

end result part 1

With this last fix applied, we're able to build the application successfully and bask in the light of our achievement. Everything seems perfect, but like it's often the case with TV series, this is when a new evil creeps in from the shadows… the evil of semi-manual memory management!

Next steps

In this article we learned how to convert the main Zig types to equivalent C types and then successfully passed a string to Swift. While we made some progress compared to passing simple integers around, this was still an easy example because we used a string literal, which doesn't require manual memory management. Here you can find more information why that's the case:

But in real world use cases we'll have to deal with memory that at one point will need to be freed and we'll also have to find a way to collaborate with Swift, since it too wants to manage memory.

In the next part we're going to keep climbing the complexity ladder and see what we need to do to manage the full lifecycle of an image buffer, while also learning about QOI.

Discussion (0)