Slice with length and capacity?

I’m writing some editor code with Zig cimgui bindings and am coming across many situations where passing values between Zig and C imgui API is kind of awkward.

In the below code, InputText is declared as

fn inputText(label: ?[*:0]const u8, buf: ?[*]u8, buf_size: usize) bool

It makes sense to be passing null-terminated strings directly to cimgui rather than having some sort of conversion in the middle. However, code using this API ends up being kind of ugly and error prone, for example:

var buf: [256]u8 = undefined;
var curAsset = editor.selectedAsset();
_ = std.fmt.bufPrintZ(&buf, "{s}", .{curAsset.name}) catch unreachable;
if (imgui.InputText("Asset name", &buf, 256)) {
    curAsset.setName(std.mem.sliceTo(&buf, 0), editor.allocator);
}

I’m thinking one reasonable pattern might be to declare some type such as CapacitySlice which would allocate a fixed size buffer to hold a certain capacity so that the below code would be something like this instead:

var str = CapacitySlice(u8, 256).init(); // hold 256 bytes of space, string length = 0 after init
var curAsset = editor.selectedAsset();

// Note: can't pass "curAsset.name" directly to imgui because curAsset is a normal
// string slice "[]u8" and doesn't have enough capacity for storing the new edited
// name which InputText writes to its destination buffer.
_ = std.fmt.bufPrintZ(&str.ptr(), "{s}", .{curAsset.name}) catch unreachable;
if (imgui.InputText_Slice("Asset name", str)) {
    curAsset.setName(std.mem.sliceTo(&buf, 0), editor.allocator);
}

Little like std.ArrayList except with a fixed size allocation.

Does this approach make sense or might there be a nicer/cleaner/more idiomatic way to do this? Or maybe there are already some useful types for this in the standard library?

Ended up writing a little utility:

const imguix = struct {
    pub const CapPtr = struct {
        ptr: [*:0]u8,
        capacity: usize,
    };

    pub fn BufString(comptime C: usize) type {
        return struct {
            buf: [C:0]u8 = undefined,
            capacity: usize = C,
            len: usize = 0,

            pub fn init() @This() {
                return .{
                    .buf = undefined,
                    .capacity = C,
                    .len = 0,
                };
            }

            pub fn printZ(self: *@This(), comptime fmt: []const u8, args: anytype) void {
                _ = std.fmt.bufPrintZ(self.buf[0 .. self.capacity - 1], fmt, args) catch unreachable;
            }

            pub fn set(self: *@This(), s: []const u8) void {
                self.printZ("{s}", .{s});
            }

            pub fn sliceTo(self: *@This()) [:0]u8 {
                return std.mem.sliceTo(&self.buf, 0);
            }

            pub fn ptr(self: *@This()) CapPtr {
                return .{
                    .ptr = self.sliceTo().ptr,
                    .capacity = self.capacity,
                };
            }
        };
    }

    pub fn bufString(comptime size: usize) BufString(size) {
        return BufString(size).init();
    }

    pub fn inputText(label: ?[*:0]const u8, buf: CapPtr) ?[:0]const u8 {
        if (imgui.InputText(label, buf.ptr, buf.capacity)) {
            return std.mem.sliceTo(buf.ptr, 0);
        }
        return null;
    }
};

Usage is a little nicer with this:

var cbuf = imguix.bufString(256);
cbuf.set(curAsset.name);
if (imguix.inputText("Asset name", cbuf.ptr())) |new_text| {
    curAsset.setName(new_text, editor.allocator);
}

Could also have cbuf.set() return a CapPtr so that one could set and convert the pointer simultaneously:

if (imguix.inputText("Asset name", cbuf.set(curAsset.name))) |new_text| {
    curAsset.setName(new_text, editor.allocator);
}

I think creating this data structure is almost a rite of passage for people who are learning Zig :^)

Here’s the one I made for my Redis client:

I don’t have any other link on hand, but I’ve seen it made by at least three other people.

1 Like

:slight_smile: I realized that I anyway need to take a little step back from advancing the ImGui API bindings API further. I thought using cimgui bindings directly in Zig should be fine but finding it to be a “death by a thousand papercuts” type of experience, so I started building a separate module “imguix” on the side, where I add things one API function at a time.