Chapter 09

C 互操作:调用与封装 C 库

Zig 与 C 语言的无缝互操作,利用庞大的 C 生态系统

C 互操作的优势

Zig 的 C 互操作是一等公民特性,不需要任何 FFI(Foreign Function Interface)桥接层。直接导入 C 头文件,Zig 编译器会解析 C 的类型定义并将其转换为对应的 Zig 类型。这意味着你可以无缝使用 libc、OpenSSL、SQLite、libcurl 等任何 C 库。

@cImport 与 @cInclude

const std = @import("std");
// @cImport 导入 C 头文件,返回一个"模块"
const c = @cImport({
    @cInclude("stdio.h");
    @cInclude("string.h");
    @cInclude("stdlib.h");
    @cDefine("_GNU_SOURCE", "");  // 等价于 #define
});

pub fn main() void {
    // 调用 C 标准库函数
    _ = c.printf("Hello from C! %d\n", @as(c_int, 42));

    // C 字符串操作
    const str = "Hello";
    const len = c.strlen(str);
    std.debug.print("strlen = {d}\n", .{len});

    // malloc/free(虽然通常用 Zig Allocator)
    const buf = c.malloc(256) orelse std.process.exit(1);
    defer c.free(buf);
}

C 类型映射

c_int
C 的 int,通常是 32 位有符号整数。对应 Zig 的 i32(大多数平台)。
c_uint
C 的 unsigned int
c_long / c_ulong
C 的 long,大小平台相关(32 或 64 位)。
c_longlong
C 的 long long,64 位有符号整数。
c_char
C 的 char,可能有符号也可能无符号(平台相关)。
c_void
C 的 void,在 Zig 中用于 *c_void(即 C 的 void*)。
c_float / c_double
C 的 floatdouble,对应 Zig 的 f32/f64。
c_size_t
C 的 size_t,平台相关无符号整数,对应 Zig 的 usize。

链接 C 库

在 build.zig 中链接

// build.zig
pub fn build(b: *std.Build) void {
    const target = b.standardTargetOptions(.{});
    const optimize = b.standardOptimizeOption(.{});

    const exe = b.addExecutable(.{
        .name = "myapp",
        .root_source_file = b.path("src/main.zig"),
        .target = target,
        .optimize = optimize,
    });

    // 链接 libc(标准 C 库)
    exe.linkLibC();

    // 链接系统库(通过 pkg-config 查找)
    exe.linkSystemLibrary("curl");   // libcurl
    exe.linkSystemLibrary("ssl");    // OpenSSL
    exe.linkSystemLibrary("sqlite3"); // SQLite

    // 添加头文件搜索路径
    exe.addIncludePath(b.path("third_party/include"));

    // 链接静态库文件
    exe.addLibraryPath(b.path("third_party/lib"));
    exe.linkSystemLibrary("mystaticlib");

    b.installArtifact(exe);
}

调用 libcurl 示例

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

// libcurl 写入回调函数
fn writeCallback(
    ptr: ?*anyopaque,
    size: c_size_t,
    nmemb: c_size_t,
    userdata: ?*anyopaque,
) callconv(.C) c_size_t {
    const data = @as([*]u8, @ptrCast(ptr));
    const buf = @as(*std.ArrayList(u8), @ptrCast(@alignCast(userdata)));
    const total = size * nmemb;
    buf.appendSlice(data[0..total]) catch return 0;
    return total;
}

pub fn httpGet(allocator: std.mem.Allocator, url: []const u8) ![]u8 {
    // 初始化 curl
    _ = c.curl_global_init(c.CURL_GLOBAL_DEFAULT);
    defer c.curl_global_cleanup();

    const curl = c.curl_easy_init() orelse return error.CurlInitFailed;
    defer c.curl_easy_cleanup(curl);

    var response = std.ArrayList(u8).init(allocator);
    errdefer response.deinit();

    // 设置 URL(需要 null-terminated)
    const url_z = try allocator.dupeZ(u8, url);
    defer allocator.free(url_z);

    _ = c.curl_easy_setopt(curl, c.CURLOPT_URL, url_z.ptr);
    _ = c.curl_easy_setopt(curl, c.CURLOPT_WRITEFUNCTION, writeCallback);
    _ = c.curl_easy_setopt(curl, c.CURLOPT_WRITEDATA, &response);

    // 执行请求
    const res = c.curl_easy_perform(curl);
    if (res != c.CURLE_OK) return error.CurlPerformFailed;

    return response.toOwnedSlice();
}

extern fn:声明外部 C 函数

// 不用 @cImport,手动声明 C 函数原型
extern fn strlen(s: [*:0]const u8) usize;
extern fn memcpy(dst: *anyopaque, src: *const anyopaque, n: usize) *anyopaque;
extern fn malloc(size: usize) ?*anyopaque;
extern fn free(ptr: ?*anyopaque) void;

pub fn main() void {
    const s: [*:0]const u8 = "Hello";
    std.debug.print("len = {d}\n", .{strlen(s)});
}

zig cc:作为 C 编译器

# zig cc 可以替代 gcc/clang 编译 C 文件
zig cc -o hello hello.c

# 交叉编译 C 代码到 Windows
zig cc -target x86_64-windows-gnu -o hello.exe hello.c

# 编译 C++ 代码
zig c++ -o myapp main.cpp

# 与 CMake 集成(设置编译器变量)
CC="zig cc" CXX="zig c++" cmake ..

# 在 Python 的 build 系统中使用(加速 C 扩展编译)
CC="zig cc" pip install numpy

# 生成静态链接的 Linux 二进制(在 macOS 上!)
zig cc -target x86_64-linux-musl -static -o server server.c
zig cc 的强大之处

zig cc 内置了 musl libc 和所有主要平台的系统库,不需要安装交叉编译工具链。在 macOS 上用 zig cc -target x86_64-linux-musl 就能生成能在任何 Linux 上运行的静态二进制,这在以前需要 Docker 或 crosstool-ng 才能实现。

C 互操作的工作原理

ABI 兼容性:Zig 如何与 C 代码对话

Zig 与 C 互操作的基础是 ABI(Application Binary Interface)兼容性。理解这个机制能帮助你正确处理数据类型边界:

extern 函数的调用约定(Calling Convention)
extern fn foo(x: i32) i32 声明的函数使用 C 调用约定(cdecl/System V AMD64 ABI 等平台相关标准):参数通过寄存器和栈传递,返回值通过寄存器返回。Zig 的默认调用约定与 C 不同(zig 调用约定允许更多优化),所以跨语言调用必须用 extern(C 约定)。
C 类型到 Zig 类型的映射
Zig 提供了与 C 类型精确对应的类型(在 std.c 命名空间):c_int(C int,平台相关大小)、c_longc_ulongc_size_t 等。对于结构体,Zig 的 extern struct(与 C packed struct 等价)保证与 C struct 相同的内存布局,而普通 Zig struct 可能有不同的字段排序。
@cImport 的工作方式
@cImport({ @cInclude("stdio.h"); }) 在编译时调用 Zig 的 C 翻译器(基于 Clang),将 C 头文件翻译为 Zig 的类型和函数声明。翻译结果在编译期缓存,所以不影响构建速度。复杂的宏可能翻译失败,需要手动声明。
anyopaque 是 C 的 void*
*anyopaque 等价于 C 的 void *——指向未知类型的指针。用于 C API 中传递"上下文指针"(callback 的 user_data 参数)。在 Zig 中使用时,需要 @ptrCast 将其转换为具体类型。

zig cc 的跨编译原理

传统跨编译流程(复杂):
  主机: macOS x86_64
  目标: Linux aarch64
  需要: 安装 aarch64-linux-gnu-gcc 工具链
       下载 Linux aarch64 的 sysroot(系统头文件和库)
       配置 PATH、CC、CXX 等环境变量
       可能遇到 glibc 版本兼容问题

zig cc 跨编译(简单):
  zig cc -target aarch64-linux-musl hello.c -o hello-arm64-linux
  完成!Zig 内置了:
  ├── LLVM 跨平台代码生成后端
  ├── musl libc(所有主流目标平台)
  ├── mingw-w64(Windows 目标)
  └── macOS SDK(macOS 目标,需要 SDK 文件)

musl vs glibc:
  glibc:大多数 Linux 发行版默认 C 库,动态链接,版本依赖
  musl:轻量级 C 库,静态链接友好,无版本依赖
  用 musl 构建的二进制可以在任何 Linux 上运行,无需预装 glibc
@cImport 的常见问题

封装 C 库:Zig 风格的安全包装器

封装 SQLite

好的 C 库封装不是简单地转发调用,而是将 C 的"错误码+输出参数"风格转换为 Zig 的错误联合类型,让调用者获得更好的使用体验。

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

// 将 SQLite 错误码转为 Zig 错误
const SqliteError = error{
    OpenFailed,
    PrepareFailed,
    StepFailed,
    BindFailed,
    NotFound,
};

// 封装 sqlite3* 句柄为 Zig 结构体
pub const Database = struct {
    db: *c.sqlite3,  // 内部持有 C 句柄(私有)

    pub fn open(path: []const u8, allocator: std.mem.Allocator) !Database {
        // SQLite 需要 null-terminated 字符串
        const path_z = try allocator.dupeZ(u8, path);
        defer allocator.free(path_z);

        var db: ?*c.sqlite3 = null;
        const rc = c.sqlite3_open(path_z.ptr, &db);
        if (rc != c.SQLITE_OK) return SqliteError.OpenFailed;

        return .{ .db = db.? };
    }

    pub fn close(self: *Database) void {
        _ = c.sqlite3_close(self.db);
    }

    // 执行不返回数据的 SQL(INSERT/UPDATE/DELETE/CREATE)
    pub fn exec(self: *Database, sql: []const u8, allocator: std.mem.Allocator) !void {
        const sql_z = try allocator.dupeZ(u8, sql);
        defer allocator.free(sql_z);

        var err_msg: ?[*:0]u8 = null;
        const rc = c.sqlite3_exec(self.db, sql_z.ptr, null, null, &err_msg);
        if (rc != c.SQLITE_OK) {
            if (err_msg) |msg| {
                std.debug.print("SQLite error: {s}\n", .{msg});
                c.sqlite3_free(msg);
            }
            return SqliteError.StepFailed;
        }
    }

    // 查询单行返回整数值
    pub fn queryInt(self: *Database, sql: []const u8, allocator: std.mem.Allocator) !i64 {
        const sql_z = try allocator.dupeZ(u8, sql);
        defer allocator.free(sql_z);

        var stmt: ?*c.sqlite3_stmt = null;
        var rc = c.sqlite3_prepare_v2(self.db, sql_z.ptr, -1, &stmt, null);
        if (rc != c.SQLITE_OK) return SqliteError.PrepareFailed;
        defer _ = c.sqlite3_finalize(stmt);

        rc = c.sqlite3_step(stmt);
        if (rc != c.SQLITE_ROW) return SqliteError.NotFound;

        return c.sqlite3_column_int64(stmt, 0);
    }
};

// 使用示例
pub fn example(allocator: std.mem.Allocator) !void {
    var db = try Database.open(":memory:", allocator);
    defer db.close();

    try db.exec("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)", allocator);
    try db.exec("INSERT INTO users VALUES (1, 'Alice')", allocator);

    const count = try db.queryInt("SELECT COUNT(*) FROM users", allocator);
    std.debug.print("User count: {d}\n", .{count});  // 1
}

导出 Zig 函数给 C 调用

不只是 Zig 调用 C,C 也可以调用 Zig。通过 export 关键字声明的函数会使用 C 调用约定并导出符号。

// lib.zig — 编译为动态库供 C 调用
const std = @import("std");

// export 使函数以 C 调用约定导出
export fn zig_add(a: c_int, b: c_int) c_int {
    return a + b;
}

// 导出处理字节数组的函数
export fn zig_reverse_bytes(data: [*]u8, len: usize) void {
    const slice = data[0..len];
    std.mem.reverse(u8, slice);
}

// 导出错误码风格的函数(与 C 约定兼容)
export fn zig_parse_int(s: [*:0]const u8, out: *i64) c_int {
    const slice = std.mem.sliceTo(s, 0);
    out.* = std.fmt.parseInt(i64, slice, 10) catch return -1;
    return 0;  // 成功
}
// build.zig 配置编译为动态库
pub fn build(b: *std.Build) void {
    const lib = b.addSharedLibrary(.{
        .name = "ziglib",
        .root_source_file = b.path("src/lib.zig"),
        .version = .{ .major = 1, .minor = 0, .patch = 0 },
        .target = b.standardTargetOptions(.{}),
        .optimize = b.standardOptimizeOption(.{}),
    });
    // 生成 C 头文件(Zig 0.13 中通过 installHeader 手动提供)
    b.installArtifact(lib);
}

null-terminated 字符串的处理

C 函数通常期望以 \0 结尾的字符串(*const c_char / char*),而 Zig 的字符串是不含终止符的切片。Zig 提供了专门的处理方式:

const std = @import("std");

pub fn demo(allocator: std.mem.Allocator) !void {
    // 字符串字面量自动以 null 结尾,类型 *const [5:0]u8
    const literal = "hello";
    _ = literal.ptr;  // 可直接作为 null-terminated 传给 C

    // Zig 切片转 null-terminated:dupeZ 分配新内存并添加 \0
    const s: []const u8 = "world";
    const s_z: [:0]u8 = try allocator.dupeZ(u8, s);
    defer allocator.free(s_z);
    // s_z.ptr 可以安全传给 C 函数

    // C 字符串转 Zig 切片:sliceTo 查找 \0 并截断
    const c_str: [*:0]const u8 = "c_string";
    const zig_slice = std.mem.sliceTo(c_str, 0);  // []const u8
    std.debug.print("len={d}\n", .{zig_slice.len});   // 8
}
C 互操作的最佳实践
本章核心要点