Skip to content

Zig C Interoperability

Zig was designed with seamless C language interoperability in mind. This chapter will cover how to use C libraries in Zig, call C functions, and expose Zig code to C.

C Interop Basics

Importing C Header Files

Zig can directly import and use C header files:

zig
const std = @import("std");
const c = @cImport({
    @cInclude("stdio.h");
    @cInclude("stdlib.h");
    @cInclude("string.h");
    @cInclude("math.h");
});

pub fn main() void {
    // Use C standard library functions
    _ = c.printf("Hello from C printf!\n");
    
    // Use C math functions
    const x: f64 = 3.14159;
    const sin_x = c.sin(x);
    const cos_x = c.cos(x);
    
    _ = c.printf("sin(%.5f) = %.5f\n", x, sin_x);
    _ = c.printf("cos(%.5f) = %.5f\n", x, cos_x);
    
    // Use C string functions
    var buffer: [100]u8 = undefined;
    _ = c.strcpy(&buffer, "Hello, C World!");
    _ = c.printf("String: %s\n", &buffer);
    _ = c.printf("Length: %zu\n", c.strlen(&buffer));
}

C Type Mapping

Zig provides types that correspond to C types:

zig
const std = @import("std");
const c = @cImport({
    @cInclude("stdint.h");
});

pub fn main() void {
    // C basic types
    var c_int: c_int = 42;
    var c_uint: c_uint = 42;
    var c_long: c_long = 1000000;
    var c_float: f32 = 3.14;
    var c_double: f64 = 3.141592653589793;
    
    // C character type
    var c_char: u8 = 'A';
    
    // C pointer type
    var c_ptr: [*c]u8 = null;
    
    std.debug.print("C type mapping example:\n");
    std.debug.print("c_int: {} (size: {} bytes)\n", .{ c_int, @sizeOf(c_int) });
    std.debug.print("c_uint: {} (size: {} bytes)\n", .{ c_uint, @sizeOf(c_uint) });
    std.debug.print("c_long: {} (size: {} bytes)\n", .{ c_long, @sizeOf(c_long) });
    std.debug.print("c_float: {d:.2} (size: {} bytes)\n", .{ c_float, @sizeOf(f32) });
    std.debug.print("c_double: {d:.10} (size: {} bytes)\n", .{ c_double, @sizeOf(f64) });
    std.debug.print("c_char: {c} (value: {})\n", .{ c_char, c_char });
    std.debug.print("c_ptr: {?*}\n", .{c_ptr});
}

Calling C Functions

Using C Standard Library

zig
const std = @import("std");
const c = @cImport({
    @cInclude("stdio.h");
    @cInclude("stdlib.h");
    @cInclude("time.h");
});

pub fn main() void {
    // Memory allocation
    const size = 10;
    const ptr = c.malloc(size * @sizeOf(c_int));
    defer c.free(ptr);
    
    if (ptr == null) {
        std.debug.print("Memory allocation failed\n", .{});
        return;
    }
    
    // Cast pointer to appropriate type
    const int_array: [*c]c_int = @ptrCast(@alignCast(ptr));
    
    // Initialize array
    for (0..size) |i| {
        int_array[i] = @intCast(i * i);
    }
    
    // Print array
    _ = c.printf("C allocated array:\n");
    for (0..size) |i| {
        _ = c.printf("array[%zu] = %d\n", i, int_array[i]);
    }
    
    // Use C time functions
    const current_time = c.time(null);
    const time_str = c.ctime(&current_time);
    _ = c.printf("Current time: %s", time_str);
}

Exporting Zig Functions to C

Export Zig Functions

zig
const std = @import("std");

// Export simple functions
export fn zig_add(a: c_int, b: c_int) c_int {
    return a + b;
}

export fn zig_multiply(a: c_double, b: c_double) c_double {
    return a * b;
}

// Export string processing function
export fn zig_string_length(str: [*:0]const u8) c_size_t {
    return std.mem.len(str);
}

// Export array processing function
export fn zig_sum_array(array: [*]const c_int, size: c_size_t) c_int {
    var sum: c_int = 0;
    for (0..size) |i| {
        sum += array[i];
    }
    return sum;
}

// Export struct operations
const Point = extern struct {
    x: c_int,
    y: c_int,
};

export fn zig_create_point(x: c_int, y: c_int) Point {
    return Point{ .x = x, .y = y };
}

export fn zig_point_distance_squared(p1: Point, p2: Point) c_int {
    const dx = p1.x - p2.x;
    const dy = p1.y - p2.y;
    return dx * dx + dy * dy;
}

// Export memory allocation functions
export fn zig_allocate_array(size: c_size_t) ?[*]c_int {
    const allocator = std.heap.c_allocator;
    const array = allocator.alloc(c_int, size) catch return null;
    return array.ptr;
}

export fn zig_free_array(ptr: [*]c_int, size: c_size_t) void {
    const allocator = std.heap.c_allocator;
    const slice = ptr[0..size];
    allocator.free(slice);
}

// Test function
pub fn main() void {
    std.debug.print("Zig function export example\n");
    
    // Test exported functions
    const sum = zig_add(10, 20);
    std.debug.print("zig_add(10, 20) = {}\n", .{sum});
    
    const product = zig_multiply(3.14, 2.0);
    std.debug.print("zig_multiply(3.14, 2.0) = {d:.2}\n", .{product});
    
    const test_string = "Hello, World!";
    const length = zig_string_length(test_string);
    std.debug.print("zig_string_length(\"{s}\") = {}\n", .{ test_string, length });
    
    const test_array = [_]c_int{ 1, 2, 3, 4, 5 };
    const array_sum = zig_sum_array(&test_array, test_array.len);
    std.debug.print("zig_sum_array([1,2,3,4,5]) = {}\n", .{array_sum});
    
    const p1 = zig_create_point(0, 0);
    const p2 = zig_create_point(3, 4);
    const dist_sq = zig_point_distance_squared(p1, p2);
    std.debug.print("Distance squared: {}\n", .{dist_sq});
}

Generate C Header File

You can generate a C header file for exported Zig functions:

c
// zig_exports.h
#ifndef ZIG_EXPORTS_H
#define ZIG_EXPORTS_H

#include <stddef.h>

#ifdef __cplusplus
extern "C" {
#endif

// Basic functions
int zig_add(int a, int b);
double zig_multiply(double a, double b);

// String functions
size_t zig_string_length(const char* str);

// Array functions
int zig_sum_array(const int* array, size_t size);

// Struct definition
typedef struct {
    int x;
    int y;
} Point;

// Struct functions
Point zig_create_point(int x, int y);
int zig_point_distance_squared(Point p1, Point p2);

// Memory management functions
int* zig_allocate_array(size_t size);
void zig_free_array(int* ptr, size_t size);

#ifdef __cplusplus
}
#endif

#endif // ZIG_EXPORTS_H

Handling C Strings

String Conversion

zig
const std = @import("std");
const c = @cImport({
    @cInclude("stdio.h");
    @cInclude("stdlib.h");
    @cInclude("string.h");
});

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();
    const allocator = gpa.allocator();
    
    // Zig string to C string
    const zig_string = "Hello from Zig!";
    const c_string = try allocator.dupeZ(u8, zig_string);
    defer allocator.free(c_string);
    
    _ = c.printf("C string: %s\n", c_string.ptr);
    
    // C string to Zig string
    const c_literal = "Hello from C!";
    const zig_from_c = std.mem.span(@as([*:0]const u8, @ptrCast(c_literal)));
    std.debug.print("Zig string: {s}\n", .{zig_from_c});
    
    // Use C string functions
    var buffer: [100]u8 = undefined;
    _ = c.strcpy(&buffer, "Initial string");
    _ = c.strcat(&buffer, " + appended");
    
    const final_string = std.mem.span(@as([*:0]u8, @ptrCast(&buffer)));
    std.debug.print("Concatenated string: {s}\n", .{final_string});
    
    // String comparison
    const str1 = "apple";
    const str2 = "banana";
    const c_str1 = try allocator.dupeZ(u8, str1);
    const c_str2 = try allocator.dupeZ(u8, str2);
    defer allocator.free(c_str1);
    defer allocator.free(c_str2);
    
    const cmp_result = c.strcmp(c_str1.ptr, c_str2.ptr);
    std.debug.print("strcmp(\"{s}\", \"{s}\") = {}\n", .{ str1, str2, cmp_result });
}

Best Practices

1. Type Safety

zig
const std = @import("std");
const c = @cImport({
    @cInclude("stdio.h");
});

// ✅ Use type-safe wrappers
fn safe_printf(comptime fmt: []const u8, args: anytype) void {
    const c_fmt = fmt ++ "\x00"; // Ensure null termination
    _ = @call(.auto, c.printf, .{c_fmt.ptr} ++ args);
}

// ✅ Check C function return values
fn safe_fopen(filename: []const u8, mode: []const u8, allocator: std.mem.Allocator) !?*c.FILE {
    const c_filename = try allocator.dupeZ(u8, filename);
    defer allocator.free(c_filename);
    
    const c_mode = try allocator.dupeZ(u8, mode);
    defer allocator.free(c_mode);
    
    const file = c.fopen(c_filename.ptr, c_mode.ptr);
    return file;
}

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();
    const allocator = gpa.allocator();
    
    // Use type-safe wrapper
    safe_printf("Safe printf: %d %s", .{ 42, "test" });
    
    // Safe file operations
    if (try safe_fopen("test.txt", "w", allocator)) |file| {
        defer _ = c.fclose(file);
        _ = c.fprintf(file, "Hello, safe C interop!\n");
    } else {
        std.debug.print("File open failed\n", .{});
    }
}

Summary

This chapter covered Zig's C language interoperability:

  • ✅ Importing and using C header files
  • ✅ C type mapping and function calls
  • ✅ Using third-party C libraries
  • ✅ Exporting Zig functions to C
  • ✅ C string handling
  • ✅ Error handling and debugging
  • ✅ Performance considerations and best practices

Zig's seamless C interoperability allows developers to gradually migrate existing C codebases or use mature C libraries in Zig projects. This interoperability is a significant advantage of Zig, providing great flexibility for systems programming.

In the next chapter, we'll learn about the concept of undefined behavior.

Content is for learning and research only.