Chapter 58Mapping C Rust Idioms

附录D. C/Rust 习语映射到 Zig 结构

概述

C 和 Rust 建立了许多 Zig 开发者带来的心智模型:手动的 malloc/free、RAII 析构函数、Option<T>Result<T, E> 和特征对象。本附录将这些习惯用法转换为惯用 Zig,使您能够移植真实代码库而不与语言特性对抗。

Zig 收紧的指针对齐规则(@alignCast)和改进的分配器诊断在封装外部 API 时反复出现。v0.15.2

学习目标

  • 将手动资源清理替换为 defer/errdefer,同时保持您期望的 C 式控制。
  • 以可组合的方式使用 Zig 可选类型和错误联合来表达受 Rust 启发的 Option/Result 逻辑。
  • 将基于回调或特征的多态性适配到 Zig 的 comptime 泛型和指针桩。

转换 C 资源生命周期

C 程序员习惯性地将每个 malloc 与相应的 free 配对。Zig 允许您使用 errdefer 和结构化错误集来编码相同意图,这样即使验证失败缓冲区也永远不会泄漏。4 下面的示例对比了直接转换和自动释放内存的 Zig 优先辅助函数,突出了分配器错误如何与域错误组合。mem.zig

Zig
//! Reinvents a C-style buffer duplication with Zig's defer-based cleanup.
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 的 Option<T> 清晰地映射到 Zig 的 ?T,而 Result<T, E> 成为带有丰富标签而非字符串类型消息的错误联合(E!T)。4 这个配方从换行符分隔的文本中提取配置值,首先使用可选搜索,然后使用特定于域的错误联合,将解析失败转换为对调用者友好的诊断。fmt.zig

Zig
//! Mirrors Rust's Option and Result idioms with Zig optionals and error unions.
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 处理较慢且可能失败的工作——镜像了将 Option::mapResult::map_err 分离的 Rust 模式。17

桥接特征和函数指针

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

Zig
//! Converts a C function-pointer callback pattern into type-safe Zig shims.
const std = @import("std");

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

fn callLegacy(callback: LegacyCallback, ctx: *anyopaque) void {
    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
  • 使用暴露小型接口(handleformat 等)的 comptime struct 实现特征风格行为,让优化器内联调用站点。15

注意事项和警告

  • 手动分配辅助函数应显式暴露 std.mem.Allocator.Error,以便调用者可以透明地继续传播失败。
  • 移植依赖 drop 语义的 Rust crates 时,审计每个分支中的 returnbreak 表达式——Zig 不会自动调用析构函数。36
  • 函数指针桩必须遵循调用约定;如果 C API 期望 extern fn,请相应地注释您的桩,然后再发布。33

练习

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

替代方案和边界情况

  • 一些 C 库期望您使用其自定义函数进行分配——将这些分配器封装在实现 std.mem.Allocator 接口的桩中,这样您其余的 Zig 代码保持统一。10
  • 移植拥有堆数据的 Rust Option<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.