Casting to a C array of strings

Hi everyone,

I’m currently building a zig project and I depend on a C library. One of the functions of the library takes as a parameter an array of strings, i.e., an array of pointers terminated with a null pointer (the array), each element of the array being a char pointer (a null-terminated string).

As an example, the function can take an argument like this:

char *strings[] = {"foo", "bar", NULL};
string_hungy_func((const char * const *) strings);

Now, I’ve struggling a lot to understand how I can cast a zig type to be used with this function. As a far as I understand, I need a null-terminated many-item pointer to null-terminated pointers to u8, so I’ve tried doing this:

const string_list: [*:null]const ?[*:0]const u8 = @alignCast(@ptrCast(&.{ &.{"Foo"}, &.{"Bar"}, null }));
const list = string_hungy_func(@ptrCast(string_list)));

Which pleases the compiler, but results in a segmation fault.

Can you explain to me how I could properly cast the arguments to this function?

Thanks!

Have you linked libc in your application? I ran into an issue the other day where I was intracting with a c library but forgot to link libc and kept getting a segfault at address 0x0. I think this is a segfault calling the library’s functions. That may be what is happening here.

Zig’s null-terminated array initiating does NOT need the last null. Zig’s null-terminated array’s len field (my_array.len) will return the same len as how many “actual” items you have, and will not include the last null. So you don’t do anything with last null in your codes… The compiler will do the job for you. See here: https://ziglang.org/documentation/master/#Sentinel-Terminated-Arrays

This should work:

const string_list = [_:null]?[*:0]const u8 {"foo", "bar"};
const list = string_hungy_func(&string_list);

I had a similar discussion another day here: https://ziggit.dev/t/how-to-handle-c-const-c-const-u8-in-a-c-library/8768

1 Like

Thanks! That’s kind of what I was looking for. Just out of curiosity, how could one deal
with strings allocated dynamically at runtime, i.e., how does one cast a [][]const u8
to a [:null]?[*:0]const u8?

I’ve tried the following:

    var str_list: [][]const u8 = undefined;

    const n_strings = 2;
    const string_len = 4;

    var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
    const allocator = arena.allocator();
    str_list = try allocator.alloc([]u8, n_strings);
    defer {
        allocator.free(str_list);
    }

    for (0..n_strings) |i| {
        const str = try allocator.alloc(u8, string_len);

        for (0..string_len) |j| {
            str[j] = 'a';
        }
        str_list[i] = str;
    }
    defer {
        for (0..n_strings) |i| {
            allocator.free(str_list[i]);
        }
    }
    // Here's how I'm trying to do the casting:
    const c_str_list: [:null]?[*:0]const u8 = @ptrCast(str_list); 
    _ = c_str_list;

But the compiler throws the error:

error: TODO: implement @ptrCast between slices changing the length

Which makes me think that I’m doing something wrong, but I’m not sure how to handle it.

Make your first allocation

to this:

str_list = try allocator.alloc([*:0]const u8, n_strings);

This should do the trick.

I actually played around about this on my own codes after I saw your post.

See here: https://gitlab.com/IceEternityProject/IceWindowing/-/blob/ef0e93a9150468cd06c466c861302e8003078897/Wayland/src/root.zig#L287

and here: https://gitlab.com/IceEternityProject/IceWindowing/-/blob/ef0e93a9150468cd06c466c861302e8003078897/Wayland/src/root.zig#L342

By the way, I use the master version of zig, well anyways 0.14.0 was just released today.

makes a [][*:0]const u8
to get [:null]?[*:0]const u8` do:

const str_list = try allocator.allocSentinal(?[*:0]const u8, n_strings, null);

there is no reason to create a undefined var first

note that str_list can be a slice, but the strings inside it have to be many item pointers.

you also need to change allocating the string in the loop to

const str = try allocator.allocSentinal(u8, string_len, 0);

note that I am creating the types instead of casting them

You shouldn’t cast a non terminated slice/mip to a terminated slice/mip. mip being a many item pointer

You can’t cast [][]const u8 to a [:null]?[*:0]const u8 as the item types have different sizes.

A slice is essentially struct { ptr: [*]T, len:usize}
note that optional pointers have the same size as non-optional pointers due to an optimisation, but that’s irrelevant atm

Thanks for your answers, they’ve been really useful for me to understand how the type system, the different types of pointers and the sentinels all go together.

However, what I meant was, if I have a C function that takes an array of strings (const char *const *) and a zig function that returns a dynamically-allocated list of strings ([][]const u8), how can I pass the former to the latter?

I have built this example in which I allocate a new copy of the string in a way that can be understood by the C function, but I wonder if there’s a better way to do it:

This is my C code:

#include <stdio.h>
void printStringList(const char *const *list) {
    for (int i = 0; list[i]; i++) {
        for (int j = 0; list[i][j]; j++) {
            printf("%c", list[i][j]);
        }
        printf("\n");
    }
}

And I have this zig function that returns a slice of strings:

fn generateStrings(allocator: std.mem.Allocator) ![][]const u8 {
    const n_strings = 2;
    const string_len = 4;

    var str_list: [][]const u8 = try allocator.alloc([]const u8, n_strings);

    for (0..n_strings) |i| {
        const str: []u8 = try allocator.alloc(u8, string_len);

        for (0..string_len) |j| {
            str[j] = 'a';
        }
        str_list[i] = str;
    }

    return str_list;
}

What I want to do if feed the output of generateStrings() into printStringList(), but the problem is that printStringList() expects a null terminator in each string and in the array containing the strings. Because the array and the strings are dynamically allocated, this requires allocating new memory.

Also (I may be wrong on this, please correct me if that’s the case), since I’m working with slices, I can’t guarantee that I can increase the size of the array and/or the strings, since the new memory needs to be adjacent to the one previously allocated.

The solution I came up with was creating a new null-terminated array, and adding a null-terminated copy of each string:

var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
defer arena.deinit();
const allocator = arena.allocator();

const dynamic_string_list = try generateStrings(allocator);
defer {
    // Do I need to free each string individually?
    for (dynamic_string_list) |str| {
        allocator.free(str);
    }
    allocator.free(dynamic_string_list);
}

const c_compatible_string_list = try allocator.allocSentinel(
    ?[*:0]u8,
    dynamic_string_list.len,
    null,
);
defer allocator.free(c_compatible_string_list);
for (0..c_compatible_string_list.len) |i| {
    c_compatible_string_list[i] = try allocator.dupeZ(u8, dynamic_string_list[i]);
}
defer {
    for (c_compatible_string_list, dynamic_string_list) |c_str, d_str| {
        allocator.free(c_str.?[0 .. d_str.len + 1]);
    }
    allocator.free(c_compatible_string_list);
}
// I can call the C function like this:
std.debug.print("Dynamic strings:\n", .{});
c.printStringList(@ptrCast(c_compatible_string_list));

Is there a better way to do this?

Nope.
I personally would rewrite the function to return the type you need to avoid the double allocation, but it shouldn’t matter much

you don’t need the @ptrCast, it should coerce

2 Likes

I see, thanks. Also, without using @ptrCast() I get an error:

error: expected type '[*c]const [*c]const u8', found '[:null]?[*:0]u8'
    c.printStringList(c_compatible_string_list);
                      ^~~~~~~~~~~~~~~~~~~~~~~~
c.printStringList(c_compatible_string_list.ptr);
2 Likes

Oops, I was thinking [*] not [*c] for coercion