Overview
Having tightened our feedback loop with profiling and safeguards, 40 we are ready to ship those binaries to other platforms. This chapter walks through target discovery, native cross-compilation, and the essentials for emitting WASI modules, using the same CLI instrumentation we relied on earlier. #entry points and command structure
The very next chapter turns these mechanics into a full WASI project, so treat this as your hands-on preflight. 42
Learning Goals
- Interpret target triples and query Zig’s built-in metadata for alternate architectures. Query.zig
- Cross-compile native executables with
zig build-exeand verify artifacts without leaving Linux. - Produce WASI binaries that share the same source as native code, ready for the project build pipeline. #Command-line-flags
Mapping Target Triples
Zig’s @import("builtin") exposes the compiler’s current idea of the world, while std.Target.Query.parse lets you inspect hypothetical targets without building them. Target.zig
This is the foundation for tailoring build graphs or ENT files before you touch zig build.
Understanding the Target Structure
Before parsing target triples, it’s valuable to understand how Zig represents compilation targets internally. The following diagram shows the complete std.Target structure:
This structure reveals how target triples map to concrete configuration. When you specify -target wasm32-wasi, you’re setting CPU architecture to wasm32, OS tag to wasi, and implicitly ObjectFormat to wasm. The triple x86_64-windows-gnu maps to arch x86_64, OS windows, ABI gnu, and format coff (Windows PE).
Each component affects code generation: the CPU arch determines instruction sets and calling conventions, the OS tag selects system call interfaces and runtime expectations, the ABI specifies calling conventions and name mangling, and the ObjectFormat chooses the linker (ELF for Linux, Mach-O for Darwin, COFF for Windows, WASM for web/WASI). Understanding this mapping helps you decode std.Target.Query.parse results, predict cross-compilation behavior, and troubleshoot target-specific issues. The CPU features field captures architecture-specific capabilities (AVX on x86_64, SIMD on ARM) that the optimizer uses for code generation.
Target Resolution Flow
Target queries (user input) get resolved into concrete targets through a systematic process:
Target queries come from three sources: command-line -target flags (explicit user choice), native detection when no target is specified (reads host CPU via cpuid or /proc/cpuinfo, OS via uname or NT APIs, and ABI via ldd or platform defaults), or module configuration in build scripts.
The resolveTargetQuery() function converts queries (which may contain "native" or "default" placeholders) into fully concrete std.Target instances by filling in all missing details. This resolution happens during compilation initialization before any code generation occurs.
When you omit -target, Zig automatically detects your host system and builds a native target. When you specify a partial triple like wasm32-wasi, resolution fills in the ABI (typically musl for WASI) and object format (wasm). The resolved target then flows into the compilation module where it controls every aspect of code generation, from instruction selection to runtime library choices.
Example: comparing host and cross targets from code
The sample introspects the host triple and then parses two cross targets, printing the resolved architecture, OS, and ABI.
// Import standard library for target querying and printing
const std = @import("std");
// Import builtin module to access compile-time host target information
const builtin = @import("builtin");
/// Entry point that demonstrates target discovery and cross-platform metadata inspection.
/// This example shows how to introspect both the host compilation target and parse
/// hypothetical cross-compilation targets without actually building for them.
pub fn main() void {
// Print the host target triple (architecture-OS-ABI) by accessing builtin.target
// This shows the platform Zig is currently compiling for
std.debug.print(
"host triple: {s}-{s}-{s}\n",
.{
@tagName(builtin.target.cpu.arch),
@tagName(builtin.target.os.tag),
@tagName(builtin.target.abi),
},
);
// Display the pointer width for the host target
// @bitSizeOf(usize) returns the size in bits of a pointer on the current platform
std.debug.print("pointer width: {d} bits\n", .{@bitSizeOf(usize)});
// Parse a WASI target query from a target triple string
// This demonstrates how to inspect cross-compilation targets programmatically
const wasm_query = std.Target.Query.parse(.{ .arch_os_abi = "wasm32-wasi" }) catch unreachable;
describeQuery("wasm32-wasi", wasm_query);
// Parse a Windows target query to show another cross-compilation scenario
// The triple format follows: architecture-OS-ABI
const windows_query = std.Target.Query.parse(.{ .arch_os_abi = "x86_64-windows-gnu" }) catch unreachable;
describeQuery("x86_64-windows-gnu", windows_query);
// Print whether the host target is configured for single-threaded execution
// This compile-time constant affects runtime library behavior
std.debug.print("single-threaded: {}\n", .{builtin.single_threaded});
}
/// Prints the resolved architecture, OS, and ABI for a given target query.
/// This helper demonstrates how to extract and display target metadata, using
/// the host target as a fallback when the query doesn't specify certain fields.
fn describeQuery(label: []const u8, query: std.Target.Query) void {
std.debug.print(
"query {s}: arch={s} os={s} abi={s}\n",
.{
label,
// Fall back to host architecture if query doesn't specify one
@tagName((query.cpu_arch orelse builtin.target.cpu.arch)),
// Fall back to host OS if query doesn't specify one
@tagName((query.os_tag orelse builtin.target.os.tag)),
// Fall back to host ABI if query doesn't specify one
@tagName((query.abi orelse builtin.target.abi)),
},
);
}
$ zig run 01_target_matrix.zighost 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: falseThe parser obeys the same syntax as -Dtarget or zig build-exe -target; recycle the output to seed build configurations before invoking the compiler.
Cross-Compiling Native Executables
With a triple in hand, cross-compiling is a matter of swapping the target flag. Zig 0.15.2 ships with self-contained libc integrations, so producing Windows or macOS binaries on Linux no longer requires additional SDKs. v0.15.2
Use file or similar tooling to confirm artifacts without booting another OS.
Example: to Windows from Linux
We keep the source identical, run it natively for sanity, then emit a Windows PE binary and inspect it in place.
// Import the standard library for printing and platform utilities
const std = @import("std");
// Import builtin to access compile-time target information
const builtin = @import("builtin");
// Entry point that demonstrates cross-compilation by displaying target platform information
pub fn main() void {
// Print the target platform's CPU architecture, OS, and ABI
// Uses builtin.target to access compile-time target information
std.debug.print("hello from {s}-{s}-{s}!\n", .{
@tagName(builtin.target.cpu.arch),
@tagName(builtin.target.os.tag),
@tagName(builtin.target.abi),
});
// Retrieve the platform-specific executable file extension (e.g., ".exe" on Windows, "" on 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});
}
$ zig run 02_cross_greeter.zighello from x86_64-linux-gnu!
default executable suffix:$ zig build-exe 02_cross_greeter.zig -target x86_64-windows-gnu -OReleaseFast -femit-bin=greeter-windows.exe
$ file greeter-windows.exegreeter-windows.exe: PE32+ executable (console) x86-64, for MS Windows, 7 sectionsPair -target with -mcpu=baseline when you need portable binaries for older hardware; the std.Target.Query output above shows which CPU model Zig will assume.
Emitting WASI Modules
WebAssembly System Interface (WASI) builds share most of the native pipeline with a different object format. The same Zig source can print diagnostics on Linux and emit a .wasm payload when cross-compiled, thanks to shared libc pieces introduced in this release.
Object Formats and Linker Selection
Before generating WASI binaries, it’s important to understand how object formats determine compilation output. The following diagram shows the relationship between ABIs and object formats:
The object format determines which linker implementation Zig uses to produce final binaries. ELF (Executable and Linkable Format) is used for Linux and BSD systems, producing .so shared libraries and standard executables. Mach-O targets Darwin systems (macOS, iOS), generating .dylib libraries and Mach executables. COFF (Common Object File Format) produces Windows PE binaries (.exe, .dll) when targeting Windows. WASM (WebAssembly) is a unique format that produces .wasm modules for web browsers and WASI runtimes. Unlike traditional formats, WASM modules are platform-independent bytecode designed for sandboxed execution. C and SPIRV are specialized: C outputs source code for integration with C build systems, while SPIRV produces GPU shader bytecode.
When you build for -target wasm32-wasi, Zig selects the WASM object format and invokes the WebAssembly linker (link/Wasm.zig), which handles WASM-specific concepts like function imports/exports, memory management, and table initialization. This is fundamentally different from the ELF linker (symbol resolution, relocations) or COFF linker (import tables, resource sections). The same source code compiles to different object formats transparently—your Zig code remains identical whether targeting native Linux (ELF), Windows (COFF), or WASI (WASM).
Example: single source, native run, WASI artifact
Our pipeline logs the execution stages and branches on builtin.target.os.tag so the WASI build announces its own entry point.
// Import standard library for debug printing capabilities
const std = @import("std");
// Import builtin module to access compile-time target information
const builtin = @import("builtin");
/// Prints a stage name to stderr for tracking execution flow.
/// This helper function demonstrates debug output in cross-platform contexts.
fn stage(name: []const u8) void {
std.debug.print("stage: {s}\n", .{name});
}
/// Demonstrates conditional compilation based on target OS.
/// This example shows how Zig code can branch at compile-time depending on
/// whether it's compiled for WASI (WebAssembly System Interface) or native platforms.
/// The execution flow changes based on the target, illustrating cross-compilation capabilities.
pub fn main() void {
// Simulate initial argument parsing stage
stage("parse-args");
// Simulate payload rendering stage
stage("render-payload");
// Compile-time branch: different entry points for WASI vs native targets
// This demonstrates how Zig handles platform-specific code paths
if (builtin.target.os.tag == .wasi) {
stage("wasi-entry");
} else {
stage("native-entry");
}
// Print the actual OS tag name for the compilation target
// @tagName converts the enum value to its string representation
stage(@tagName(builtin.target.os.tag));
}
$ zig run 03_wasi_pipeline.zigstage: parse-args
stage: render-payload
stage: native-entry
stage: linux$ zig build-exe 03_wasi_pipeline.zig -target wasm32-wasi -OReleaseSmall -femit-bin=wasi-pipeline.wasm
$ ls -lh wasi-pipeline.wasm-rwxr--r-- 1 zkevm zkevm 4.6K Nov 6 13:40 wasi-pipeline.wasmRun the resulting module with your preferred runtime (Wasmtime, Wasmer, browsers) or hand it to the build graph from the next chapter. No source changes required.
Notes & Caveats
zig targetsprovides the authoritative matrix of supported triples. Script it to validate your build matrix before dispatching jobs.- Some targets default to
ReleaseSmall-style safety. Explicitly set-Doptimizewhen you require consistent runtime checks across architectures. #releasefast - When cross-linking to glibc, populate
ZIG_LIBCor usezig fetchto cache sysroot artifacts so the linker does not reach for host headers unexpectedly.
Exercises
- Extend the greeter program with
--cpuand--osflags, then emit binaries forx86_64-macos-gnuandaarch64-linux-musland capture their sizes withls -lh. - Modify the WASI pipeline to emit JSON via
std.json.stringify, then run it in a WASI runtime and capture the output for regression tests. json.zig - Write a
build.zigstep that loops over a list of target triples and callsaddExecutableonce per target, using thestd.Target.Queryhelper to print human-friendly labels. 22
Alternatives & Edge Cases:
- LLVM-backed targets may still behave differently from Zig’s self-hosted codegen. Fall back to
-fllvmwhen you hit nascent architectures. - WASI forbids many syscalls and dynamic allocation patterns. Keep logging terse or gated to avoid blowing the import budget.
- Windows cross-compiles pick the GNU toolchain by default. Add
-msvcor switch ABI if you intend to link against MSVC-provided libraries. 20