首页 > 解决方案 > 将递归结构公开为 Lua 表

问题描述

由于历史原因,我正在用 C++ 构建一个模拟 lua 表的结构。

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;
};

我可以将其简化为表格是alista map

我想通过 LuaJIT FFI 使用一些高级语法访问这个结构:

x = c_tbl.a.b.c

是代表

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

在我开始考虑惰性评估(在某些元表中建立前缀)之前,我通常想知道我是否以正确的方式考虑这个问题。

最简单的方法是c_tbl通过解析LuaTable结构进行初始化。这就是我卡住的地方。有没有一种直接的方法来解析这样的递归结构?我愿意重新设计它以减轻解析/遍历的困难。

标签: c++clualuajit

解决方案


如果您可以使用 Lua C API 而不是 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;
}

我把你的卷ValueType进去ValueTableValue,但在其他方面保持了你想要的结构。

用这个测试它:

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)

结果:

42
true
0.5
foo
nested
bar
1
2
5

推荐阅读