How to correctly resize a sentinel-terminated array?

I’m starting my zig journey with the win32 api (a horrible choice) and ran into a bit of confusion regarding resizing a buffer that something else filled with null-terminated data, assuming I want to keep it null-terminated for use in later calls. The method that felt right doesn’t work, and two that do don’t feel right (cases below) !!

I wasn’t able to find any past discussion around this, and it feels like a pretty obvious harsh edge when interacting with C APIs, so I think I might just be missing something. Is there a reason realloc doesn’t preserve sentinels (or the Allocator interface provide a variant that does, like reallocSentinel or reallocZ)?

Thanks much !!

const std = @import("std");

const target = "Wow, nice and roomy in here!";

fn stringProvider(string_pointer: [*:0]u8) c_uint {
    @memcpy(string_pointer, target[0..target.len+1]);
    return target.len;
}

const alloc = std.testing.allocator;

test "method 1: alloc with sentinel for type consistency" {
    const buffer = try alloc.allocSentinel(u8, 9000, 0);
    errdefer alloc.free(buffer);

    const length = stringProvider(buffer.ptr);
    // BORKED: no sentinel on return type, despite input having one
    const minimal = try alloc.realloc(buffer, length);
    defer alloc.free(minimal);

    try std.testing.expectEqualSentinel(u8, 0, target, minimal); // complie error!
}

test "method 2: ptrCast to sentinel" {
    const buffer = try alloc.alloc(u8, 9000);
    errdefer alloc.free(buffer);

    const length = stringProvider(@ptrCast(buffer.ptr));
    // GROSS: Have to adjust lengths both in realloc call (or else free != alloc) and in returned slice
    var minimal: [:0]u8 = @ptrCast(try alloc.realloc(buffer, length + 1));
    minimal.len -= 1;
    defer alloc.free(minimal);

    try std.testing.expectEqualSentinel(u8, 0, target, minimal);
}

test "method 3: duplicate" {
    const buffer = try alloc.alloc(u8, 9000);
    defer alloc.free(buffer);

    const length = stringProvider(@ptrCast(buffer.ptr));
    // GROSS: Could be less efficient than resize/realloc? (depending on allocator implimentation)??
    const minimal = try alloc.dupeZ(u8, buffer[0..length]);
    defer alloc.free(minimal);

    try std.testing.expectEqualSentinel(u8, 0, target, minimal);
}

Welcome to the forum!
I agree that it’s weird that there is no realloc for sentinel-terminated slices.

Am I correct to assume that stringProvider is the C API function and you have no control over that?
(if you have control over it I would just use an ArrayList instead, which has more safety)

For now I can only suggest you some less gross versions:

test "method 2 advanced: sentinel slicing" {
    const buffer = try alloc.alloc(u8, 9000);
    errdefer alloc.free(buffer);

    const length = stringProvider(@ptrCast(buffer.ptr));
    // less gross: no pointer cast and no length adjusting.
    var minimal: [:0]u8 = (try alloc.realloc(buffer, length + 1))[0..length :0];
    defer alloc.free(minimal);

    try std.testing.expectEqualSentinel(u8, 0, target, minimal);
}

test "method 3 advanced: duplicate with less allocations" {
    var buffer: [9000]u8 = undefined;

    const length = stringProvider(@ptrCast(&buffer));
    // less gross: Still always requires an extra memcpy, but we removed the first alloc by using the stack.
    const minimal = try alloc.dupeZ(u8, buffer[0..length]);
    defer alloc.free(minimal);

    try std.testing.expectEqualSentinel(u8, 0, target, minimal);
}
2 Likes

Thanks for the quick response!

Am I correct to assume that stringProvider is the C API function and you have no control over that?

Yep !
For example, zigwin32’s wrapper for GetFullPathNameW has a lpBuffer parameter that is typed ?[*:0]u16. While not impossible to change, it’d be a bit of a pain.

So far,

  • (realloc)[0..length :0] works well and ultimately makes sense, but definitely requires a comment or two!
  • Using the stack also feels better (assuming the buffer size is comptime known), but still requires a @ptrCast, which is a bit of a bummer

I’ll keep working with it and see if it sticks out to me as much later :slight_smile:

You can get around the pointercast using a null-terminated array. This should work:

var buffer: [9000:0]u8 = undefined;

const length = stringProvider(&buffer);
1 Like