Search code examples
c++clualuajit

Exposing recursive struct as Lua Table


For historical reasons, I am constructing a struct in C++ that emulates a lua Table.

typedef union Value Value;
typedef struct LuaTable LuaTable;
typedef struct TableValue TableValue;

enum ValueType {
    INT = 0,
    BOOL = 1,
    FLOAT = 2,
    STRING = 3,
    TABLE = 4,
};

struct LuaTable {
    std::vector<TableValue> array;
    std::map<std::string, TableValue> hashmap;
};

union Value {
    int c_int;
    bool c_bool;
    double c_float;
    std::string c_str;
    LuaTable table;
};

struct TableValue {
  ValueType type;
  Value val;
};

I may simplify this such that a table is either a list or a map.

I would like to access this struct through LuaJIT FFI with some high-level syntax:

x = c_tbl.a.b.c

is a proxy for

LuaTable l_tbl {...};
...
l_tbl['a']['b']['c']

Before I start thinking about lazy evaluation (build up the prefix in some metatable), I would generally like to know if I am thinking about this the right way.

The simplest way would be to initialize c_tbl by parsing the LuaTable struct. This is the point where I am stuck. Is there a straightforward way to parse a recursive struct like this? I am willing to re-design this to alleviate parsing / traversal difficulties.


Solution

  • Something like this would work, if you're okay with using the Lua C API instead of FFI:

    #include <string>
    #include <map>
    #include <vector>
    #include <lua5.1/lua.hpp>
    
    typedef struct LuaTable LuaTable;
    typedef struct TableValue TableValue;
    
    struct LuaTable {
        std::vector<TableValue> array;
        std::map<std::string, TableValue> hashmap;
    };
    
    struct TableValue {
      enum {
        INT = 0,
        BOOL = 1,
        FLOAT = 2,
        STRING = 3,
        TABLE = 4,
      } type;
      union {
        int c_int;
        bool c_bool;
        double c_float;
        std::string c_str;
        LuaTable table;
      };
    
      TableValue(int i) : type(INT), c_int(i) {}
      TableValue(bool b) : type(BOOL), c_bool(b) {}
      TableValue(double d) : type(FLOAT), c_float(d) {}
      TableValue(const char *c) : type(STRING), c_str(c) {}
      TableValue(const std::string &s) : type(STRING), c_str(s) {}
      TableValue(std::string &&s) : type(STRING), c_str(s) {}
      TableValue(const LuaTable &t) : type(TABLE), table(t) {}
      TableValue(LuaTable &&t) : type(TABLE), table(t) {}
      TableValue(const TableValue &other) : type(other.type) {
        switch(type) {
        case INT:
          c_int = other.c_int;
          break;
        case BOOL:
          c_bool = other.c_bool;
          break;
        case FLOAT:
          c_float = other.c_float;
          break;
        case STRING:
          new (&c_str) std::string(other.c_str);
          break;
        case TABLE:
          new (&table) LuaTable(other.table);
        }
      }
      // TODO write assignment operators and move constructor
      ~TableValue() {
        switch(type) {
        case STRING:
          {
            using std::string;
            c_str.~string();
          }
          break;
        case TABLE:
          table.~LuaTable();
        }
      }
    };
    
    static void q64936405_push(lua_State *L, const TableValue *v) {
        if(!v) {
            lua_pushnil(L);
            return;
        }
        switch(v->type) {
        case TableValue::INT:
            lua_pushinteger(L, v->c_int);
            break;
        case TableValue::BOOL:
            lua_pushboolean(L, v->c_bool);
            break;
        case TableValue::FLOAT:
            lua_pushnumber(L, v->c_float);
            break;
        case TableValue::STRING:
            lua_pushlstring(L, v->c_str.c_str(), v->c_str.length());
            break;
        case TableValue::TABLE:
            // TODO consider making a weak table somewhere to hold these, so retrieving the same subtable multiple times doesn't make duplicate userdata
            // and also make them compare equal like they would in a real table
            const LuaTable **t = static_cast<const LuaTable **>(lua_newuserdata(L, sizeof *t));
            *t = &v->table;
            lua_getfield(L, LUA_REGISTRYINDEX, "q64936405");
            lua_setmetatable(L, -2);
            break;
        }
    }
    
    static int q64936405_index(lua_State *L) {
        const LuaTable *t = *static_cast<const LuaTable **>(luaL_checkudata(L, 1, "q64936405"));
        if(lua_isnumber(L, 2)) {
            lua_Integer k = lua_tointeger(L, 2);
            if(k >= 1 && k <= t->array.size()) {
                q64936405_push(L, &t->array[k - 1]);
            } else {
                lua_pushnil(L);
            }
        } else {
            size_t len;
            const char *rawk = luaL_checklstring(L, 2, &len);
            const TableValue *v;
            { // this block makes sure a Lua error won't cause UB by skipping the iterator's destruction
                auto it = t->hashmap.find(std::string{rawk, len});
                if(it == t->hashmap.end()) {
                    v = nullptr;
                } else {
                    v = &it->second;
                }
            }
            q64936405_push(L, v);
        }
        return 1;
    }
    
    static int q64936405_len(lua_State *L) {
        const LuaTable *t = *static_cast<const LuaTable **>(luaL_checkudata(L, 1, "q64936405"));
        lua_pushinteger(L, t->array.size());
        return 1;
    }
    
    // it's important that once this is passed to Lua, it's immutable until the lua_State is closed
    // otherwise Bad Things will happen
    static const LuaTable the_struct{
        {
            42,
            true,
            0.5,
            "foo",
            LuaTable{
                {
                    "nested"
                }, {
                    {"foo", "bar"}
                }
            }
        }, {
            {"one", 1},
            {"two", 2}
        }
    };
    
    extern "C"
    int luaopen_q64936405(lua_State *L) {
        const LuaTable **v = static_cast<const LuaTable **>(lua_newuserdata(L, sizeof *v));
        *v = &the_struct;
        luaL_newmetatable(L, "q64936405");
        lua_pushcfunction(L, q64936405_index);
        lua_setfield(L, -2, "__index");
        lua_pushcfunction(L, q64936405_len);
        lua_setfield(L, -2, "__len");
        lua_setmetatable(L, -2);
        // TODO write __pairs and __ipairs
        return 1;
    }
    

    I rolled your ValueType and Value into TableValue but otherwise kept the structure you wanted.

    Test it with this:

    local q64936405 = require('q64936405')
    print(q64936405[1])
    print(q64936405[2])
    print(q64936405[3])
    print(q64936405[4])
    print(q64936405[5][1])
    print(q64936405[5].foo)
    print(q64936405.one)
    print(q64936405.two)
    print(#q64936405)
    

    The result:

    42
    true
    0.5
    foo
    nested
    bar
    1
    2
    5