概述
借助上一章的跨编译机制(参见第41章),我们现在可以组装一个完整的WASI项目,该项目使用单个build.zig文件编译到本机和WebAssembly目标。本章构建一个小型的日志分析器CLI,它读取输入、处理输入并发出汇总统计信息——这种功能清晰地映射到WASI的文件和stdio功能(参见wasi.zig)。您只需编写一次应用程序,然后使用Wasmtime或Wasmer等运行时生成和测试Linux可执行文件和.wasm模块(参见v0.15.2)。
构建系统将定义多个目标,每个目标都有自己的产物,您将连接运行步骤,根据目标自动启动正确的运行时(参见第22章)。到最后,您将拥有一个可工作的模板,用于将可移植命令行工具作为本机二进制文件和WASI模块进行发布。
学习目标
- 构建一个共享源代码的Zig项目,该项目可以干净地编译到
x86_64-linux和wasm32-wasi两个平台(参见Target.zig)。 - 在
build.zig中集成多个addExecutable目标,采用不同的优化和命名策略(参见Build.zig)。 - 配置带有运行时检测(本机 vs Wasmtime/Wasmer)的运行步骤,并向最终的二进制文件传递参数(参见第22章)。
- 在原生和WASI环境中测试相同的逻辑路径,验证跨平台行为(参见#Command-line-flags)。
项目结构
我们将分析器组织成一个单包工作区,其中src/目录包含入口点和分析逻辑。build.zig将创建两个产物:log-analyzer-native和log-analyzer-wasi。
目录布局
42-log-analyzer/
├── build.zig
├── build.zig.zon
└── src/
├── main.zig
└── analysis.zig由于我们没有外部依赖,build.zig.zon是最小的;它用作潜在未来打包的元数据(参见第21章)。
包元数据
.{
// Package identifier used in dependencies and imports
// Must be a valid Zig identifier (no hyphens or special characters)
.name = .log_analyzer,
// Semantic version of this package
// Format: major.minor.patch following semver conventions
.version = "0.1.0",
// Minimum Zig compiler version required to build this package
// Ensures compatibility with language features and build system APIs
.minimum_zig_version = "0.15.2",
// List of paths to include when publishing or distributing the package
// Empty string includes all files in the package directory
.paths = .{
"",
},
// Unique identifier generated by the package manager for integrity verification
// Used to detect changes and ensure package authenticity
.fingerprint = 0xba0348facfd677ff,
}
.minimum_zig_version字段可防止使用缺少0.15.2版本中引入的WASI改进的旧编译器进行意外构建。
构建系统设置
我们的build.zig定义了两个共享相同根源文件但针对不同平台的可执行文件。我们还为WASI二进制文件添加了一个自定义运行步骤,以检测可用的运行时。
多目标构建脚本
const std = @import("std");
/// Build script for log-analyzer project demonstrating native and WASI cross-compilation.
/// Produces two executables: one for native execution and one for WASI runtimes.
pub fn build(b: *std.Build) void {
// Standard target and optimization options from command-line flags
// These allow users to specify --target and --optimize when building
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
// Native executable: optimized for fast runtime performance on the host system
// This target respects user-specified target and optimization settings
const exe_native = b.addExecutable(.{
.name = "log-analyzer-native",
.root_module = b.createModule(.{
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
}),
});
// Register the native executable for installation to zig-out/bin
b.installArtifact(exe_native);
// WASI executable: cross-compiled to WebAssembly with WASI support
// Uses ReleaseSmall to minimize binary size for portable distribution
const wasi_target = b.resolveTargetQuery(.{
.cpu_arch = .wasm32,
.os_tag = .wasi,
});
const exe_wasi = b.addExecutable(.{
.name = "log-analyzer-wasi",
.root_module = b.createModule(.{
.root_source_file = b.path("src/main.zig"),
.target = wasi_target,
.optimize = .ReleaseSmall, // Prioritize small binary size over speed
}),
});
// Register the WASI executable for installation to zig-out/bin
b.installArtifact(exe_wasi);
// Create run step for native target that executes the compiled binary directly
const run_native = b.addRunArtifact(exe_native);
// Ensure the binary is built and installed before attempting to run it
run_native.step.dependOn(b.getInstallStep());
// Forward any command-line arguments passed after -- to the executable
if (b.args) |args| {
run_native.addArgs(args);
}
// Register the run step so users can invoke it with `zig build run-native`
const run_native_step = b.step("run-native", "Run the native log analyzer");
run_native_step.dependOn(&run_native.step);
// Create run step for WASI target with automatic runtime detection
// First, attempt to detect an available WASI runtime (wasmtime or wasmer)
const run_wasi = b.addSystemCommand(&.{"echo"});
const wasi_runtime = detectWasiRuntime(b) orelse {
// If no runtime is found, provide a helpful error message
run_wasi.addArg("ERROR: No WASI runtime (wasmtime or wasmer) found in PATH");
const run_wasi_step = b.step("run-wasi", "Run the WASI log analyzer (requires wasmtime or wasmer)");
run_wasi_step.dependOn(&run_wasi.step);
return;
};
// Construct the command to run the WASI binary with the detected runtime
const run_wasi_cmd = b.addSystemCommand(&.{wasi_runtime});
// Both wasmtime and wasmer require the 'run' subcommand
if (std.mem.eql(u8, wasi_runtime, "wasmtime") or std.mem.eql(u8, wasi_runtime, "wasmer")) {
run_wasi_cmd.addArg("run");
// Grant access to the current directory for file I/O operations
run_wasi_cmd.addArg("--dir=.");
}
// Add the WASI binary as the target to execute
run_wasi_cmd.addArtifactArg(exe_wasi);
// Forward user arguments after the -- separator to the WASI program
if (b.args) |args| {
run_wasi_cmd.addArg("--");
run_wasi_cmd.addArgs(args);
}
// Ensure the WASI binary is built before attempting to run it
run_wasi_cmd.step.dependOn(b.getInstallStep());
// Register the WASI run step so users can invoke it with `zig build run-wasi`
const run_wasi_step = b.step("run-wasi", "Run the WASI log analyzer (requires wasmtime or wasmer)");
run_wasi_step.dependOn(&run_wasi_cmd.step);
}
/// Detect available WASI runtime in the system PATH.
/// Checks for wasmtime first, then wasmer as a fallback.
/// Returns the name of the detected runtime, or null if neither is found.
fn detectWasiRuntime(b: *std.Build) ?[]const u8 {
// Attempt to locate wasmtime using the 'which' command
var exit_code: u8 = undefined;
_ = b.runAllowFail(&.{ "which", "wasmtime" }, &exit_code, .Ignore) catch {
// If wasmtime is not found, try wasmer as a fallback
_ = b.runAllowFail(&.{ "which", "wasmer" }, &exit_code, .Ignore) catch {
// Neither runtime was found in PATH
return null;
};
return "wasmer";
};
// wasmtime was successfully located
return "wasmtime";
}
$ zig build(成功时无输出;产物安装到zig-out/bin/)WASI目标设置-OReleaseSmall以最小化模块大小,而本机目标使用-OReleaseFast以提高运行时速度——展示了每个产物的优化控制。
分析逻辑
分析器读取整个日志内容,按行拆分,计算严重性关键字(ERROR, WARN, INFO)的出现次数,并打印摘要。我们将解析部分拆分到analysis.zig中,以便可以独立于I/O进行单元测试。
核心分析模块
// This module provides log analysis functionality for counting severity levels in log files.
// It demonstrates basic string parsing and struct usage in Zig.
const std = @import("std");
// LogStats holds the count of each log severity level found during analysis.
// All fields are initialized to zero by default, representing no logs counted yet.
pub const LogStats = struct {
info_count: u32 = 0,
warn_count: u32 = 0,
error_count: u32 = 0,
};
/// Analyze log content, counting severity keywords.
/// Returns statistics in a LogStats struct.
pub fn analyzeLog(content: []const u8) LogStats {
// Initialize stats with all counts at zero
var stats = LogStats{};
// Create an iterator that splits the content by newline characters
// This allows us to process the log line by line
var it = std.mem.splitScalar(u8, content, '\n');
// Process each line in the log content
while (it.next()) |line| {
// Count occurrences of severity keywords
// indexOf returns an optional - if found, we increment the corresponding counter
if (std.mem.indexOf(u8, line, "INFO")) |_| {
stats.info_count += 1;
}
if (std.mem.indexOf(u8, line, "WARN")) |_| {
stats.warn_count += 1;
}
if (std.mem.indexOf(u8, line, "ERROR")) |_| {
stats.error_count += 1;
}
}
return stats;
}
// Test basic log analysis with multiple severity levels
test "analyzeLog basic counting" {
const input = "INFO startup\nERROR failed\nWARN retry\nINFO success\n";
const stats = analyzeLog(input);
// Verify each severity level was counted correctly
try std.testing.expectEqual(@as(u32, 2), stats.info_count);
try std.testing.expectEqual(@as(u32, 1), stats.warn_count);
try std.testing.expectEqual(@as(u32, 1), stats.error_count);
}
// Test that empty input produces zero counts for all severity levels
test "analyzeLog empty input" {
const input = "";
const stats = analyzeLog(input);
// All counts should remain at their default zero value
try std.testing.expectEqual(@as(u32, 0), stats.info_count);
try std.testing.expectEqual(@as(u32, 0), stats.warn_count);
try std.testing.expectEqual(@as(u32, 0), stats.error_count);
}
通过接受切片内容,analyzeLog保持了简单和可测试性。main.zig处理文件读取;该函数只处理文本(参见mem.zig)。
主入口点
入口点解析命令行参数,读取整个文件内容(或标准输入),委托给analyzeLog,并打印结果。本机和WASI构建都共享此代码路径;WASI通过其虚拟文件系统或标准输入处理文件访问。
主源文件
const std = @import("std");
const analysis = @import("analysis.zig");
pub fn main() !void {
// Initialize general-purpose allocator for dynamic memory allocation
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
// Parse command-line arguments into an allocated slice
const args = try std.process.argsAlloc(allocator);
defer std.process.argsFree(allocator, args);
// Check for optional --input flag to specify a file path
var input_path: ?[]const u8 = null;
var i: usize = 1; // Skip program name at args[0]
while (i < args.len) : (i += 1) {
if (std.mem.eql(u8, args[i], "--input")) {
i += 1;
if (i < args.len) {
input_path = args[i];
} else {
std.debug.print("ERROR: --input requires a file path\n", .{});
return error.MissingArgument;
}
}
}
// Read input content from either file or stdin
// Using labeled blocks to unify type across both branches
const content = if (input_path) |path| blk: {
std.debug.print("analyzing: {s}\n", .{path});
// Read entire file content with 10MB limit
break :blk try std.fs.cwd().readFileAlloc(allocator, path, 10 * 1024 * 1024);
} else blk: {
std.debug.print("analyzing: stdin\n", .{});
// Construct File handle directly from stdin file descriptor
const stdin = std.fs.File{ .handle = std.posix.STDIN_FILENO };
// Read all available stdin data with same 10MB limit
break :blk try stdin.readToEndAlloc(allocator, 10 * 1024 * 1024);
};
defer allocator.free(content);
// Delegate log analysis to the analysis module
const stats = analysis.analyzeLog(content);
// Print summary statistics to stderr (std.debug.print)
std.debug.print("results: INFO={d} WARN={d} ERROR={d}\n", .{
stats.info_count,
stats.warn_count,
stats.error_count,
});
}
--input标志允许使用文件进行测试;省略它会从标准输入读取,WASI运行时可以轻松地通过管道传递。请注意,WASI文件系统访问需要运行时明确授予功能权限(参见posix.zig)。
构建和运行
源代码完成后,我们可以构建两个目标并排运行它们,以确认行为一致。
本机执行
$ zig build
$ echo -e "INFO startup\nERROR failed\nWARN retry\nINFO success" > sample.log
$ ./zig-out/bin/log-analyzer-native --input sample.loganalyzing: sample.log
results: INFO=2 WARN=1 ERROR=1使用Wasmer进行WASI执行(标准输入)
$ zig build
$ echo -e "INFO startup\nERROR failed\nWARN retry\nINFO success" | wasmer run zig-out/bin/log-analyzer-wasi.wasmanalyzing: stdin
results: INFO=2 WARN=1 ERROR=1WASI标准输入管道在各种运行时中都能可靠工作。使用--input进行文件访问需要功能授权(--dir或--mapdir),这因运行时实现而异,并且在preview1中可能存在限制。
用于比较的本机标准输入测试
$ echo -e "INFO startup\nERROR failed\nWARN retry\nINFO success" | ./zig-out/bin/log-analyzer-nativeanalyzing: stdin
results: INFO=2 WARN=1 ERROR=1本机和WASI在从标准输入读取时产生相同的输出,展示了命令行工具真正的源代码级可移植性。
使用运行步骤
build.zig包含两个目标的运行步骤定义。直接调用它们:
$ zig build run-native -- --input sample.loganalyzing: sample.log
results: INFO=2 WARN=1 ERROR=1$ echo -e "INFO test" | zig build run-wasianalyzing: stdin
results: INFO=1 WARN=0 ERROR=0run-wasi步骤会自动选择已安装的WASI运行时(Wasmtime或Wasmer),如果两者都不可用则会报错。请参阅build.zig中的detectWasiRuntime帮助程序。
二进制文件大小比较
使用-OReleaseSmall构建的WASI模块会产生紧凑的产物:
$ ls -lh zig-out/bin/log-analyzer-*-rwxrwxr-x 1 user user 7.9M Nov 6 14:29 log-analyzer-native
-rwxr--r-- 1 user user 18K Nov 6 14:29 log-analyzer-wasi.wasm.wasm模块明显更小(18KB vs 7.9MB),因为它省略了本机操作系统集成,并依赖宿主运行时进行系统调用,使其成为边缘部署或浏览器环境的理想选择。
扩展项目
此模板可作为针对WASI的更复杂CLI工具的基础:
注意与警告
- WASI preview1(当前快照)缺少网络、线程,并且文件系统功能有限。Stdin/stdout可靠工作,但文件访问需要运行时特定的功能授权。
- 0.15.2版本中引入的
zig libc在musl和wasi-libc之间共享实现,提高了跨平台的一致性,并使readToEndAlloc等函数能够在不同平台上以相同方式工作。 - WASI运行时的权限模型各不相同。Wasmer的
--mapdir在测试中存在问题,而stdin管道则普遍适用。在为WASI设计CLI工具时,应优先考虑stdin。
练习
- 添加一个
--format json标志,用于发出{"info": N, "warn": N, "error": N}而不是纯文本摘要,然后通过管道传递给jq以验证输出。 - 使用单元测试扩展
analysis.zig,以验证不区分大小写的匹配(例如,"info"和"INFO"都计数),展示std.ascii.eqlIgnoreCase(参见第13章)。 - 为
wasm32-freestanding(无WASI)创建第三个构建目标,该目标通过@export将分析器公开为可从JavaScript调用的导出函数(参见wasm.zig)。 - 使用大型日志文件(生成10万行)对本机与WASI的执行时间进行基准测试,比较启动开销和吞吐量(参见第40章)。
限制、替代方案和边缘情况
- 如果您需要线程,WASI preview2(组件模型)引入了实验性的并发原语。请查阅上游WASI规范以获取迁移路径。
- 对于浏览器目标,请切换到
wasm32-freestanding并使用JavaScript互操作(@export/@extern)而不是WASI系统调用(参见第33章)。 - 一些WASI运行时(例如Wasmedge)支持非标准的扩展,如套接字或GPU访问。为获得最大可移植性,请坚持使用preview1,或明确记录特定于运行时的依赖项。