How to handle architecture-dependent changes in C-import naming

I am cross compiling libbpf and using it my project. As part of that, one can enroll a logger to report errors when loading and validating ebpf for instance. This can be done easily with zig (x86) by defining a function such as:

fn log(level: c.enum_libbpf_print_level, fmt: [*c]const u8, ap: [*c]c.struct___va_list_tag_1) callconv(.c) c_int {
    ...
}

However, on aarch64 it wont compile as the type of the last argument type gets a different name. Thus, I need to switch the signature:

fn log(level: c.enum_libbpf_print_level, fmt: [*c]const u8, ap: c.struct___va_list_1) callconv(.c) c_int {
    ...
}

So, it seems that the C-imported names are different depending on the architecture. This all feels a bit brittle, as though the type is arbitrary somehow. I realize it may be a bit “cursed” what I’m trying here, but what are the best ways of working when it comes to issues such as this one?

Something like:

const builtin = @import("builtin");

const Ap = switch (builtin.cpu.arch) {
    .aarch64 => c.struct___va_list_1,
    .x86_64 => c.struct___va_list_tag_1,
    else => |t| @compileError("unsupported architecture " ++ @tagName(t)),
};

fn log(level: c.enum_libbpf_print_level, fmt: [*c]const u8, ap: [*c]Ap) callconv(.c) c_int {
    ...
}

That’s a nice approach yes. I have something similar in my code base as a band-aid fix. Perhaps this stuff is variable-args specific, I wonder I there’s an existing abstraction that handles this built-in?

If these are normal variadic functions on the C side then have a look at C Variadic Functions in the language reference.

The second code block specifically.

zig does support variadic args as it’s needed for c interop, the type you want is std.builtin.VaList.

Which @leecannon effectively reimplemented in their snippet, though the one in std doesn’t rely on libc

see also @cVaStart, @cVaEnd, @cVaArg and @cVaCopy.

You can also do fn(...) like you can in c (requires c calling convention).
But that’s for the function that receives the var args in the first place which is not the log function you are defining. rather it is getting the var args handle which is how var args are propagated to other functions.

Yes, I tried to use this (zig 0.15.1), unfortunately the compiler didn’t quite agree when I replaced the function arguments directly:

fn log(level: c.enum_libbpf_print_level, fmt: [*c]const u8, ap: std.builtin.VaList) callconv(.c) c_int {
 {
    ...
}

aarch64

nix/store/wwjxwysa0igp0kjryg6bsnvzq7b0byi1-zig-0.15.1/lib/std/builtin.zig:899:29: error: disabled due to miscompilations
            .stage2_llvm => @compileError("disabled due to miscompilations"),

x86_64

src/bpf.zig:8:36: error: expected type '*const fn (c_uint, [*c]const u8, [*c]cimport.struct___va_list_tag_1) callconv(.c) c_int', found '*const fn (c_uint, [*c]const u8, builtin.VaListX86_64) callconv(.c) c_int'
        .zig => c.libbpf_set_print(log),
                                   ^~~
src/bpf.zig:8:36: note: pointer type child 'fn (c_uint, [*c]const u8, builtin.VaListX86_64) callconv(.c) c_int' cannot cast into pointer type child 'fn (c_uint, [*c]const u8, [*c]cimport.struct___va_list_tag_1) callconv(.c) c_int'
src/bpf.zig:8:36: note: parameter 2 'builtin.VaListX86_64' cannot cast into '[*c]cimport.struct___va_list_tag_1'
/nix/store/wwjxwysa0igp0kjryg6bsnvzq7b0byi1-zig-0.15.1/lib/std/builtin.zig:876:33: note: struct declared here
pub const VaListX86_64 = extern struct {
                         ~~~~~~~^~~~~~
referenced by:
    init: src/app.zig:40:35
    main: src/main.zig:67:36
    4 reference(s) hidden; use '-freference-trace=6' to see all references
src/bpf.zig:175:51: error: expected type '[*c]cimport.struct___va_list_tag_1', found 'builtin.VaListX86_64'
    const len_c = c.vsnprintf(&buf, buf.len, fmt, ap);

Might be that my case is a degenerate one or that I’m using the API wrong, though :frowning:. Perhaps its also a zig 0.15.1 thing.

I think you’re supposed to use a pointer to VaList.

But, the c code is already translated and using a distinct type, so even if they are compatible it might still complain about them being different.

I’d bet if you manually wrote the bindings for the offending function, using zigs VaList it’d work, but that would be annoying to maintain.

I agree with you that a custom binding may be the way to do. I do indeed get a type error, due to the function signature that is being generated.

pub const libbpf_print_fn_t = ?*const fn (enum_libbpf_print_level, [*c]const u8, [*c]struct___va_list_tag_1) callconv(.c) c_int;
pub extern fn libbpf_set_print(@"fn": libbpf_print_fn_t) libbpf_print_fn_t;

Just to resolve this in case anyone else stumbles onto it, but the most robust answer for me is to just use reflection (rather obvious in hindsight).

pub fn DetermineFunctionArgumentType(comptime func: anytype, comptime index: usize) type {
    return @typeInfo(@typeInfo(@typeInfo(func).optional.child).pointer.child).@"fn".params[index].type.?;
}


const InferredVAListType = DetermineFunctionArgumentType(c.libbpf_print_fn_t, 2)
1 Like