Calling variadic C functions

I’m using the IUP UI library in Zig through cImport. So far it’s going well. But I came across a variadic function in the API and was having trouble calling it.
No matter what I try I just seem to get a seg fault doing it. It’s not necessary for me to use it. But I’d like to learn how you’re supposed to call them.

The translated definition:

pub extern fn IupSetAtt(handle_name: [*c]const u8, ih: ?*Ihandle, name: [*c]const u8, ...) ?*Ihandle;

The function expects the same type as “name” for each extra value. Except for the last one, which it expects to be null.
Any help understanding this would be appreciated.

2 Likes

something like this might work?

fn setAtt(handle_name: [*:0]const u8, ih: ?*Ihandle, name: [*:0]const u8, args: anytype) ?*Ihandle {
    // should do some comptime programming here to enforce that `args` is a tuple of `[*:0]const u8`
    return @call(.auto, IupSetAtt, .{ handle_name, ih, name} ++ args ++ .{null});
}

I’ve tried something like this. But it seems you’re not allowed to pass a null type to a variadic function. Unless there’s a type/cast I need to use to pass an equivalent to it.

Ok, I got it figured out. I was the calling the variadic function correctly. But was just not passing the null parameter right. Casting null to a C pointer works. :smiley:

c.IupSetAtt(null, main_dialog, "TITLE", "Hello", @as([*c]const u8, null));
4 Likes

Note that while it generates identical code, it would be more correct and descriptive to cast it using @as(?[*:0]const u8, null). You should never actually use C pointers in code you write yourself (coercing the value to the correct pointer type is always the better option) and as a matter of fact, writing the token [*c] anywhere in non-translated code will soon be a compile error.

This works if you change .{null} to .{@as(?[*:0]const u8, null)}. But if you’re going to wrap the function, you could go even further and make it even more type safe by ensuring that names are always followed by values (IupSetAttributes).

As a fun exercise, this is what a complete ziggified wrapper for IupSetAtt would look like:

/// Usage: `IupSetAtt("Dialog", dialog, .{ "TITLE", "Test" })`
pub fn IupSetAtt(handle_name: ?[*:0]const u8, ih: *Ihandle, name_value_pairs: anytype) ?*Ihandle {
    // Create a tuple of '[*:0]const u8' that we can coerce 'name_value_pairs' to.
    // This makes the function easier to call by allowing passing names/values as '[:0]const u8'
    // slices; if we didn't do this coercion, the function would only accept pointers, which would
    // likely require the user to litter their code with '@as([*:0]const u8, ...)' statements.
    const CStringNameValuePairs = comptime make_type: {
        var info = @typeInfo(@TypeOf(name_value_pairs)).Struct;
        if (!info.is_tuple or info.fields.len == 0 or info.fields.len % 2 != 0) {
            @compileError("'name_value_pairs' must be a tuple with an even length >= 2");
        }
        var c_string_fields: [info.fields.len]std.builtin.Type.StructField = undefined;
        for (&c_string_fields, info.fields) |*c_string_field, nvp_field| {
            c_string_field.* = .{
                .name = nvp_field.name,
                .type = [*:0]const u8,
                .default_value = null,
                .is_comptime = false,
                .alignment = @alignOf([*:0]const u8),
            };
        }
        info.fields = &c_string_fields;
        break :make_type @Type(.{ .Struct = info });
    };

    return @call(
        .auto,
        c.IupSetAtt,
        .{ handle_name, ih } ++ @as(CStringNameValuePairs, name_value_pairs) ++ .{@as(?[*:0]const u8, null)},
    );
}
3 Likes

Thank you for this example and advice on the pointer definition! Makes good sense of it for me.
I like that they’re making that a compile error. It definitely felt like a type I shouldn’t use directly.
Something I’ve been loving about Zig is how specific it makes you define your code. And tells you off if you aren’t specific enough or are incorrect. Much better than learning C/C++ magic. xD

1 Like