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(¤t_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_HHandling 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.