Null terminated array of strings from C function

My code is calling the jack API. The function jack_get_ports is giving me a headache. The documentation says this function returns a null terminated array of ports (actually the port’s names).

This is the function’s signature generated by @cImport.

pub extern fn jack_get_ports(client: ?*jack_client_t, port_name_pattern: [*c]const u8, type_name_pattern: [*c]const u8, flags: c_ulong) [*c][*c]const u8;

Apparently cImport missed the null terminated part ?

const ports = c.jack_get_ports(self.client, "", "", c.JackPortIsPhysical | c.JackPortIsOutput);
for (ports) |p| {
    _ = p;
}

This won’t compile because ports has no upper bound - I guess that’s because the null terminated part is missing from the type declaration.

So, should I coerce it to the actual type ? Is it [*:0][*c]const u8 ? I must admit I’m a bit lost with pointer juggling here …

const ports = c.jack_get_ports(self.client, "", "", c.JackPortIsPhysical | c.JackPortIsOutput);
const ports_coerced = @as([*:0][*c]const u8, ports);
for (ports_coerced) |p| {
    _ = p;
}

this compiles but I get the same “no upper bound” error.

Do for loops work on null terminated arrays? Maybe they do, I have no idea. But the documentation only mentions slices and arrays with known size.

Maybe try a while loop here.

3 Likes

Well, you can iterate over a string with for - but the string “goku” has type [4:0]const u8, because the compiler has already inferred the length from the string, so I’m probably testing the wrong thing.

EDIT:

const ports = c.jack_get_ports(self.client, "", "", c.JackPortIsPhysical | c.JackPortIsOutput);
var index: usize = 0;
while (ports[index] != 0) {
    std.debug.print("{s}\n", .{ports[index]});
    index += 1;
}

you were right, this works as expected!

2 Likes

So, should I coerce it to the actual type ? Is it [*:0][*c]const u8 ?

The correct type of the returned list of ports is [*:null]?[*:0]const u8. Quite the mouthful.

As you discovered, for loops don’t work with sentinel-terminated pointers, only slices and arrays. But if you really wanted to use a for loop for whatever reason, you could use std.mem.span to convert it to a slice:

const ports: [*:null]?[*:0]const u8 = jack_get_ports(...);
for (std.mem.span(ports)) |port| {
    std.debug.print("{s}\n", .{port.?});
}

(In terms of actual work done this is strictly worse than the while loop since it will need to iterate through the list twice, first to compute the length to create the slice and then a second time to actually loop through it.)

5 Likes

Yes, it makes sense. In a case like this, is it useful to coerce my variable to its proper type? Would it help the compiler do some clever stuff, or help the programmer not do dumb stuff? How would I coerce this type?

1 Like

Coercing C pointers to proper types is mainly for safety reasons and to ease cognitive load. A C pointer leaves out a lot of information; you won’t know if it’s a pointer to one or many elements, whether it’s optional, whether it’s a pointer to an array of a known length, whether it’s sentinel-terminated and so on, so as a compromise it acts as if it has all of those properties all at once. By explicitly coercing it to a correctly annotated type, the compiler will prevent you from doing incorrect things with it such as writing null to a non-optional pointer or indexing into a single-element pointer beyond the first element.

4 Likes

Trying to coerce the type, but obviously I’m doing it wrong:

const sources: [*:null][*:0]const u8 = c.jack_get_ports(self.client, "", "", c.JackPortIsPhysical | c.JackPortIsOutput);
src/jack.zig:84:27: error: expected type '[*:0]const u8', found '@TypeOf(null)'
        const sources: [*:null][*:0]const u8 = @ptrCast(c.jack_get_ports(self.client, "", "", c.JackPortIsPhysical | c.JackPortIsOutput));
                          ^~~~

sorry for being dense :sweat_smile:

You’re missing a ? no? Or have you tried that already.

1 Like

Indeed! And after coercing to the correct type, my while loop complains because I was testing against sentinel value 0 instead of null. I don’t think it matters a lot here, but I can totally see how that would help prevent errors in many situations!

Thanks again for your time and advices, ziggit community :heart:

4 Likes

The thing is the error message was not clear honestly… @castholm, could you help us understand what this type is trying to say?

[*:null]?[*:0]const u8

You have a C pointer to a null terminated array that could itself be null (thus the ?) and this pointer points to null terminated strings. But those pointers to string could also be null themselves. So why isn’t the type is?:

[*:null]?[*:0]?const u8

Thanks for your help!

For null terminated strings 0 is already a valid value for u8. So [*:0]const u8 makes sense. But the pointer type [*:0]const u8 is not allowed to be 0 (null) in Zig unless you add the ? to mark it as an optional pointer. That’s how you end up with the horrible looking, but very descriptive type [*:null]?([*:0]const u8).

2 Likes

The question mark goes on the left of the pointer, not the right. The list itself is not optional, but the elements (the strings) are. If we break it down:

[*:null]               // many-item 'null'-terminated pointer to
        ?              // optional of
         [*:0]const    // many-item '0'-terminated const pointer to
                    u8 // unsigned 8-bit integer

Perhaps the best way to explain complicated pointer types is to use Zig itself:

comptime {
    explainType([*:null]?[*:0]const u8);
}

fn explainType(comptime T: type) void {
    comptime {
        @compileLog("Explaining " ++ @typeName(T));
        var maybe_current: ?type = T;
        while (maybe_current) |current| {
            const info = @typeInfo(current);
            @compileLog(info);
            maybe_current = switch (info) {
                inline .Array,
                .Vector,
                .Pointer,
                .Optional,
                => |parent_info| parent_info.child,
                else => null,
            };
        }
    }
}

Pasting this into any source file and compiling will fail the compilation but print the following useful information to the terminal:

@as(*const [33:0]u8, "Explaining [*:null]?[*:0]const u8")
@as(builtin.Type, .{ .Pointer = .{.size = .Many, .is_const = false, .is_volatile = false, .alignment = 8, .address_space = .generic, .child = ?[*:0]const u8, .is_allowzero = false, .sentinel = null} })
@as(builtin.Type, .{ .Optional = .{.child = [*:0]const u8} })
@as(builtin.Type, .{ .Pointer = .{.size = .Many, .is_const = true, .is_volatile = false, .alignment = 1, .address_space = .generic, .child = u8, .is_allowzero = false, .sentinel = 0} })
@as(builtin.Type, .{ .Int = .{.signedness = .unsigned, .bits = 8} })
9 Likes