Skip to content

Instantly share code, notes, and snippets.

@tbreslein
Last active November 13, 2024 15:32
Show Gist options
  • Save tbreslein/1c06f415c5bbb397cd2793ca00dd36a0 to your computer and use it in GitHub Desktop.
Save tbreslein/1c06f415c5bbb397cd2793ca00dd36a0 to your computer and use it in GitHub Desktop.
Almost generically parse a lua table into a Zig struct

I have this lua file, and I want to parse the table it returns into a Zig struct as generically as possible:

return {
  symlinks = {
    {
      source = "~/.dotfiles/config/alacritty/alacritty.toml",
      target = "~/.config/alacritty/alacritty.toml",
      force = true,
    },
    {
      source = "~/.dotfiles/config/direnv.toml",
      target = "~/.config/direnv/direnv.toml",
      force = false,
    },
  },
}

Turns out, using comptime, as long as the field names in the struct and the table match, this is almost trivial using comptime "metaprogramming". You just iterate through your struct's fields, switch on their types, and then recursively call the parseFromLua function on each field (with an exception for the allocator, which is why it's "almost" generic).

const std = @import("std");
const Lua = @import("ziglua").Lua;
const Allocator = std.mem.Allocator;

pub const Config = struct {
    symlinks: []Symlink = undefined,

    pub const Symlink = struct {
        source: []const u8 = "",
        target: []const u8 = "",
        force: bool = false,
    };
};

pub fn parseFromLua(comptime t: type, allocator: Allocator, lua: *Lua) !t {
    // NOTE: this does not work if the type has fields that do not have an equivalent in lua.
    // for example, if the type needs an Allocator, this parse does not work. In the program I
    // use this in it works perfectly, because it's a CLI tool and I just use an arena allocator.

    var x = t{};
    inline for (std.meta.fields(t)) |field| {
        _ = lua.getField(-1, field.name);
        defer lua.pop(1);
        switch (field.type) {
            []const u8 => @field(x, field.name) = try lua.toString(-1),
            bool => @field(x, field.name) = lua.toBoolean(-1),

            else => {
                // this might be an array...
                if (@typeName(field.type)[0] == '[' and @typeName(field.type)[1] == ']') {
                    const elem_type: type = std.meta.Elem(field.type);
                    _ = lua.len(-1);
                    const n: usize = @intCast(try lua.toInteger(-1));
                    lua.pop(1);

                    const arr = try allocator.alloc(elem_type, n);
                    for (0..n) |i| {
                        _ = lua.getIndex(-1, @intCast(i + 1));
                        defer lua.pop(1);
                        arr[i] = try parseFromLua(elem_type, allocator, lua);
                    }
                    @field(x, field.name) = arr;
                } else {
                    @panic("foo");
                }
            },
        }
    }
    return x;
}

And now I can load the zig table using ziglua, and parse it with this:

try lua.doFile("/path/to/my/lua/file");
const conf = try config.parseFromLua(config.Config, allocator, lua);
defer conf.deinit();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment