New wierd pattern from clay-zig

There was clay ui library resently so I checked for bindings and found Zettexe/clay-zig - Codeberg.org. Looks pretty solid but it uses super strange language construct I never saw before.

const std = @import("std");

pub fn flex() @TypeOf(&flexEnd) {
    std.log.info("flex scope started", .{});

    return flexEnd;
}

pub fn flexEnd(_: void) void {
    std.log.info("flex scope ended", .{});
}

pub fn main() !void {
    flex()({
        std.log.info("Draw elements inside of flex", .{});
    });
}
> zig run main.zig
info: flex scope started
info: Draw elements inside of flex
info: flex scope ended

Is this even a good idea or it will be axed by @mlugg at some point?

3 Likes

This seems needlessly hard to read when you could just do:

{
    flexStart();
    defer flexEnd();

    // Draw calls
}

Edit: The defer version also handles if you return in the draw scope (not that you would ever want to do that).

{
    flexStart();
    defer flexEnd();

    std.log.info("in flex scope", .{});
    return; // flexEnd runs
}
flex()({
    std.log.info("in flex scope", .{});
    return; // flexEnd does not run
});
1 Like

I don’t think this is likely to stop working, but it’s definitely… pretty weird code. I wouldn’t ever suggest writing an API that works like this.

4 Likes

flex indeed

2 Likes

Most all the patterns used to implement clay bindings for Zig are weird - see also the if + defer pattern.

Clay uses a method of abstraction that, as far as I can tell, is unrepresentable in Zig but common enough in C that I’ve personally run against it a few times. I talk about it more here, though I’m not convinced that the proposal there is the best way to solve it.

2 Likes

When the return type of the flex function is changed to @TypeOf(flexEnd), then the function doesn’t compile. The error message hints that the error is related to comptime expression evaluation. I am really missing here. Can anyone make an explanation here?

Function types like @TypeOf(endFlex) = fn (void) void are comptime only, similar to comptime_int and type. Having a comptime only return type forces flex to be evaluated at comptime, which results in an error since the call to std.log.info cannot be evaluated at compile time. Function pointers on the other hand are allowed at runtime, so @TypeOf(&endFlex) or *const @TypeOf(endFlex) will work.

4 Likes

I like that proposal, but I also liked stack capturing macros :stuck_out_tongue: I don’t see how it would pass the “no major language changes” gate. Womp womp.

The double function approach in the first post is needlessly weird, but at least that if(clay.open(.{ approach used by raugl/clay-zig has the concrete benefit of being able to skip subtrees.

Maybe that’s needed less often in clay, but skipping subtrees or earlying-out is a big part of using dearimgui and similar push/pop oriented apis.

Inline blocks or stack capturing macros or a similar construct would really help with these ImGui and alike libraries. Defer is usually enough, but sometimes some control flow abstraction would be helpful.

As an example, here’s a snippet from my code handling being able to draw a property editor inside of an existing window, optionally supporting being drawn inside an ImGui::TreeNode() pair:

const EditBlock = struct {
    pop_tree: bool,
    draw_editor: bool,

    fn begin(root_editor_drawer: *RootEditorDrawer) EditBlock {
        return if (root_editor_drawer.scratch.popLabel()) |label| blk: {
            zgui.setNextItemOpen(.{ .is_open = true, .cond = .once });
            const is_open = zgui.treeNode(label);
            break :blk .{
                .pop_tree = is_open,
                .draw_editor = is_open,
            };
        } else .{
            .pop_tree = false,
            .draw_editor = true,
        };
    }

    fn end(self: @This()) void {
        if (self.pop_tree) {
            zgui.treePop();
        }
    }
};

pub fn drawListEditor(
    data: *Data,
    ctx: EditContext,
) bool {
    // BEGIN: drawEditor function prelude
    const edit_block: EditBlock = .begin(ctx.root_editor_drawer);
    if (!edit_block.draw_editor) {
        return false;
    }
    defer edit_block.end();
    // END: drawEditor function prelude

    return drawTheRestOfTheEditor();
}

That 5 line prelude isn’t so bad in just this one function, but there are currently 4 or 5 which use that prelude, and more will be added. Inline blocks would collapse those down to 1 line:

pub fn drawListEditor(
    data: *Data,
    ctx: EditContext,
) bool {
    return editBlock(ctx.root_editor_drawer, inline {
        return drawTheRestOfTheEditor();
    });
}

With Zig today I could write a wrapper that takes a comptime function ref and the args, or maybe just anytype for both, but it gets a little ugly and pushes the decision of whether or not to use the EditBlock outside of the function doing the core work, which is sometimes helpful and sometimes not.

The thing I dislike about the wrapper approach is it makes me prone to injecting a bunch of generic layers between functions at the top coordinating work and functions at the bottom doing the work. That’s why I’m just living with copy-pasting these 5 lines into every drawEditor function. Maybe I’ll use a context struct, but that feels like it’ll just muddy up the code even more.

Anyway, preaching to the choir, I’m sure.

1 Like