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.
Numbers
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;
}
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);
Enums
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 { ... };
enum Foo { a, b, c };
void bar(enum Foo foo);
Structs
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 { ... };
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);
Unions
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) { ... };
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);
Arrays
Arrays are fairly straightforward to translate to C.
const Foo = [4]u8;
export fn bar(x: [2]bool) void {...}
#include <stddef.h>
#include <stdbool.h>
#include <stdint.h>
typedef uint8_t [4] Foo;
void bar(bool [2] x);
Pointers
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 ptr
and 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
https://ziglang.org/documentation/master/#Pointers
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 {...}
#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);
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
.
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.";
}
Then let's add it to our exports.h
file.
const char *helloFromZig(void);
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>)
.
Text(String(cString: helloFromZig()))
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.
Top comments (0)