将 Zig 作为 C 语言构建系统


即使你暂时不打算学习 Zig 语言本身,Zig 工具链也是一个非常优秀的 C/C++ 构建工具。它内置交叉编译支持、增量编译、智能缓存,且无需安装额外的工具链。

为什么用 Zig 构建 C 项目?

传统的 C 项目构建工具(Make、CMake、Autotools)各有痛点:

工具问题
Make语法晦涩,跨平台困难
CMake配置复杂,学习曲线陡峭
Autotools过于繁琐,现代项目很少使用

Zig 构建系统的优势:

  • 零配置交叉编译:一行命令编译到任意平台
  • 内置 C/C++ 编译器:基于 LLVM,无需安装 GCC/Clang
  • 增量编译与缓存:只重新编译修改过的文件
  • 声明式配置:使用 Zig 语言编写构建脚本,类型安全
  • 单一可执行文件:整个工具链只有一个 zig 二进制文件

项目结构

我们将构建一个简单的 C 项目,包含一个 power 函数用于计算幂次方:

helloCByzig/
├── build.zig          # Zig 构建脚本
├── include/
│   └── foo.h          # 头文件
└── src/
    ├── main.c         # 主程序
    └── foo.c          # power 函数实现

源代码

include/foo.h

#ifndef FOO_H
#define FOO_H

// power: 计算 base 的 n 次方; n >= 0
int power(int base, int n);

#endif

src/foo.c

#include "foo.h"

int power(int base, int n) {
    int i, p;
    p = 1;
    for (i = 1; i <= n; ++i)
        p = p * base;
    return p;
}

src/main.c

#include <stdio.h>
#include "foo.h"

int main() {
    int i;
    for (i = 0; i < 10; ++i)
        printf("%d %d %d\n", i, power(2, i), power(-3, i));
    return 0;
}

build.zig 详解

这是项目的核心——Zig 构建脚本:

const std = @import("std");

pub fn build(b: *std.Build) void {
    // 标准目标选项(允许通过命令行指定目标平台)
    const target = b.standardTargetOptions(.{});
    // 标准优化选项(Debug/ReleaseSafe/ReleaseFast/ReleaseSmall)
    const optimize = b.standardOptimizeOption(.{});

    // 创建可执行文件
    const exe = b.addExecutable(.{
        .name = "hello",
        .root_module = b.createModule(.{
            .target = target,
            .optimize = optimize,
            .link_libc = true,  // 链接 C 标准库
        }),
    });

    // 添加 C 源文件
    exe.addCSourceFiles(.{
        .files = &.{
            "src/main.c",
            "src/foo.c",
        },
        .flags = &.{
            "-Wall",      // 开启所有警告
            "-Wextra",    // 开启额外警告
            "-std=c11",   // 使用 C11 标准
        },
    });

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

    // 安装构建产物
    b.installArtifact(exe);

    // 添加 run 命令
    const run_cmd = b.addRunArtifact(exe);
    run_cmd.step.dependOn(b.getInstallStep());

    // 允许传递命令行参数
    if (b.args) |args| {
        run_cmd.addArgs(args);
    }

    // 创建 "zig build run" 步骤
    const run_step = b.step("run", "Run the application");
    run_step.dependOn(&run_cmd.step);
}

build.zig 关键概念

函数/方法作用
b.standardTargetOptions(.{})允许通过 -Dtarget=xxx 指定目标平台
b.standardOptimizeOption(.{})允许通过 -Doptimize=xxx 指定优化级别
b.addExecutable()创建可执行文件构建目标
exe.addCSourceFiles()添加 C 源文件和编译标志
exe.addIncludePath()添加头文件搜索路径
b.installArtifact()将构建产物安装到 zig-out/bin/
b.addRunArtifact()创建运行命令

构建与运行

基本命令

# 构建项目
zig build

# 构建并运行
zig build run

# 查看构建产物
ls zig-out/bin/
# 输出: hello

运行结果

0 1 1
1 2 -3
2 4 9
3 8 -27
4 16 81
5 32 -243
6 64 729
7 128 -2187
8 256 6561
9 512 -19683

优化级别

Zig 提供四种优化级别:

# Debug(默认):包含调试信息,无优化
zig build

# ReleaseSafe:优化 + 运行时安全检查
zig build -Doptimize=ReleaseSafe

# ReleaseFast:最大性能优化
zig build -Doptimize=ReleaseFast

# ReleaseSmall:最小二进制体积
zig build -Doptimize=ReleaseSmall

交叉编译

Zig 的杀手级特性——零配置交叉编译:

# 编译到 Windows
zig build -Dtarget=x86_64-windows-gnu

# 编译到 ARM64 Linux(如树莓派 4)
zig build -Dtarget=aarch64-linux-gnu

# 编译到 macOS
zig build -Dtarget=x86_64-macos

# 编译到 macOS ARM(M1/M2)
zig build -Dtarget=aarch64-macos

# 编译到 WebAssembly
zig build -Dtarget=wasm32-wasi

无需安装任何交叉编译工具链,Zig 内置了所有目标平台的支持。

直接使用 zig cc

如果不想写 build.zig,可以直接使用 zig cc 作为 C 编译器的替代品:

# 编译单文件
zig cc -o hello src/main.c src/foo.c -Iinclude -Wall

# 运行
./hello

# 交叉编译到 Windows
zig cc -target x86_64-windows-gnu -o hello.exe src/main.c src/foo.c -Iinclude

zig cc 可以作为 gccclang 的直接替代品。

与 CMake 对比

同样的项目,CMake 的 CMakeLists.txt

cmake_minimum_required(VERSION 3.10)
project(hello C)

set(CMAKE_C_STANDARD 11)
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wall -Wextra")

add_executable(hello
    src/main.c
    src/foo.c
)

target_include_directories(hello PRIVATE include)

构建步骤:

# CMake 需要两步
mkdir build && cd build
cmake ..
make

# Zig 只需要一步
zig build

交叉编译对比:

# CMake 交叉编译(需要安装工具链 + 编写工具链文件)
cmake -DCMAKE_TOOLCHAIN_FILE=toolchain-arm.cmake ..

# Zig 交叉编译(零配置)
zig build -Dtarget=aarch64-linux-gnu

扩展:构建静态库

如果想将 power 函数编译为静态库:

const std = @import("std");

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

    // 创建静态库
    const lib = b.addStaticLibrary(.{
        .name = "foo",
        .root_module = b.createModule(.{
            .target = target,
            .optimize = optimize,
            .link_libc = true,
        }),
    });

    lib.addCSourceFiles(.{
        .files = &.{"src/foo.c"},
        .flags = &.{ "-Wall", "-std=c11" },
    });

    lib.addIncludePath(b.path("include"));
    lib.installHeader(b.path("include/foo.h"), "foo.h");

    b.installArtifact(lib);
}

构建后会生成 zig-out/lib/libfoo.a

总结

Zig 作为 C 构建系统的优势:

特性说明
单一工具一个 zig 二进制文件包含编译器、链接器、构建系统
声明式配置使用真正的编程语言(Zig)编写构建脚本
零配置交叉编译内置所有目标平台支持
增量编译智能缓存,只重编译修改的文件
C/C++ 兼容可以直接编译 C/C++ 代码

即使你不打算使用 Zig 语言本身,将 zig cczig build 作为 C 项目的构建工具也是一个明智的选择。

参考资源