# C Interoperability Reference (Zig 0.16.0) Zig can export C-compatible APIs for use from any language that supports the C ABI: Swift, Objective-C, Python, Ruby, Rust, etc. This enables architectures like Ghostty (93% Zig business logic + 4% platform-native GUI). Primary Zig 0.16 release-note source: https://ziglang.org/download/0.16.0/release-notes.html Zig 0.16 translate-c is Aro/translate-c based rather than libclang based. `@cImport` is deprecated as the long-term API; prefer `b.addTranslateC(...)` in build scripts and import `translate_c.createModule()`. For ABI-sensitive bindings, keep comptime assertions for `@sizeOf`, `@alignOf`, field offsets, enum/flag values, and calling conventions. Treat generated binding differences as high-risk until checked. ## Table of Contents - [Quick Start](#quick-start) - [Exporting Functions](#exporting-functions) - [C-Compatible Types](#c-compatible-types) - [Building C Libraries](#building-c-libraries) - [Creating Header Files](#creating-header-files) - [macOS Integration](#macos-integration) - [Swift Integration](#swift-integration) - [Common Patterns](#common-patterns) ## Quick Start Minimal C-compatible library: **src/lib.zig:** ```zig const std = @import("std"); // Global state (opaque to C consumers). This minimal example is not // thread-safe; init must be called exactly once before get/set/deinit. var context: ?*Context = null; const Context = struct { allocator: std.mem.Allocator, value: i32, }; /// Initialize the library. Returns 0 on success, -1 on failure. export fn mylib_init() c_int { if (context != null) return -1; const gpa = std.heap.c_allocator; context = gpa.create(Context) catch return -1; context.?.* = .{ .allocator = gpa, .value = 0 }; return 0; } /// Clean up resources. export fn mylib_deinit() void { if (context) |ctx| { ctx.allocator.destroy(ctx); context = null; } } /// Get the current value. export fn mylib_get_value() c_int { return if (context) |ctx| ctx.value else 0; } /// Set the value. export fn mylib_set_value(v: c_int) void { if (context) |ctx| ctx.value = v; } ``` **build.zig:** ```zig const std = @import("std"); pub fn build(b: *std.Build) void { const target = b.standardTargetOptions(.{}); const optimize = b.standardOptimizeOption(.{}); const lib = b.addLibrary(.{ .name = "mylib", .linkage = .static, .root_module = b.createModule(.{ .root_source_file = b.path("src/lib.zig"), .target = target, .optimize = optimize, .link_libc = true, }), }); b.installArtifact(lib); // Install header alongside library b.installFile("include/mylib.h", "include/mylib.h"); } ``` **include/mylib.h:** ```c #ifndef MYLIB_H #define MYLIB_H #ifdef __cplusplus extern "C" { #endif int mylib_init(void); void mylib_deinit(void); int mylib_get_value(void); void mylib_set_value(int v); #ifdef __cplusplus } #endif #endif /* MYLIB_H */ ``` ## Exporting Functions ### `export` Keyword The `export` keyword creates a function with C ABI linkage: ```zig // Creates symbol "add" with C calling convention export fn add(a: c_int, b: c_int) c_int { return a + b; } ``` Equivalent to: ```zig fn add(a: c_int, b: c_int) callconv(.c) c_int { return a + b; } comptime { @export(&add, .{ .name = "add" }); } ``` ### Custom Symbol Names Use `@export` for custom symbol names: ```zig fn zigAdd(a: c_int, b: c_int) callconv(.c) c_int { return a + b; } comptime { @export(&zigAdd, .{ .name = "mylib_add" }); // Symbol: mylib_add } ``` ### Calling Convention For internal C-callable functions (not exported): ```zig // C calling convention, but not exported as symbol fn internalCallback(data: ?*anyopaque) callconv(.c) void { // Called by C code via function pointer } ``` ### Restrictions on Exported Functions Exported function signatures are limited to C-compatible constructs: **Allowed:** - C integer types: `c_int`, `c_uint`, `c_long`, `c_ulong`, `c_char`, etc. - Fixed-width integers matching C: `i8`, `i16`, `i32`, `i64`, `u8`, `u16`, `u32`, `u64` - Floating point: `f32` (`float`), `f64` (`double`) - Pointers: `*T`, `[*]T`, `[*c]T`, `?*T` - `bool` (maps to C `_Bool`) - `void` - `usize`, `isize` (map to `size_t`, `ptrdiff_t`) **Not allowed in signatures:** - Comptime parameters - Generic types (`anytype`) - Zig error unions (`!T`) - Zig optionals (except optional pointers `?*T`) - Slices (`[]T`) - use pointer + length instead - Non-extern structs/unions/enums - Arbitrary bit-width integers (`u3`, `i47`) **Inside the function body**, all Zig features work: ```zig export fn process(data: [*]const u8, len: usize) c_int { // Inside: full Zig features const slice = data[0..len]; for (slice) |byte| { if (byte == 0) return -1; } return @intCast(slice.len); } ``` ## C-Compatible Types ### Integer Type Mapping | Zig Type | C Type | Notes | |----------|--------|-------| | `c_char` | `char` | Signed or unsigned (platform-dependent) | | `c_short` | `short` | | | `c_int` | `int` | | | `c_long` | `long` | 32-bit on Windows, 64-bit elsewhere | | `c_longlong` | `long long` | | | `c_uchar` | `unsigned char` | | | `c_ushort` | `unsigned short` | | | `c_uint` | `unsigned int` | | | `c_ulong` | `unsigned long` | | | `c_ulonglong` | `unsigned long long` | | | `usize` | `size_t` | | | `isize` | `ptrdiff_t` | | | `i8`/`u8` | `int8_t`/`uint8_t` | | | `i16`/`u16` | `int16_t`/`uint16_t` | | | `i32`/`u32` | `int32_t`/`uint32_t` | | | `i64`/`u64` | `int64_t`/`uint64_t` | | ### Pointer Type Mapping | Zig Type | C Equivalent | Notes | |----------|--------------|-------| | `*T` | `T*` | Non-null pointer | | `?*T` | `T*` | Nullable pointer | | `[*]T` | `T*` | Many-item pointer | | `[*c]T` | `T*` | C pointer (nullable, arithmetic allowed) | | `*const T` | `const T*` | Const pointer | ### Extern Structs For structs passed across FFI boundary: ```zig // Extern struct: C-compatible layout pub const Point = extern struct { x: f64, y: f64, }; // Can be passed by value or pointer export fn distance(a: Point, b: Point) f64 { const dx = a.x - b.x; const dy = a.y - b.y; return @sqrt(dx * dx + dy * dy); } ``` ### Extern Unions ```zig pub const Value = extern union { i: c_int, f: f32, p: ?*anyopaque, }; ``` ### Extern Enums ```zig // Specify backing type for C compatibility pub const Status = enum(c_int) { ok = 0, err_invalid = -1, err_nomem = -2, }; export fn get_status() Status { return .ok; } ``` ## Building C Libraries ### Static Library ```zig const lib = b.addLibrary(.{ .name = "mylib", .linkage = .static, // Creates libmylib.a .root_module = b.createModule(.{ .root_source_file = b.path("src/lib.zig"), .target = target, .optimize = optimize, .link_libc = true, // Required by c_allocator or direct libc calls. }), }); b.installArtifact(lib); ``` ### Dynamic/Shared Library ```zig const lib = b.addLibrary(.{ .name = "mylib", .linkage = .dynamic, // Creates libmylib.so / libmylib.dylib / mylib.dll .root_module = b.createModule(.{ .root_source_file = b.path("src/lib.zig"), .target = target, .optimize = optimize, .link_libc = true, }), .version = .{ .major = 1, .minor = 0, .patch = 0 }, }); b.installArtifact(lib); ``` ### Cross-Compilation Build for specific targets: ```zig // Build for Apple Silicon Mac const mac_arm = b.resolveTargetQuery(.{ .cpu_arch = .aarch64, .os_tag = .macos, }); const lib = b.addLibrary(.{ .name = "mylib", .linkage = .static, .root_module = b.createModule(.{ .root_source_file = b.path("src/lib.zig"), .target = mac_arm, .optimize = .ReleaseFast, }), }); ``` ### Multi-Target Build ```zig const targets = [_]std.Target.Query{ .{ .cpu_arch = .x86_64, .os_tag = .macos }, .{ .cpu_arch = .aarch64, .os_tag = .macos }, .{ .cpu_arch = .x86_64, .os_tag = .linux, .abi = .gnu }, .{ .cpu_arch = .aarch64, .os_tag = .linux, .abi = .gnu }, }; for (targets) |t| { const resolved = b.resolveTargetQuery(t); const lib = b.addLibrary(.{ .name = b.fmt("mylib-{s}-{s}", .{ @tagName(t.cpu_arch.?), @tagName(t.os_tag.?), }), .linkage = .static, .root_module = b.createModule(.{ .root_source_file = b.path("src/lib.zig"), .target = resolved, .optimize = .ReleaseFast, }), }); b.installArtifact(lib); } ``` ## Creating Header Files Zig does not auto-generate C headers. Write them manually to match exported symbols. ### Header Template ```c #ifndef MYLIB_H #define MYLIB_H #include #include #include #ifdef __cplusplus extern "C" { #endif /* Opaque handle type */ typedef struct mylib_context mylib_context_t; /* Lifecycle */ mylib_context_t* mylib_create(void); void mylib_destroy(mylib_context_t* ctx); /* Operations */ int mylib_process(mylib_context_t* ctx, const uint8_t* data, size_t len); const char* mylib_get_error(mylib_context_t* ctx); /* Callback type */ typedef void (*mylib_callback_t)(void* user_data, int result); void mylib_set_callback(mylib_context_t* ctx, mylib_callback_t cb, void* user_data); #ifdef __cplusplus } #endif #endif /* MYLIB_H */ ``` ### Matching Zig Implementation ```zig const std = @import("std"); pub const Context = struct { allocator: std.mem.Allocator, // Storage is owned by this context and remains valid until the next // operation that changes the error or until mylib_destroy. error_msg: ?[:0]const u8 = null, callback: ?Callback = null, const Callback = struct { func: *const fn (?*anyopaque, c_int) callconv(.c) void, user_data: ?*anyopaque, }; }; export fn mylib_create() ?*Context { const allocator = std.heap.c_allocator; const ctx = allocator.create(Context) catch return null; ctx.* = .{ .allocator = allocator }; return ctx; } export fn mylib_destroy(ctx: ?*Context) void { if (ctx) |c| { c.allocator.destroy(c); } } export fn mylib_process(ctx: ?*Context, data: [*]const u8, len: usize) c_int { const c = ctx orelse return -1; const slice = data[0..len]; // Process data... _ = slice; if (c.callback) |cb| { cb.func(cb.user_data, 0); } return 0; } export fn mylib_get_error(ctx: ?*Context) [*:0]const u8 { const c = ctx orelse return "null context"; return if (c.error_msg) |msg| msg.ptr else "no error"; } export fn mylib_set_callback( ctx: ?*Context, cb: ?*const fn (?*anyopaque, c_int) callconv(.c) void, user_data: ?*anyopaque, ) void { if (ctx) |c| { c.callback = if (cb) |f| .{ .func = f, .user_data = user_data } else null; } } ``` ## macOS Integration ### Universal Binaries (Fat Binaries) Build for both architectures and combine with `lipo`: **build.zig:** ```zig pub fn build(b: *std.Build) void { const optimize = b.standardOptimizeOption(.{}); // Build for both architectures const arm64 = b.resolveTargetQuery(.{ .cpu_arch = .aarch64, .os_tag = .macos }); const x86_64 = b.resolveTargetQuery(.{ .cpu_arch = .x86_64, .os_tag = .macos }); const lib_arm64 = b.addLibrary(.{ .name = "mylib", .linkage = .static, .root_module = b.createModule(.{ .root_source_file = b.path("src/lib.zig"), .target = arm64, .optimize = optimize, }), }); const lib_x86_64 = b.addLibrary(.{ .name = "mylib", .linkage = .static, .root_module = b.createModule(.{ .root_source_file = b.path("src/lib.zig"), .target = x86_64, .optimize = optimize, }), }); // Use lipo to create universal binary const lipo = b.addSystemCommand(&.{ "lipo", "-create", "-output", }); const universal_lib = lipo.addOutputFileArg("libmylib.a"); lipo.addFileArg(lib_arm64.getEmittedBin()); lipo.addFileArg(lib_x86_64.getEmittedBin()); // Install universal binary const install = b.addInstallFile(universal_lib, "lib/libmylib.a"); const universal_step = b.step("universal", "Build universal binary"); universal_step.dependOn(&install.step); } ``` **Manual lipo usage:** ```bash # Build each architecture zig build -Dtarget=aarch64-macos -Doptimize=ReleaseFast mv zig-out/lib/libmylib.a libmylib-arm64.a zig build -Dtarget=x86_64-macos -Doptimize=ReleaseFast mv zig-out/lib/libmylib.a libmylib-x86_64.a # Combine into universal binary lipo -create -output libmylib.a libmylib-arm64.a libmylib-x86_64.a # Verify architectures lipo -info libmylib.a ``` ### XCFramework Creation XCFrameworks are the modern way to distribute libraries for Apple platforms: ```bash # 1. Build universal library (see above) # 2. Create directory structure mkdir -p MyLib.xcframework/macos-arm64_x86_64/Headers # 3. Copy library and headers cp libmylib.a MyLib.xcframework/macos-arm64_x86_64/ cp include/mylib.h MyLib.xcframework/macos-arm64_x86_64/Headers/ # 4. Create module map cat > MyLib.xcframework/macos-arm64_x86_64/Headers/module.modulemap << 'EOF' module MyLib { umbrella header "mylib.h" export * } EOF # 5. Create Info.plist cat > MyLib.xcframework/Info.plist << 'EOF' AvailableLibraries HeadersPath Headers LibraryIdentifier macos-arm64_x86_64 LibraryPath libmylib.a SupportedArchitectures arm64 x86_64 SupportedPlatform macos CFBundlePackageType XFWK XCFrameworkFormatVersion 1.0 EOF ``` **Using xcodebuild (simpler):** ```bash xcodebuild -create-xcframework \ -library libmylib.a \ -headers include/ \ -output MyLib.xcframework ``` ## Swift Integration ### Module Map Create `module.modulemap` alongside your header: ```c module MyLib { umbrella header "mylib.h" export * } ``` ### Using from Swift ```swift import MyLib // Use C functions directly let result = mylib_init() if result == 0 { mylib_set_value(42) print("Value: \(mylib_get_value())") mylib_deinit() } ``` ### Swift-Friendly Wrapper ```swift import MyLib class MyLibWrapper { private var initialized = false init?() { guard mylib_init() == 0 else { return nil } initialized = true } deinit { if initialized { mylib_deinit() } } var value: Int32 { get { mylib_get_value() } set { mylib_set_value(newValue) } } } ``` ### Xcode Project Integration 1. Drag `MyLib.xcframework` into Xcode project 2. Ensure "Embed & Sign" or "Do Not Embed" (for static libs) 3. Import module: `import MyLib` For static libraries without XCFramework: 1. Add library to "Link Binary With Libraries" 2. Add header path to "Header Search Paths" 3. Create bridging header if not using module map ### Improving Swift Interop (Advanced) For better Swift projection, use API notes (`.apinotes` files): **MyLib.apinotes:** ```yaml Name: MyLib Functions: - Name: mylib_create SwiftName: "MyLibContext.create()" NullabilityOfRet: O # Nullable C pointer; imported as Optional in Swift - Name: mylib_destroy SwiftName: "MyLibContext.destroy(self:)" - Name: mylib_get_error NullabilityOfRet: N ResultType: "const char * _Nonnull" ``` See [Swift.org: Improving the Usability of C APIs](https://www.swift.org/documentation/cxx-interop/) for more. ## Common Patterns ### Opaque Pointers Hide implementation details from C consumers: ```zig const std = @import("std"); const InternalState = struct { allocator: std.mem.Allocator, data: std.ArrayList(u8), // Complex internal state... }; // C sees: typedef struct handle handle_t; // (opaque, can't access fields) export fn handle_create() ?*InternalState { const allocator = std.heap.c_allocator; const state = allocator.create(InternalState) catch return null; state.* = .{ .allocator = allocator, .data = std.ArrayList(u8).init(allocator), }; return state; } export fn handle_destroy(h: ?*InternalState) void { if (h) |state| { state.data.deinit(); state.allocator.destroy(state); } } ``` ### Error Handling Across FFI Zig errors can't cross FFI boundary. Use return codes or out parameters: ```zig pub const ErrorCode = enum(c_int) { ok = 0, invalid_argument = -1, out_of_memory = -2, io_error = -3, unknown = -99, }; export fn process_data( data: [*]const u8, len: usize, out_result: *c_int, ) ErrorCode { const slice = data[0..len]; // Internal Zig code can use errors const result = processInternal(slice) catch |err| { return switch (err) { error.OutOfMemory => .out_of_memory, error.InvalidData => .invalid_argument, else => .unknown, }; }; out_result.* = result; return .ok; } fn processInternal(data: []const u8) !c_int { // Full Zig error handling here if (data.len == 0) return error.InvalidData; return @intCast(data.len); } ``` ### Callbacks C callbacks with user data: ```zig const CallbackFn = *const fn ( user_data: ?*anyopaque, event_type: c_int, event_data: ?*const anyopaque, ) callconv(.c) void; var stored_callback: ?CallbackFn = null; var stored_user_data: ?*anyopaque = null; export fn register_callback(cb: ?CallbackFn, user_data: ?*anyopaque) void { stored_callback = cb; stored_user_data = user_data; } export fn trigger_event(event_type: c_int) void { if (stored_callback) |cb| { cb(stored_user_data, event_type, null); } } ``` ### String Handling Zig slices vs C strings: ```zig const std = @import("std"); // Accept C string, return length export fn string_length(s: [*:0]const u8) usize { return std.mem.len(s); } // Accept pointer + length (more efficient) export fn process_string(s: [*]const u8, len: usize) c_int { const slice = s[0..len]; // Process slice... _ = slice; return 0; } // Return C string (must be static or allocated) const greeting: [:0]const u8 = "Hello from Zig!"; export fn get_greeting() [*:0]const u8 { return greeting.ptr; } // Allocate a sentinel-terminated string. The caller must pass the same // payload length to free_string; the sentinel is stored at index len. export fn alloc_string(len: usize) ?[*:0]u8 { const allocator = std.heap.c_allocator; const buf = allocator.allocSentinel(u8, len, 0) catch return null; return buf.ptr; } export fn free_string(s: ?[*:0]u8, len: usize) void { if (s) |ptr| { const allocator = std.heap.c_allocator; allocator.free(ptr[0..len :0]); } } ``` ### Thread Safety For simple exported counters, atomics avoid the need to expose an I/O context across the C ABI. More complex synchronization in Zig 0.16 uses `std.Io.Mutex`, whose lock operations take an `std.Io` value; design that runtime context into the library rather than using the removed `std.Thread.Mutex` API. ```zig const std = @import("std"); var atomic_counter: std.atomic.Value(c_int) = .init(0); export fn atomic_increment() c_int { return atomic_counter.fetchAdd(1, .seq_cst) + 1; } ``` ### Versioning Export version info for runtime checking: ```zig pub const version_major: c_int = 1; pub const version_minor: c_int = 2; pub const version_patch: c_int = 3; comptime { @export(&version_major, .{ .name = "mylib_version_major" }); @export(&version_minor, .{ .name = "mylib_version_minor" }); @export(&version_patch, .{ .name = "mylib_version_patch" }); } export fn mylib_version_string() [*:0]const u8 { return "1.2.3"; } ``` **Header:** ```c extern const int mylib_version_major; extern const int mylib_version_minor; extern const int mylib_version_patch; const char* mylib_version_string(void); ```