FFI means foreign function interface. It is the boundary where Zig calls code written in another language, or where another language calls Zig.
FFI means foreign function interface. It is the boundary where Zig calls code written in another language, or where another language calls Zig.
Most often, this means C.
Zig can call C functions directly.
const c = @cImport({
@cInclude("stdio.h");
});
pub fn main() void {
_ = c.puts("hello from C");
}@cImport asks Zig to import C declarations. @cInclude includes a C header.
The call:
c.puts("hello from C");calls the C function puts.
This looks simple, but the boundary is unsafe. C does not carry Zig’s safety rules with it. A C function may expect a null-terminated string. It may store a pointer. It may write into a buffer. It may return null. It may use global state. It may require the caller to free memory in a particular way.
The Zig side must express those rules clearly.
A common mistake is passing a normal slice to C.
const msg: []const u8 = "hello";A slice has a pointer and a length. C usually expects a pointer only.
For C strings, use a sentinel-terminated value.
const msg: [:0]const u8 = "hello";
_ = c.puts(msg);The type:
[:0]const u8means a slice of bytes ending with a zero byte.
That matches the usual C string convention.
When passing a buffer to C, pass both pointer and length if the C API accepts both.
extern fn fill(buf: [*]u8, len: usize) c_int;
pub fn main() void {
var buffer: [128]u8 = undefined;
const rc = fill(&buffer, buffer.len);
_ = rc;
}The pointer type:
[*]u8has no length. The length must be passed separately.
The C function can write past the end if the length is wrong. Zig cannot prevent that once control passes to C.
Null pointers must be handled explicitly.
extern fn find_name(id: c_int) ?[*:0]const u8;The return type:
?[*:0]const u8means the function returns either a null pointer or a pointer to a zero-terminated string.
Use an optional branch before reading it.
const p = find_name(10);
if (p) |name| {
std.debug.print("{s}\n", .{name});
} else {
std.debug.print("not found\n", .{});
}A C pointer does not tell you who owns the memory.
This must be learned from the C API.
Some returned pointers are borrowed:
extern fn getenv(name: [*:0]const u8) ?[*:0]const u8;The caller must not free the result.
Some returned pointers are owned:
extern fn make_name() ?[*:0]u8;
extern fn free_name(p: [*:0]u8) void;The caller must later call free_name.
Zig code should make this rule hard to miss.
const name = make_name() orelse return error.OutOfMemory;
defer free_name(name);The defer places the release next to the acquisition.
C error handling also needs care.
Many C functions return an integer status.
extern fn open_device() c_int;Wrap it in a Zig function that returns an error union.
const std = @import("std");
extern fn open_device() c_int;
fn openDevice() !void {
const rc = open_device();
if (rc != 0) {
return error.OpenFailed;
}
}Now the rest of the Zig program can use try.
try openDevice();Keep C conventions at the boundary. Convert them to Zig conventions as soon as possible.
Callbacks require special attention.
const Callback = *const fn (value: c_int) callconv(.c) void;
extern fn set_callback(cb: Callback) void;The calling convention:
callconv(.c)says that the function uses the C ABI.
A Zig callback passed to C must use the ABI the C code expects.
fn onValue(value: c_int) callconv(.c) void {
_ = value;
}Then:
set_callback(onValue);The callback must remain valid as long as C may call it. Function declarations are fine. Pointers to temporary data are not.
Struct layout is another boundary issue.
A normal Zig struct does not promise C layout.
Use extern struct for C-compatible layout.
const Point = extern struct {
x: c_int,
y: c_int,
};Use C integer types when matching C declarations.
c_int
c_uint
c_long
usizeDo not assume that C’s int is always i32 on every target. Use the translated C types or Zig’s C ABI types.
For bit-exact binary layouts, packed struct may be useful. For C ABI structs, use extern struct.
| Need | Type |
|---|---|
| C ABI layout | extern struct |
| Exact bit layout | packed struct |
| Zig-only data | struct |
Variadic C functions are another sharp edge.
_ = c.printf("x = %d\n", @as(c_int, 10));The argument types must match the C format string. Zig cannot fully check the meaning of a C format string.
Prefer Zig formatting except at the C boundary.
std.debug.print("x = {d}\n", .{10});The safest FFI design is thin at the edge and typed inside.
Poor style:
// C pointers and integer status codes spread everywhere.Better style:
// One small wrapper module owns the C interface.
// The rest of the program sees Zig types and Zig errors.A C call should be treated like unsafe code. Check nulls. Check lengths. Check ownership. Check calling conventions. Check layout.
FFI is useful because Zig is designed to cooperate with C. It is dangerous because C and Zig do not enforce the same rules.
Keep the boundary narrow.
Exercise 19-25. Import stdio.h and call puts.
Exercise 19-26. Write an extern struct that matches a C struct with two int fields.
Exercise 19-27. Wrap a C-style integer return code in a Zig function that returns !void.
Exercise 19-28. Explain why extern struct and packed struct solve different problems.