Zig 中的错误载荷
Error payloads in Zig

原始链接: https://srcreigh.ca/posts/error-payloads-in-zig/

本文详细介绍了一种在 Zig 中使用基于 union(enum) 的“Diagnostics”类型的新颖错误处理方法。不同于传统的错误代码,每个函数定义一个 `Diagnostics` 类型,将潜在的错误负载封装为 unioned enum 的成员。这允许提供丰富的、上下文相关的错误信息,而不会显著增加代码量。 核心 `FromUnion` 函数内联生成这些类型,直接从 enum 标签创建错误集合。一个关键特性是 `withContext` 方法,它可以在错误返回期间有效地将错误负载附加到 `Diagnostics` 实例,从而节省大量内存,例如,通过直接存储 SQLite 错误负载,节省了 500 字节。 该系统简化了调用站点处的错误传播。`call` 方法自动处理 diagnostics 实例化和负载复制,减少了样板代码。错误负载易于访问,用于日志记录和详细报告,如 `logBuildError` 函数所示。虽然 Zig 的静态分析器有时需要显式类型注释,但总体而言,该系统提供了一种强大而简洁的方式来管理 Zig 代码中的错误。作者提供了一个 gist 链接到他们的 diagnostics 模块,以供进一步探索。

Hacker News 新闻 | 过去 | 评论 | 提问 | 展示 | 招聘 | 提交 登录 Zig 中的错误载荷 (srcreigh.ca) 5 分,由 srcreigh 55 分钟前发布 | 隐藏 | 过去 | 收藏 | 讨论 帮助 指南 | 常见问题 | 列表 | API | 安全 | 法律 | 申请 YC | 联系 搜索:
相关文章

原文

I do error payloads in Zig by making a union(enum)-based Diagnostics type for each function. These types have special methods which remove code bloat at call sites. A Diagnostics type can be defined inline, and the errorset can be generated inline from the Diagnostic’s enum tag.

pub fn scan(
    db: *c.sqlite,
    diag: *diagnostics.FromUnion(union(enum) {
        SqliteError: sqlite.ErrorPayload,
        OutOfMemory: void,
        LoadPluginsError: diagnostics.OfFunction(transforms.loadPlugins).ErrorPayload(error.LoadPluginsError),
    }),
): diagnostics.Error(@TypeOf(diag))!void {
    // ...
}

My diagnostics module as a gist

The generated type is a wrapper around an optional payload. It generates an error set type from the union(enum) fields.

// diagnostics.zig
pub fn FromUnion(comptime _Payload: type) type {
    return struct {
        pub const Payload = _Payload;
        pub const Error = ErrorSetFromEnum(std.meta.FieldEnum(Payload));

        payload: ?Payload = null,

        // ... methods ...
    };
}

The first thing you will want to do is set a payload while you return an error. For this, there is the withContext method.

pub fn countRows(
    alloc: std.mem.Allocator,
    db: *c.sqlite,
    opts: Options,
    diag: *diagnostics.FromUnion(union(enum) {
        SqliteError: sqlite.ErrorPayload,
        OutOfMemory: void,
    }),
) !usize {
    const st = sqlite.prepareStmt(
        alloc,
        db,
        "SELECT COUNT(*) FROM {0s} WHERE ({1s})",
        .{ opts.table_name, opts.where_expr },
    ) catch |err| return switch (err) {
        error.SqliteError => diag.withContext(error.SqliteError, .init(db)),
        error.OutOfMemory => error.OutOfMemory,
    };

    // ...
}

Here, sqlite.ErrorPayload.init saves 500 bytes of error message from sqlite. That payload gets saved to diag and the error is returned.

You would expect callsites to need tons of boilerplate, but it’s actually very common to just need to value copy a payload from one diag to another, and this can be done in a single line of code.

pub const BuildDiagnostics = diagnostics.FromUnion(union(enum) {
    SqliteError: sqlite.ErrorPayload,
    OutOfMemory: void,
    // ... 15 more ...
});

pub fn build(..., diag: *BuildDiagnostics) !void {
    // Choose N chunks
    const n_rows = try diag.call(countRows, .{ alloc, db, opts });
    const n_chunks = @max(1, n_rows / opts.chunk_size);
}

The countRows func needs 4 arguments, but the tuple only has 3. The call method inspects the type of countRows to determine the type of its diag arg, instantiates the diag, calls countRows, and if there is an error, copies the error payload to the *BuildDiagnostics.

Written explicitly, this call would be around 5 lines of code.

pub fn build(..., diag: *BuildDiagnostics) !void {
    // Choose N chunks
    var count_rows_diag: diagnostics.OfFunction(countRows) = .{};
    const n_rows = countRows(alloc, db, opts, &count_rows_diag) catch |err| return switch (err) {
        error.SqliteError => diag.withContext(error.SqliteError, count_rows_diag.get(error.SqliteError)),
        error.OutOfMemory => error.OutOfMemory,
    }
    const n_chunks = @max(1, n_rows / opts.chunk_size);
}

At the edges, the error payload is accessible for logging or other purposes.

fn logBuildError(diag: build.BuildDiagnostics, err: build.BuildDiagnostics.Error) void {
    switch (err) {
        error.LoadPluginError => if (diag.get(error.LoadPluginError)) |info| {
            std.log.err("failed to load plugin '{s}': {s}", .{ info.name, @errorName(info.err) });
        } else {
            std.log.err("failed to load plugin: unknown error", .{});
        },

        // ... (handle many other errors) ...
    }
}

ZLS can’t infer the result of the diag.call invocations, so it can be useful to put in an explicit type annotations.

My diagnostics module as a gist

联系我们 contact @ memedata.com