Handling errors in C-library callback functions

I’m interacting with a C library where the main method of communicating with the library is through user-specified callback functions. Of course, in Zig, writing something like this is fairly easy

const c = @cImport({
    @cInclude("api/foo.h");
    @cInclude("api/bar.h");
};

fn fooCallback(
    args: ?[*:0]const u8,
    context: ?*anyopaque,
) callconv(.c) c_int {
    // Implementation
}

fn barCallback(
    args: ?[*:0]const u8,
    context: ?*anyopaque,
) callconv(.c) c_int {
    // Implementation
}

// snip 

var foo_context: FooCallbackContext = .init();
c.api_register_foo_callback(fooCallback, &foo_context);

var bar_context: BarCallbackContext = .init();
c.api_register_bar_callback(barCallback, &bar_context);

//etc etc

Now I’m left with the problem that writing those callback functions is a bit of a pain because of the type restriction on the function pointer.

Within the function, every error union needs to be handled and turned into an appropriate c_int value, and that adds a lot of noise to unhappy paths in code that is already very noise-prone:

const result = try failable(arg1, arg2);

becomes something like

const result = failable(arg1, arg2) catch |err| return switch |err| {
    error.case1 => c.api_case1,
    error.case2 => c.api_case2,
    error.case3 => c.api_case_not_quite_3_but_close_enough_I_guess,
    else => c.api_case_unexpected,
};

Are there any good patterns for handling this kind of thing?

The switch is probably the most pragmatic solution, I’m not sure why you think it adds a lot of noise. If you mean that you don’t want to have to duplicate the mapping from Zig errors to integer status codes at every point you want to use try you could try delegating the mapping to an outer wrapper function:

fn myCallbackC(args: ?[*:0]const u8, context: ?*anyopaque) callconv(.c) c_int {
    myCallbackZig(args, @ptrCast(@alignCast(context.?))) catch |err| {
        return switch (err) {
            error.Foo => c.api_result_foo,
            error.Bar => c.api_result_bar,
            // ...
            else => c.api_result_unknown,
        };
    };
    return c.api_result_success;
}

fn myCallbackZig(args: ?[*:0]const u8, context: *MyCallbackContext) !void {
    const foo = try getFoo(...);
    const bar = try getBar(...);
    // ....
}

If you have multiple distinct callbacks but they all need to be handled the same way you could probably figure out a way to make a generic wrapper so that you only need to write the error mapping once.

You might also want to consider whether or not you want to log the error and return trace in the outer wrapper function. If the callbacks are synchronous it is possible to preserve errors and return traces across extern calls with a bit of advanced tinkering.

2 Likes

I think this is workable for my use case. Don’t know why I didn’t think of it. Thanks for the help. :sweat_smile: