Passing pointer to slice, vs passing pointer to structure wrapping a slice

Hello everybody,

I am trying to write a little GUI, using raylib and clay, both using the zig wrappers

Now I am in this situation:

The following is my main zig file. It sets up raylib, as well as clay, and then renders a UI, that lists a bunch of songs in a vertical, scrollable container, each being named Song {i}. I do this by first allocating a string, with the appropriate index number and then pushing that to clay. (I know this leaks memory as hell, but I want to understand why this behaves the way it does, before fixing it). The UI renders correctly, with each list entry having its appropriate index.
I also set up a click hander with clay.onHover, to which I pass the clicked element string as a pointer to the string slice.
Now the problem: Inside the click handler it always receives the pointer to the last slice. So Song 49.

const std = @import("std");
const builtin = @import("builtin");
const raylib = @import("raylib");
const clay = @import("zclay");

const rl_renderer = @import("raylib_render_clay.zig");

const WINDOW_WIDTH = 1000;
const WINDOW_HEIGHT = 1000;
const TARGET_FPS = 120;

fn loadFont(file_data: ?[]const u8, font_id: u16, font_size: i32) !void {
    rl_renderer.raylib_fonts[font_id] = try raylib.loadFontFromMemory(".ttf", file_data, font_size * 2, null);
    raylib.setTextureFilter(rl_renderer.raylib_fonts[font_id].?.texture, .bilinear);
}

pub fn main() !void {
    const allocator = std.heap.page_allocator;

    // init clay
    const min_memory_size: u32 = clay.minMemorySize();
    const memory = try allocator.alloc(u8, min_memory_size);
    defer allocator.free(memory);
    const arena: clay.Arena = clay.createArenaWithCapacityAndMemory(memory);
    _ = clay.initialize(arena, .{ .h = WINDOW_HEIGHT, .w = WINDOW_WIDTH }, .{});
    clay.setMeasureTextFunction(void, {}, rl_renderer.measureText);

    // init raylib
    raylib.setConfigFlags(.{
        .msaa_4x_hint = true,
        .window_resizable = true,
    });
    raylib.initWindow(WINDOW_WIDTH, WINDOW_HEIGHT, "Raylib zig Example");
    raylib.setTargetFPS(TARGET_FPS);

    // load assets
    try loadFont(@embedFile("./resources/Roboto-Regular.ttf"), 0, 24);

    var debug_allocator: std.heap.DebugAllocator(.{}) = .init;
    const alloc, const is_debug = gpa: {
        break :gpa switch (builtin.mode) {
            .Debug, .ReleaseSafe => .{ debug_allocator.allocator(), true },
            .ReleaseFast, .ReleaseSmall => .{ std.heap.smp_allocator, false },
        };
    };
    defer if (is_debug) {
        _ = debug_allocator.deinit();
    };

    var debug_mode_enabled = false;
    while (!raylib.windowShouldClose()) {
        if (raylib.isKeyPressed(.d)) {
            debug_mode_enabled = !debug_mode_enabled;
            clay.setDebugModeEnabled(debug_mode_enabled);
        }

        clay.setLayoutDimensions(.{
            .w = @floatFromInt(raylib.getScreenWidth()),
            .h = @floatFromInt(raylib.getScreenHeight()),
        });

        const mouse_pos = raylib.getMousePosition();
        clay.setPointerState(.{
            .x = mouse_pos.x,
            .y = mouse_pos.y,
        }, raylib.isMouseButtonDown(.left));

        const scroll_delta = raylib.getMouseWheelMoveV().multiply(.{ .x = 6, .y = 6 });
        clay.updateScrollContainers(
            false,
            .{ .x = scroll_delta.x, .y = scroll_delta.y },
            raylib.getFrameTime(),
        );

        clay.beginLayout();
        clay.UI()(.{
            .id = .ID("MainContainer"),
            .layout = .{
                .sizing = .grow,
                .child_alignment = .{
                    .x = .center,
                    .y = .top,
                },
            },
            .background_color = .{255, 255, 255, 255},
        })({
            clay.UI()(.{
                .id = .ID("SongContainer"),
                .background_color = .{0, 0, 0, 255},
                .layout = .{
                    .sizing = .fit,
                    .direction = .top_to_bottom,
                    .child_gap = 16,
                },
                .clip = .{
                    .vertical = true,
                    .child_offset = clay.getScrollOffset(),
                },
            })({
                // PART IN QUESTION
                for (0..50) |i| {
                    const name = std.fmt.allocPrint(alloc, "Song {d}", .{i}) catch continue;
                    clay.UI()(.{
                        .id = .ID(name),
                        .layout = .{
                            .sizing = .fit,
                        },
                    })({
                        clay.onHover(*const []u8, &name, button_interaction);
                        clay.text(name, .{ .font_size = 24, .color = .{255, 255, 255, 255} });
                    });
                }
                // PART IN QUESTION
            });
        });
        const render_commands = clay.endLayout();

        raylib.beginDrawing();
        try rl_renderer.clayRaylibRender(render_commands, allocator);
        raylib.endDrawing();
    }
}
const Test = struct {
    name: []u8,
};

fn button_interaction(elem: clay.ElementId, pointer: clay.PointerData, user_data: *const []u8) void {
    _ = elem;
    if (pointer.state == .pressed_this_frame) {
        std.debug.print("clicked element: {s}\n", .{user_data.*});
    }
}

You can see the output below. It always ends up being the same name.

~/code/lauscher [nix: nix-lauscher-dev-env]
130 » zig build -freference-trace=10 run
...
clicked element: Song 49
clicked element: Song 49
clicked element: Song 49

After experimenting a bit, I came up with this construct. I create a wrapper struct for the string slice on the heap, passing a pointer to that, which is then dereferenced and the name is printed.

                for (0..50) |i| {
                    const name = std.fmt.allocPrint(alloc, "Song {d}", .{i}) catch continue;
                    const t: *Test = alloc.create(Test) catch unreachable;
                    t.name = name;
                    clay.UI()(.{
                        .id = .ID(name),
                        .layout = .{
                            .sizing = .fit,
                        },
                    })({
                        clay.onHover(*Test, t, button_interaction);
                        clay.text(name, .{ .font_size = 24, .color = .{255, 255, 255, 255} });
                    });
                }


fn button_interaction(elem: clay.ElementId, pointer: clay.PointerData, user_data: *Test) void {
    _ = elem;
    if (pointer.state == .pressed_this_frame) {
        std.debug.print("clicked element: {s}\n", .{user_data.name});
    }
}

As you can see, this works.

~/code/lauscher [nix: nix-lauscher-dev-env]
130 » zig build -freference-trace=10 run                                                                                                    zeno@apokory | 8:47:57
...
clicked element: Song 12
clicked element: Song 11
clicked element: Song 10

Now, why does that work? In my understanding, both are pointers to heap structures, or at least pointers to memory that lives longer, then the frame, as it is never freed.
And how can I built this, so I dont have to wrap the strings in a separate structure?

If need be, I can put everything in a git repo, together with a nix flake, if that makes things easier to debug.

Thank you

Passing in &name passes in the address of the local variable “name”. Rhe address of the allocated memory is stored in that slice and gets overwritten in each loop.

1 Like

Ah ok. I so the allocPrint call here allocates memory on the heap to store the string, but the structure of the slice as a whole is stored on the stack, that tripped me up, as I thought both would be on the heap. So, as passing a slice to a function works, there it copies the slice structure, instead of passing a pointer to it?

Thank you

you could do that also, but it would be more steps, the fact that you took the address of a local variable should tip you off in the future.

I am back home now. You say that would be more effort, what would be the correct way to pass the slice to that function? The problem is, I am only allowed to pass in a pointer.

A slice is a pointer+len.

Exactly, that is the problem. Now vulpesx mentioned there is an easier way than copying both of those to a heap structure and passing a pointer to that.

It’s possible the simplest solution would be to allocate using a sentinel/Z method and pass a [:0]const u8, as that doesn’t store length and is just a pointer (C style).

3 Likes

You can have a null-terminated slice, and pass slice.ptr to raylib.

3 Likes