第14章 C 语言互操作性
第14章 C 语言互操作性
Zig 的一个核心设计目标是与 C 语言的无缝互操作。
这不仅仅是“可以调用 C 代码”(很多语言都通过 FFI 支持),而是“直接理解 C 代码”。Zig 编译器内置了一个完整的 C 编译器(zig cc),并且能够直接解析 .h 头文件,将其中的类型、函数和宏直接暴露给 Zig 代码使用。
这意味着:你不需要为 C 库编写繁琐的绑定(Bindings)。
1. 导入 C 头文件
在 Zig 中,你可以使用 @cImport 内置函数来导入 C 头文件。这会在后台调用 translate-c 工具,将 C 的定义转换为 Zig 代码。
const std = @import("std");
// 导入 C 标准库的 stdio.h
const c = @cImport({
@cInclude("stdio.h");
});
pub fn main() void {
// 直接调用 C 函数 printf
_ = c.printf("Hello from C!\n");
}
常用指令
@cInclude("file.h"):包含一个头文件。@cDefine("MACRO", "VALUE"):定义一个 C 宏(在包含头文件之前)。@cUndef("MACRO"):取消定义一个宏。
2. 编译与链接
要运行上面的代码,你需要链接 libc。
# 链接 libc
zig run main.zig -lc
如果你的代码依赖于第三方库(例如 libcurl),你需要告诉编译器链接该库。
# 链接 libcurl 和 libc
zig run main.zig -lc -lcurl
在 build.zig 中,你可以这样配置:
exe.linkLibC();
exe.linkSystemLibrary("curl");
3. 类型转换:Zig <-> C
虽然 Zig 能理解 C 类型,但它们在 Zig 中有特定的表示方式。
原始类型
大多数原始类型会自动映射:
char->u8int->c_int(Zig 专门定义了c_int,c_long等类型以匹配 C 的 ABI)float->f32double->f64
指针与数组
这是最需要注意的地方。
- 单项指针 (
*T):Zig 的单项指针对应 C 的指针。 - 多项指针 (
[*]T):C 的数组指针通常对应 Zig 的多项指针。 - C 字符串 (
char*):在 Zig 中表示为[*c]u8(或[*c]const u8)。这是一个特殊的指针类型,专门为了兼容 C 的模糊指针语义而设计(它既可能是 null,也可能指向单个元素,也可能指向数组,还支持指针运算)。
字符串转换实战
场景 1:Zig 字符串 -> C 函数
Zig 字符串是切片([]u8),包含长度。C 字符串是以 \0 结尾的指针。
如果 Zig 字符串是字面量(编译时已知以 \0 结尾),可以直接传递。
const c = @cImport(@cInclude("stdio.h"));
pub fn main() void {
// 字面量,自动兼容
_ = c.printf("Hello\n");
}
如果 Zig 字符串是运行时动态生成的,你需要确保它以 \0 结尾,并传递指针。
const std = @import("std");
const c = @cImport(@cInclude("stdio.h"));
pub fn main() !void {
const allocator = std.heap.page_allocator;
// 创建一个以 0 结尾的 Zig 字符串 (Sentinel-Terminated)
const zig_str = try std.fmt.allocPrintZ(allocator, "Number: {d}\n", .{42});
defer allocator.free(zig_str);
// zig_str 的类型是 [:0]u8
// zig_str.ptr 可以直接传给 C
_ = c.printf(zig_str.ptr);
}
场景 2:C 字符串 -> Zig
当你从 C 函数得到一个 char* 时,在 Zig 中它是 [*c]u8。你需要将其转换为 Zig 的切片才能方便使用(例如获取长度、打印)。
const std = @import("std");
const c = @cImport(@cInclude("stdlib.h"));
pub fn main() void {
// 假设这是从 C 获得的字符串
const c_str: [*c]const u8 = c.getenv("HOME");
if (c_str == null) {
std.debug.print("HOME not set\n", .{});
return;
}
// 将 C 字符串转换为 Zig 切片
// std.mem.span 会遍历字符串直到找到 \0,计算长度
const zig_slice = std.mem.span(c_str);
std.debug.print("HOME: {s}\n", .{zig_slice});
}
4. 使用 C 结构体
Zig 可以直接使用 C 定义的结构体。
假设 user.h:
struct User {
int id;
char* name;
};
Zig 代码:
const c = @cImport(@cInclude("user.h"));
pub fn main() void {
// 初始化 C 结构体
// 注意:C 结构体通常没有默认值,最好显式初始化或使用 undefined
var user: c.User = undefined;
user.id = 1;
// 赋值 C 字符串
user.name = "Ziggy";
std.debug.print("User ID: {d}\n", .{user.id});
}
5. 宏 (Macros)
Zig 会尝试将 C 的宏转换为 const 常量或内联函数。
- 简单的常量宏(
#define MAX 100)会被转换为const MAX = 100;。 - 类似函数的宏会被转换为 Zig 函数。
- 复杂的宏可能无法转换,这种情况下你需要手动在该头文件中通过
@cDefine或辅助 C 文件来包装它。
总结
Zig 的 C 互操作性是其作为“现代 C 替代品”的核心竞争力。
@cImport:直接引入 C 头文件,无需绑定。zig cc:内置 C 编译器,轻松构建 C 代码。- 类型映射:理解
c_int、[*c]u8以及如何使用std.mem.span在 C 和 Zig 字符串之间转换。
掌握了这些,你就拥有了整个 C 语言生态系统的庞大资产库。