实战Zig构建系统:从小白到高手的爆款技能!

1.1 构建系统介绍

Zig 提供了四种构建模式,其中调试模式是默认的,因为它只需要最短的编译时间。 

Runtime SafetyOptimizations
DebugYesNo
ReleaseSafeYesYes, Speed
ReleaseSmallNoYes, Size
ReleaseFastNoYes, Speed

这些模式可以通过 zig run 和 zig test 命令使用 -O ReleaseSafe、-O ReleaseSmall 和 -O ReleaseFast 参数来使用。

1.2 生成可执行文件

可以使用命令 zig build-exe、zig build-lib 和 zig build-obj 分别输出可执行文件、库和目标文件。这些命令接受一个源文件和参数。

一些常见的参数:

  • -fsingle-threaded,这断言二进制文件是单线程的。这将使线程安全措施,如互斥锁,变成无操作。
  • -fstrip,这从二进制文件中移除调试信息。
  • --dynamic,这与 zig build-lib 一起使用来输出一个动态/共享库。

让我们创建一个微小的 hello world。将这个保存为 tiny-hello.zig,然后运行 zig build-exe tiny-hello.zig -O ReleaseSmall -fstrip -fsingle-threaded。

const std = @import("std");

pub fn main() void {
    std.io.getStdOut().writeAll(
        "Hello World!",
    ) catch unreachable;
}

1.3 交叉编译

默认情况下,Zig 将为您的 CPU 和 OS 组合进行编译。这可以通过 -target 参数来覆盖。让我们将我们的微小 hello world 编译到一个 64 位 arm Linux 平台。

zig build-exe .\tiny-hello.zig -O ReleaseSmall -fstrip -fsingle-threaded -target aarch64-linux

可以使用 QEMU 或类似工具方便地测试为外部平台制作的可执行文件。

一些你可以进行交叉编译的 CPU 架构:

  • x86_64
  • arm
  • aarch64
  • i386
  • riscv64
  • wasm32

一些你可以进行交叉编译的操作系统:

  • linux
  • macos
  • windows
  • freebsd
  • netbsd
  • dragonfly
  • UEFI

虽然许多其他编译目标可用,但它们尚未经过充分测试。如需了解更多详情,请查阅 Zig 的支持表,其中列出的经过充分测试的目标列表正在持续更新。

Zig 默认针对您的特定 CPU 进行编译,这意味着生成的二进制文件可能不兼容于架构略有差异的其他计算机。为了提高兼容性,您可以考虑指定一个通用的基线 CPU 模型。请注意,虽然选择较旧的 CPU 架构可以提高兼容性,但这意味着您将无法利用较新的 CPU 指令,这涉及到一个效率与兼容性的权衡。

让我们针对 sandybridge CPU(Intel x86_64,约 2011 年)编译一个二进制文件,确保大多数拥有 x86_64 CPU 的用户能够运行它。在此情况下,我们可以使用 "native" 来代表我们的 CPU 或 OS,以便利用当前系统的配置。

zig build-exe tiny-hello.zig -target x86_64-native -mcpu sandybridge

通过执行 zig targets 命令,您可以获取关于可用架构、操作系统、CPU 和 ABI 的详细信息。由于输出内容较多,您可能希望将其重定向到文件中,例如 zig targets > targets.json

1.4 Zig Build

Zig 构建系统允许人们在他们的 Zig 项目中进行更高级的操作,包括:

  • 引入依赖项
  • 构建多个工件(例如同时构建静态库和动态库)
  • 提供额外的配置
  • 在构建时执行自定义任务
  • 分多个步骤构建(例如在编译之前获取和处理数据)

Zig 构建系统允许您实现这些更复杂的用例,而无需引入任何额外的构建工具或语言(例如 cmake、python),同时充分利用编译器内置的缓存系统。

1.4.1 第一次 Zig Build

.
├── build.zig
└── src
    └── main.zig

定义一个构建函数如下所示,作为我们进入构建系统的入口点,它将允许我们定义一个构建运行器要执行的“步骤”图。将此代码放入 build.zig 中。

const std = @import("std");

pub fn build(b: *std.Build) void {
    const exe = b.addExecutable(.{
        .name = "hello",
        .root_source_file = .{ .path = "src/main.zig" },
        .target = b.standardTargetOptions(.{}),
        .optimize = b.standardOptimizeOption(.{}),
    });

    b.installArtifact(exe);
}

将您的可执行文件入口点放在 src/main.zig 中。

const std = @import("std");

pub fn main() void {
    std.debug.print("Hello, {s}!\n", .{"Zig Build"});
}

我们现在可以运行 zig build,它将输出我们的可执行文件。

$ zig build
$ ./zig-out/bin/hello
Hello, Zig Build!

1.4.2 Target & Optimisation 选项

之前,我们使用 zig build-exe 加上 -target 和 -O 来告诉 Zig 使用哪个目标和优化模式。当使用 Zig 构建系统时,这些设置现在被传递到 b.addExecutable 中。 大多数 Zig 项目将希望使用这些标准选项。

        .target = b.standardTargetOptions(.{}),
        .optimize = b.standardOptimizeOption(.{}),

当使用 standardTargetOptions 和 standardOptimizeOption 时,目标将默认为 native,这意味着可执行文件的目标将与构建它的计算机相匹配。优化模式将默认为 debug。

如果您运行 zig build --help,您可以看到这些函数已经注册了项目特定的构建选项。

Project-Specific Options:
  -Dtarget=[string]            The CPU architecture, OS, and ABI to build for
  -Dcpu=[string]               Target CPU features to add or subtract
  -Doptimize=[enum]            Prioritize performance, safety, or binary size (-O flag)
                                 Supported Values:
                                   Debug
                                   ReleaseSafe
                                   ReleaseFast
                                   ReleaseSmall

我们现在可以通过参数提供它们,例如:

zig build -Dtarget=x86_64-windows -Dcpu=x86_64_v3 -Doptimize=ReleaseSafe

1.4.3 构建选项

得益于标准目标和优化选项,我们已经有一些有用的构建选项。在更高级的项目中,您可能希望添加自己的项目特定选项;这是一个创建和使用更改可执行文件名称的选项的基本示例。

    const exe_name = b.option(
        []const u8,
        "exe_name",
        "Name of the executable",
    ) orelse "hello";

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

如果您现在运行 zig build --help,我们可以看到项目特定的构建选项已经扩展到包括 exe_name。

Project-Specific Options:
  -Dexe_name=[string]          Name of the executable
  -Dtarget=[string]            The CPU architecture, OS, and ABI to build for
$ zig build -Dtarget=x86_64-windows -Dexe_name="Hello!"
$ file zig-out/bin/Hello\!.exe
zig-out/bin/Hello!.exe: PE32+ executable (console) x86-64, for MS Windows, 7 sections

1.4.4 增加 Run Step

我们之前使用 zig run 作为调用 zig build-exe 然后运行生成的二进制文件的便捷快捷方式。我们可以相当容易地使用 Zig 构建系统做类似的事情。

    b.installArtifact(exe);

    const run_exe = b.addRunArtifact(exe);

    const run_step = b.step("run", "Run the application");
    run_step.dependOn(&run_exe.step);
$ zig build run
Hello, Zig Build!

Zig 构建系统使用一个 DAG(有向无环图)步骤,它会并发运行。在这里,我们创建了一个名为“run”的步骤,它依赖于 run_exe 步骤,该步骤又依赖于我们的编译步骤。

让我们看看我们构建中的步骤分解。

$ zig build run --summary all
Hello, Zig Build!
Build Summary: 3/3 steps succeeded
run success
└─ run hello success 471us MaxRSS:3M
   └─ zig build-exe hello Debug native success 881ms MaxRSS:220M

随着我们的进步,我们将看到更高级的构建图。

1.5 生成文档

Zig 编译器附带了自动文档生成。这可以通过向您的 zig build-{exe, lib, obj} 或 zig run 命令添加 -femit-docs 来调用。这些文档被保存在 ./docs 中,作为一个小的静态网站。

Zig 的文档生成利用了文档注释,这些注释类似于注释,使用 /// 而不是 //,并位于全局变量之前。

我们将这个保存为 x.zig 并使用 zig build-lib -femit-docs x.zig -target native-windows 为它构建文档。这里有一些要点:

  • 只有带有文档注释的公共内容才会出现
  • 可以使用空白文档注释
  • 文档注释可以利用 Markdown 的一个子集
  • 只有当编译器分析它们时,内容才会出现在生成的文档中;您可能需要强制分析发生以使内容出现。
const std = @import("std");
const w = std.os.windows;

///**Opens a process**, giving you a handle to it. 
///[MSDN](https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-openprocess)
pub extern "kernel32" fn OpenProcess(
    ///[The desired process access rights](https://docs.microsoft.com/en-us/windows/win32/procthread/process-security-and-access-rights)
    dwDesiredAccess: w.DWORD,
    ///
    bInheritHandle: w.BOOL,
    dwProcessId: w.DWORD,
) callconv(w.WINAPI) ?w.HANDLE;

///spreadsheet position
pub const Pos = struct{
    ///row
    x: u32,
    ///column
    y: u32,
};

pub const message = "hello!";

//used to force analysis, as these things aren't otherwise referenced.
comptime {
    _ = OpenProcess;
    _ = Pos;
    _ = message;
}

//Alternate method to force analysis of everything automatically, but only in a test build:
test "Force analysis" {
    comptime {
        std.testing.refAllDecls(@This());
    }
}

当使用 build.zig 时,这可以通过在 CompileStep 上将 emit_docs 字段设置为 .emit 来调用。我们可以创建一个构建步骤来生成文档,如下所示,并使用 $ zig build docs 来调用它。

const std = @import("std");

pub fn build(b: *std.build.Builder) void {
    const mode = b.standardReleaseOptions();

    const lib = b.addStaticLibrary("x", "src/x.zig");
    lib.setBuildMode(mode);
    lib.install();

    const tests = b.addTest("src/x.zig");
    tests.setBuildMode(mode);

    const test_step = b.step("test", "Run library tests");
    test_step.dependOn(&tests.step);

    //Build step to generate docs:
    const docs = b.addTest("src/x.zig");
    docs.setBuildMode(mode);
    docs.emit_docs = .emit;
    
    const docs_step = b.step("docs", "Generate docs");
    docs_step.dependOn(&docs.step);
}

这个生成是实验性的,经常在复杂的示例中失败。这被标准库文档所使用。

当合并错误集时,最左侧的错误集的文档字符串优先于右侧的。在这种情况下,C.PathNotFound 的文档注释是 A 中提供的文档注释。

const A = error{
    NotDir,

    /// A doc comment
    PathNotFound,
};
const B = error{
    OutOfMemory,

    /// B doc comment
    PathNotFound,
};

const C = A || B;

1.6 总结

为了运行 Zig 代码,我们一般使用 zig run learning.zig。测试的时候我们还用 zig test learning.zig 进行一次测试。运行和测试命令用来玩玩还行,但如果要做更复杂的事情,就需要使用构建命令了。编译命令是依赖于带有特殊编译入口的 build.zig 文件。所以本篇就是为了让你了解软件工程中编译环节不可缺少的一个技术环节,希望你尽快掌握,为后续我们编写实战应用做好技术储备。

  • 23
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

xiaodeshi

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值