Chapter 58Mapping C Rust Idioms

附录D. 将 C/Rust 惯用法映射为 Zig 结构

概览

C 与 Rust 形成了许多 Zig 开发者带来的心智模型:手动malloc/free、RAII 析构、Option<T>Result<T, E>与 trait 对象。本附录将这些习惯转换为地道的 Zig,以便你无需与语言“搏斗”就能移植真实代码库。

Zig 收紧的指针对齐规则(@alignCast)与更好的分配器诊断在包装外部 API 时频繁出现。v0.15.2

学习目标

  • defer/errdefer 交换手动资源清理,同时保留你从 C 期望的控制。
  • 以可组合的方式用 Zig 可选值和错误联合表达受 Rust 启发的 Option/Result 逻辑。
  • 将基于回调或 trait 的多态性适配到 Zig 的 comptime 泛型和指针垫片。

翻译 C 的资源生命周期

C 程序员习惯性地将每个 malloc 与匹配的 free 配对。Zig 让你用 errdefer 和结构化错误集编码相同意图,使缓冲即使在验证失败时也从不泄漏。4 下面的示例对比了直接翻译与 Zig 优先的自动释放内存辅助函数,突出显示分配器错误如何与域错误组合。mem.zig

Zig
// ! 使用 Zig 基于 defer 的清理机制重新实现 C 风格的缓冲区复制。
const std = @import("std");

pub const NormalizeError = error{InvalidCharacter} || std.mem.Allocator.Error;

pub fn duplicateAlphaUpper(allocator: std.mem.Allocator, input: []const u8) NormalizeError![]u8 {
    const buffer = try allocator.alloc(u8, input.len);
    errdefer allocator.free(buffer);

    for (buffer, input) |*dst, src| switch (src) {
        'a'...'z', 'A'...'Z' => dst.* = std.ascii.toUpper(src),
        else => return NormalizeError.InvalidCharacter,
    };

    return buffer;
}

pub fn cStyleDuplicateAlphaUpper(allocator: std.mem.Allocator, input: []const u8) NormalizeError![]u8 {
    const buffer = try allocator.alloc(u8, input.len);
    var ok = false;
    defer if (!ok) allocator.free(buffer);

    for (buffer, input) |*dst, src| switch (src) {
        'a'...'z', 'A'...'Z' => dst.* = std.ascii.toUpper(src),
        else => return NormalizeError.InvalidCharacter,
    };

    ok = true;
    return buffer;
}

test "duplicateAlphaUpper releases buffer on failure" {
    const allocator = std.testing.allocator;
    try std.testing.expectError(NormalizeError.InvalidCharacter, duplicateAlphaUpper(allocator, "zig-0"));
}

test "c style duplicate succeeds with valid input" {
    const allocator = std.testing.allocator;
    const dup = try cStyleDuplicateAlphaUpper(allocator, "zig");
    defer allocator.free(dup);
    try std.testing.expectEqualStrings("ZIG", dup);
}
运行
Shell
$ zig test 01_c_style_cleanup.zig
输出
Shell
All 2 tests passed.

显式的NormalizeError联合同时跟踪分配器失败与校验失败,这一模式在第 10 章的分配器巡礼中被广泛推荐。

映射 Rust 的 Option 与 Result 类型

Rust’s Option<T> maps cleanly to Zig’s ?T, while Result<T, E> becomes an error union (E!T) with rich tags instead of stringly typed messages. 4 This recipe pulls a configuration value from newline-separated text, first with an optional search and then with a domain-specific error union that converts parsing failures into caller-friendly diagnostics. fmt.zig

Zig
// ! 使用 Zig 的可选类型和错误联合体模仿 Rust 的 Option 和 Result 习惯用法。
const std = @import("std");

pub fn findPortLine(env: []const u8) ?[]const u8 {
    var iter = std.mem.splitScalar(u8, env, '\n');
    while (iter.next()) |line| {
        if (std.mem.startsWith(u8, line, "PORT=")) {
            return line["PORT=".len..];
        }
    }
    return null;
}

pub const ParsePortError = error{
    Missing,
    Invalid,
};

pub fn parsePort(env: []const u8) ParsePortError!u16 {
    const raw = findPortLine(env) orelse return ParsePortError.Missing;
    return std.fmt.parseInt(u16, raw, 10) catch ParsePortError.Invalid;
}

test "findPortLine returns optional when key absent" {
    try std.testing.expectEqual(@as(?[]const u8, null), findPortLine("HOST=zig-lang"));
}

test "parsePort converts parse errors into domain error set" {
    try std.testing.expectEqual(@as(u16, 8080), try parsePort("PORT=8080\n"));
    try std.testing.expectError(ParsePortError.Missing, parsePort("HOST=zig"));
    try std.testing.expectError(ParsePortError.Invalid, parsePort("PORT=xyz"));
}
运行
Shell
$ zig test 02_rust_option_result.zig
输出
Shell
All 2 tests passed.

由于 Zig 将“是否存在”的发现与错误传播分离,你可以复用findPortLine进行快速路径检查,同时让parsePort处理较慢且可能失败的工作——这对应于 Rust 将Option::mapResult::map_err分开的惯例。17

桥接 Trait 与函数指针

C 和 Rust 都依赖回调——带有上下文有效负载的原始函数指针或带有显式 self 参数的 trait 对象。Zig 用 *anyopaque 垫片加上 comptime 适配器建模相同的抽象,因此你可以保持类型安全和零成本间接寻址。33 下面的示例展示了一个 C 风格回调和一个类似 trait 的 handle 方法,通过相同的传统桥接复用,依赖于 Zig 的指针转换和对齐断言。builtin.zig

Zig
// ! 将 C 函数指针回调模式转换为类型安全的 Zig shim。
const std = @import("std");

pub const LegacyCallback = *const fn (ctx: *anyopaque) void;

fn callLegacy(callback: LegacyCallback, ctx: *anyopaque) void {
    callLegacy(callback, ctx);
}

const Counter = struct {
    value: u32,
};

fn incrementShim(ctx: *anyopaque) void {
    const counter: *Counter = @ptrCast(@alignCast(ctx));
    counter.value += 1;
}

pub fn incrementViaLegacy(counter: *Counter) void {
    callLegacy(incrementShim, counter);
}

pub fn dispatchWithContext(comptime Handler: type, ctx: *Handler) void {
    const shim = struct {
        fn invoke(raw: *anyopaque) void {
            const typed: *Handler = @ptrCast(@alignCast(raw));
            Handler.handle(typed);
        }
    };

    callLegacy(shim.invoke, ctx);
}

const Stats = struct {
    total: u32 = 0,

    fn handle(self: *Stats) void {
        self.total += 2;
    }
};

test "incrementViaLegacy integrates with C-style callback" {
    var counter = Counter{ .value = 0 };
    incrementViaLegacy(&counter);
    try std.testing.expectEqual(@as(u32, 1), counter.value);
}

test "dispatchWithContext adapts trait-like handle method" {
    var stats = Stats{};
    dispatchWithContext(Stats, &stats);
    try std.testing.expectEqual(@as(u32, 2), stats.total);
}
运行
Shell
$ zig test 03_callback_bridge.zig
输出
Shell
All 2 tests passed.

额外的@alignCast调用反映了 0.15.2 的“地雷”——指针转换现在会断言对齐,因此在包装来自 C 库的*anyopaque句柄时请保留这些断言。v0.15.2

随手可用的模式

  • errdefer 将分配器清理保持在局部,同时暴露类型化结果,使 C 端口保持无泄漏,而无需扩展的 goto 块。4
  • 尽早将外部枚举转换为 Zig 错误联合,然后在你的模块边界重新导出专注的错误集。57
  • 用暴露小接口的 comptime 结构体(handleformat 等)实现 trait 风格行为,让优化器内联调用点。15

注意与警示

  • 手动分配辅助函数应显式暴露 std.mem.Allocator.Error,使调用者能够透明地继续传播失败。
  • 移植依赖 drop 语义的 Rust crate 时,请审查每个分支的returnbreak表达式——Zig 不会自动调用析构。36
  • 函数指针垫片必须遵守调用约定;若 C API 期望 extern fn,请在发布前相应地为你的垫片添加注释。33

练习

  • 扩展规范化辅助函数以接受下划线,方法是将它们转换为连字符,并添加涵盖成功和失败情况的测试。10
  • 修改 parsePort 以返回包含主机和端口的结构体,然后记录组合错误联合如何扩展。57
  • 泛化 dispatchWithContext 使其接受处理器的编译期列表,镜像 Rust 的 trait 对象虚表。15

替代方案与边界情况

  • 一些 C 库期望你使用其自定义函数分配——将这些分配器包装在实现 std.mem.Allocator 接口的垫片中,使你其余的 Zig 代码保持一致。10
  • 移植持有堆数据的 RustOption<T>时,可考虑返回“切片 + 长度哨兵”,而非复制所有权语义。3
  • 若你的回调桥跨越线程,请在改变共享状态前添加第 29 章的同步原语。29

Help make this chapter better.

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