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.
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