Chapter 41Cross Compilation And Wasm

跨编译与 WASM

概览

通过性能分析与护栏收紧反馈回路后(40),我们已准备将二进制交付到其他平台。本章将介绍目标发现、原生跨编译以及发射 WASI 模块的要点,沿用先前依赖的 CLI 插桩。#entry points and command structure

下一章将把这些机制变为完整的 WASI 项目,因此可将本章视为动手的预飞检查。42

学习目标

  • 解释目标三元组并查询 Zig 的内置元数据以获取替代架构。Query.zig
  • 使用 zig build-exe 跨编译原生可执行文件,并在不离开 Linux 的情况下验证制品。
  • 生成与原生代码共享相同源码的 WASI 二进制,为项目构建流水线做好准备。#Command-line-flags

映射目标三元组

Zig 的@import("builtin")暴露编译器对当前世界的认知,而std.Target.Query.parse允许你在不构建的情况下检查假设目标。Target.zig

这是在使用zig build之前定制构建图或 ENT 文件的基础。

理解 Target 结构

在解析目标三元组之前,了解 Zig 在内部如何表示编译目标很有价值。下图展示了完整的std.Target结构:

graph TB subgraph "std.Target Structure" TARGET["std.Target"] CPU["cpu: Cpu"] OS["os: Os"] ABI["abi: Abi"] OFMT["ofmt: ObjectFormat"] DYNLINKER["dynamic_linker: DynamicLinker"] TARGET --> CPU TARGET --> OS TARGET --> ABI TARGET --> OFMT TARGET --> DYNLINKER end subgraph "Cpu Components" CPU --> ARCH["arch: Cpu.Arch"] CPU --> MODEL["model: *const Cpu.Model"] CPU --> FEATURES["features: Feature.Set"] ARCH --> ARCHEX["x86_64, aarch64, wasm32, etc"] MODEL --> MODELEX["generic, native, specific variants"] FEATURES --> FEATEX["CPU feature flags"] end subgraph "Os Components" OS --> OSTAG["tag: Os.Tag"] OS --> VERSION["version_range: VersionRange"] OSTAG --> OSEX["linux, windows, macos, wasi, etc"] VERSION --> VERUNION["linux: LinuxVersionRange<br/>windows: WindowsVersion.Range<br/>semver: SemanticVersion.Range<br/>none: void"] end subgraph "Abi and Format" ABI --> ABIEX["gnu, musl, msvc, none, etc"] OFMT --> OFMTEX["elf, macho, coff, wasm, c, spirv"] end

该结构揭示三元组如何映射到具体配置。当你指定-target wasm32-wasi时,CPU 架构设为wasm32,OS 标签设为wasi,并隐式将对象格式设为wasmx86_64-windows-gnu映射为架构x86_64、OSwindows、ABIgnu与格式coff(Windows PE)。

各组件影响代码生成:CPU 架构决定指令集与调用约定;OS 标签选择系统调用接口与运行时期望;ABI 指定调用约定与名称改编;对象格式决定链接器(Linux 用 ELF、Darwin 用 Mach-O、Windows 用 COFF、Web/WASI 用 WASM)。理解该映射有助于你解读std.Target.Query.parse结果、预测跨编译行为并排查目标特定问题。CPU 特性字段捕捉架构特定能力(x86_64 上的 AVX、ARM 上的 SIMD),供优化器用于代码生成。

目标解析流程

目标查询(用户输入)通过系统化流程解析为具体目标:

graph TB subgraph "Resolution Flow" QUERY["std.Target.Query<br/>user input with defaults"] RESOLVE["resolveTargetQuery()"] TARGET["std.Target<br/>fully resolved"] QUERY --> RESOLVE RESOLVE --> TARGET end subgraph "Query Sources" CMDLINE["-target flag<br/>command line"] DEFAULT["native detection<br/>std.zig.system"] MODULE["Module.resolved_target"] CMDLINE --> QUERY DEFAULT --> QUERY end subgraph "Native Detection" DETECT["std.zig.system detection"] CPUDETECT["CPU: cpuid, /proc/cpuinfo"] OSDETECT["OS: uname, NT version"] ABIDETECT["ABI: ldd, platform defaults"] DETECT --> CPUDETECT DETECT --> OSDETECT DETECT --> ABIDETECT end TARGET --> COMP["Compilation.root_mod<br/>.resolved_target.result"]

目标查询来自三个来源:命令行 -target 标志(显式用户选择)、未指定目标时的原生检测(通过 cpuid 或 /proc/cpuinfo 读取主机 CPU,通过 uname 或 NT API 读取 OS,通过 ldd 或平台默认值读取 ABI),或构建脚本中的模块配置。

resolveTargetQuery()会通过补全缺失细节,将查询(可能包含"native"或"default"占位符)转换为具体的std.Target实例。该解析在编译初始化阶段进行,先于任何代码生成。

当你未提供-target时,Zig 会自动检测宿主系统并构建本机目标。指定诸如wasm32-wasi等部分三元组时,解析将补全 ABI(WASI 通常为musl)与对象格式(wasm)。解析得到的目标随后进入编译模块,控制代码生成的各个方面,从指令选择到运行时库选择。

示例:在代码中比较宿主与跨目标

示例会内省宿主三元组,并解析两个跨目标,打印解析后的架构、OS 与 ABI。

Zig
// 导入标准库以进行目标查询和打印
const std = @import("std");
// 导入内置模块以访问编译时主机目标信息
const builtin = @import("builtin");

// / 演示目标发现和跨平台元数据检查的入口点。
// / 此示例展示了如何内省主机编译目标以及解析
// / 假设的交叉编译目标,而无需实际构建它们。
pub fn main() void {
    // 通过访问 builtin.target 打印主机目标三元组(架构-操作系统-ABI)
    // 这显示了 Zig 当前正在为其编译的平台
    std.debug.print(
        "host triple: {s}-{s}-{s}\n",
        .{
            @tagName(builtin.target.cpu.arch),
            @tagName(builtin.target.os.tag),
            @tagName(builtin.target.abi),
        },
    );

    // 显示主机目标的指针宽度
    // @bitSizeOf(usize) 返回当前平台指针的位大小
    std.debug.print("pointer width: {d} bits\n", .{@bitSizeOf(usize)});

    // 从目标三元组字符串解析 WASI 目标查询
    // 这演示了如何以编程方式检查交叉编译目标
    const wasm_query = std.Target.Query.parse(.{ .arch_os_abi = "wasm32-wasi" }) catch unreachable;
    describeQuery("wasm32-wasi", wasm_query);

    // 解析 Windows 目标查询以显示另一个交叉编译场景
    // 三元组格式如下:架构-操作系统-ABI
    const windows_query = std.Target.Query.parse(.{ .arch_os_abi = "x86_64-windows-gnu" }) catch unreachable;
    describeQuery("x86_64-windows-gnu", windows_query);

    // 打印主机目标是否配置为单线程执行
    // 此编译时常量影响运行时库行为
    std.debug.print("single-threaded: {}\n", .{builtin.single_threaded});
}

//  打印给定目标查询的已解析架构、操作系统和 ABI。
//  此帮助程序演示了如何提取和显示目标元数据,使用
//  当查询未指定某些字段时,将主机目标用作回退。
fn describeQuery(label: []const u8, query: std.Target.Query) void {
    std.debug.print(
        "query {s}: arch={s} os={s} abi={s}\n",
        .{
            label,
            // 如果查询未指定,则回退到主机架构
            @tagName((query.cpu_arch orelse builtin.target.cpu.arch)),
            // 如果查询未指定,则回退到主机操作系统
            @tagName((query.os_tag orelse builtin.target.os.tag)),
            // 如果查询未指定,则回退到主机 ABI
            @tagName((query.abi orelse builtin.target.abi)),
        },
    );
}
运行
Shell
$ zig run 01_target_matrix.zig
输出
Shell
host triple: x86_64-linux-gnu
pointer width: 64 bits
query wasm32-wasi: arch=wasm32 os=wasi abi=gnu
query x86_64-windows-gnu: arch=x86_64 os=windows abi=gnu
single-threaded: false

解析器遵循与-Dtargetzig build-exe -target相同的语法;可在调用编译器前复用解析输出以播种构建配置。

跨编译原生可执行文件

拿到三元组后,跨编译只需切换目标标志。Zig 0.15.2 随附自包含的 libc 集成,因此在 Linux 上生成 Windows 或 macOS 二进制不再需要额外 SDK。v0.15.2

使用 file 或类似工具确认制品,而无需启动另一个操作系统。

示例:在 Linux 上使用生成 Windows 可执行

我们保持源码相同,原生运行以进行健全性检查,然后发射 Windows PE 二进制并在原地检查它。

Zig
// 导入标准库以获取打印和平台工具
const std = @import("std");
// 导入内置模块以访问编译时目标信息
const builtin = @import("builtin");

// 演示通过显示目标平台信息进行交叉编译的入口点
pub fn main() void {
    // 打印目标平台的 CPU 架构、操作系统和 ABI
    // 使用 builtin.target 访问编译时目标信息
    std.debug.print("hello from {s}-{s}-{s}!\n", .{
        @tagName(builtin.target.cpu.arch),
        @tagName(builtin.target.os.tag),
        @tagName(builtin.target.abi),
    });

    // 检索特定于平台的 EXE 文件扩展名(例如,Windows 上的“.exe”,Linux 上的“”)
    const suffix = std.Target.Os.Tag.exeFileExt(builtin.target.os.tag, builtin.target.cpu.arch);
    std.debug.print("default executable suffix: {s}\n", .{suffix});
}
运行
Shell
$ zig run 02_cross_greeter.zig
输出
Shell
hello from x86_64-linux-gnu!
default executable suffix:
跨编译
Shell
$ zig build-exe 02_cross_greeter.zig -target x86_64-windows-gnu -OReleaseFast -femit-bin=greeter-windows.exe
$ file greeter-windows.exe
输出
Shell
greeter-windows.exe: PE32+ executable (console) x86-64, for MS Windows, 7 sections

当您需要为较旧硬件提供可移植二进制时,将 -target-mcpu=baseline 配对;上面的 std.Target.Query 输出显示 Zig 将假定哪个 CPU 模型。

发射 WASI 模块

WebAssembly System Interface (WASI) 构建与原生流水线共享大部分内容,但使用不同的对象格式。相同的 Zig 源码可以在 Linux 上打印诊断信息,并在跨编译时发射 .wasm 负载,这得益于本版本中引入的共享 libc 组件。

对象格式与链接器选择

在生成 WASI 二进制之前,理解对象格式如何决定编译输出很重要。下图展示了 ABI 与对象格式之间的关系:

graph TB subgraph "Common ABIs" ABI["Abi enum"] ABI --> GNU["gnu<br/>GNU toolchain"] ABI --> MUSL["musl<br/>musl libc"] ABI --> MSVC["msvc<br/>Microsoft Visual C++"] ABI --> NONE["none<br/>freestanding"] ABI --> ANDROID["android, gnueabi, etc<br/>platform variants"] end subgraph "Object Formats" OFMT["ObjectFormat enum"] OFMT --> ELF["elf<br/>Linux, BSD"] OFMT --> MACHO["macho<br/>Darwin systems"] OFMT --> COFF["coff<br/>Windows PE"] OFMT --> WASM["wasm<br/>WebAssembly"] OFMT --> C["c<br/>C source output"] OFMT --> SPIRV["spirv<br/>Shaders"] end

对象格式决定 Zig 使用哪种链接器实现来生成最终二进制。ELF(可执行与可链接格式)用于 Linux 与 BSD 系统,生成.so共享库与标准可执行文件。Mach-O面向 Darwin 系统(macOS、iOS),生成.dylib库与 Mach 可执行。COFF(通用对象文件格式)在面向 Windows 时生成 PE 二进制(.exe.dll)。WASM(WebAssembly)是一种独特格式,生成供浏览器与 WASI 运行时使用的.wasm模块;与传统格式不同,WASM 模块是为沙箱执行设计的平台无关字节码。CSPIRV较为特殊:C 输出可供 C 构建系统集成的源代码,而 SPIRV 生成 GPU 着色器字节码。

当你为-target wasm32-wasi构建时,Zig 会选择 WASM 对象格式并调用 WebAssembly 链接器(link/Wasm.zig),其处理函数导入/导出、内存管理与表初始化等 WASM 特有概念。这与 ELF 链接器(符号解析、重定位)或 COFF 链接器(导入表、资源段)有本质区别。同一源码可透明编译为不同对象格式——无论面向原生 Linux(ELF)、Windows(COFF)还是 WASI(WASM),你的 Zig 代码保持一致。

示例:单一源码,原生运行,WASI 制品

我们的流水线记录执行阶段并在 builtin.target.os.tag 上分支,以便 WASI 构建宣布其自己的入口点。

Zig
// 导入标准库以获取调试打印功能
const std = @import("std");
// 导入内置模块以访问编译时目标信息
const builtin = @import("builtin");

//  将阶段名称打印到 stderr 以跟踪执行流程。
//  此辅助函数演示了跨平台上下文中的调试输出。
fn stage(name: []const u8) void {
    std.debug.print("stage: {s}\n", .{name});
}

//  演示基于目标操作系统的条件编译。
//  此示例展示了 Zig 代码如何根据
//  它是为 WASI(WebAssembly 系统接口)还是原生平台编译,在编译时进行分支。
//  执行流程根据目标变化,说明了交叉编译功能。
pub fn main() void {
    // 模拟初始参数解析阶段
    stage("parse-args");
    // 模拟有效负载渲染阶段
    stage("render-payload");

    // 编译时分支:WASI 与原生目标的不同入口点
    // 这演示了 Zig 如何处理平台特定的代码路径
    if (builtin.target.os.tag == .wasi) {
        stage("wasi-entry");
    } else {
        stage("native-entry");
    }

    // 打印编译目标的实际 OS 标签名
    // @tagName 将枚举值转换为其字符串表示
    stage(@tagName(builtin.target.os.tag));
}
运行
Shell
$ zig run 03_wasi_pipeline.zig
输出
Shell
stage: parse-args
stage: render-payload
stage: native-entry
stage: linux
WASI 构建
Shell
$ zig build-exe 03_wasi_pipeline.zig -target wasm32-wasi -OReleaseSmall -femit-bin=wasi-pipeline.wasm
$ ls -lh wasi-pipeline.wasm
输出
Shell
-rwxr--r-- 1 zkevm zkevm 4.6K Nov  6 13:40 wasi-pipeline.wasm

使用您偏好的运行时(Wasmtime、Wasmer、浏览器)运行生成的模块,或将其交给下一章的构建图。无需更改源码。

注意与警示

  • zig targets 提供支持的三元组的权威矩阵。在分派作业之前编写脚本以验证您的构建矩阵。
  • 某些目标默认为 ReleaseSmall 风格的安全性。当您需要在跨架构间保持一致的运行时检查时,请显式设置 -Doptimize#releasefast
  • 在与 glibc 进行跨链接时,请填充ZIG_LIBC或使用zig fetch缓存 sysroot 制品,以避免链接器意外地引用宿主头文件。

练习

  • 扩展问候程序,添加 --cpu--os 标志,然后为 x86_64-macos-gnuaarch64-linux-musl 发射二进制,并使用 ls -lh 捕获它们的大小。
  • 修改 WASI 流水线以通过 std.json.stringify 发射 JSON,然后在 WASI 运行时中运行它并捕获输出以进行回归测试。json.zig
  • 编写一个 build.zig 步骤,循环遍历目标三元组列表,并为每个目标调用 addExecutable,使用 std.Target.Query 辅助程序打印人类友好的标签。22

替代方案与边界情况:

  • LLVM 支持的目标可能仍与 Zig 的自托管代码生成行为不同。当您遇到新兴架构时,请回退到 -fllvm
  • WASI 禁止许多系统调用和动态分配模式。保持日志简洁或受控,以避免超出导入预算。
  • Windows 跨编译默认选择 GNU 工具链。如果您打算链接到 MSVC 提供的库,请添加 -msvc 或切换 ABI。20

Help make this chapter better.

Found a typo, rough edge, or missing explanation? Open an issue or propose a small improvement on GitHub.