How to (ab)use slices with C functions?

Hi. I’ve been writing Vulkan stuff (just the basics) with Zig. Many SDL and Vk functions have this pattern of first getting the required list size by calling once with a null ptr, then calling again with a valid ptr to sufficient memory for holding a list.

With Zig I managed to get this to work using Allocator.alloc like so:

var count = 0;
c.vkEnumerateThings(&count, null); // ignore errors

var things = try allocator.alloc(c.VkThing, count);
c.vkEnumerateThings(&count, @ptrCast([*c]c.VkThing, &things[0]));
std.debug.assert(count == things.len);

// Now things is a valid slice of c.VkThing values.

One issue: I feel like @ptrCast and especially this C++ -ism of taking the address of the first element (&things[0]) is a hack. If I slip up and forget the [0] for example, the slice’s len may get overwritten and terrible things may happen. Is there an automatic way to coerce a slice to a many-item pointer? Just writing things or &things there without a cast did not work (at least in some cases).

No need for @ptrCast; use things.ptr.

4 Likes

Thank you very much. It’s there in the docs, I just should’ve had some sleep to be able to see it.

1 Like

I can’t believe it was possible. I even abstracted this pattern in Zig like so:

fn EnumerateFunctionOutputType(comptime func: anytype) type {
    const FuncType = @TypeOf(func);
    const info = @typeInfo(FuncType);
    if (info != .Fn) {
        @compileError("Expected function, found " ++ @typeName(FuncType));
    }
    const params = info.Fn.params;
    if (params.len < 2) {
        @compileError("Expected function with at least two parameters");
    }
    const last_param = params[params.len - 1];
    const param_info = @typeInfo(last_param.type.?);
    if (param_info != .Pointer) {
        @compileError("Expected function which has a pointer type as the last parameter");
    }
    return param_info.Pointer.child;
}

fn enumerateToSlice(
    self: *VkBuilder,
    enumerateFunc: anytype,
    prefix_args: anytype,
    success: anytype,
) Error![]EnumerateFunctionOutputType(enumerateFunc) {
    var count: u32 = 0;
    var ret = @call(.auto, enumerateFunc, prefix_args ++ .{ &count, null });
    if (ret != success) {
        return Error.VkInitError;
    }
    var buf = try self.allocator.alloc(EnumerateFunctionOutputType(enumerateFunc), count);
    errdefer self.allocator.free(buf);
    ret = @call(.auto, enumerateFunc, prefix_args ++ .{ &count, buf.ptr });
    if (ret != success) {
        return Error.VkInitError;
    }
    std.debug.assert(count == buf.len);
    return buf;
}

Took me a day, but it’s so rewarding to have this work!

Now I can call it like this (and there are quite a few places to do so):

        var devices = self.enumerateToSlice(
            c.vkEnumeratePhysicalDevices,
            .{instance},
            c.VK_SUCCESS,
        );
2 Likes