config和vconfig都是C++中的class,vconfig是config为便于lua处理lua-config而新加的class。
C++、lua协同处理config,可分为以下步骤。
- (C++)向lua注册表保存vconfig元表。
- (C++)lua_pcall前,把vconfig作为参数压栈,并设置vconfig元表。
- (lua)执行cfg.__shallow_literal,触发元操作__index对应的impl_vconfig_get。
- (C++)impl_vconfig_get生成lua可使用的lua-config。
- (lua)使用lua-config
4.4.2.1 (C++)向lua注册表保存vconfig元表
std::string register_vconfig_metatable(lua_State *L) { // 假设进入此函数时,栈中单元数是0。 luaL_newmetatable(L, vconfigKey); // 此时stack是1。多出的栈单元就是新建的元表,该表已经以vconfigKey的指针为key保存在注册表。 static luaL_Reg const callbacks[] { { "__gc", &impl_vconfig_collect}, { "__index", &impl_vconfig_get}, { "__len", &impl_vconfig_size}, { "__pairs", &impl_vconfig_pairs}, { "__ipairs", &impl_vconfig_ipairs}, { nullptr, nullptr } }; luaL_setfuncs(L, callbacks, 0); // 此时stack是1。但元表已经新增了callbacks指示的7条记录。 lua_pushstring(L, "wml object"); // 此时stack是2。 lua_setfield(L, -2, "__metatable"); // 此时stack是1。至此元表新增加了一条"__metatable"="wml object"记录。 // 为什么__metatable没有放在callbacks?——callbacks存的是函数,luaL_setfuncs把值放入栈中时用的是lua_pushcclosure。 // Metatables for the iterator userdata // I don't bother setting __metatable because this // userdata is only ever stored in the iterator's // upvalues, so it's never visible to the user. luaL_newmetatable(L, vconfigpairsKey); lua_pushstring(L, "__gc"); lua_pushcfunction(L, &impl_vconfig_pairs_collect); lua_rawset(L, -3); luaL_newmetatable(L, vconfigipairsKey); lua_pushstring(L, "__gc"); lua_pushcfunction(L, &impl_vconfig_ipairs_collect); lua_rawset(L, -3); }
register_vconfig_metatable后,栈会新增三个类型是table的栈单元,它们都已经保存在注册表。从顶到底依次是vconfigipairsKey、vconfigpairsKey、vconfigKey。
4.4.2.2 (C++)lua_pcall前,把vconfig作为参数压栈,并设置vconfig元表
void luaW_pushvconfig(lua_State *L, const vconfig& cfg) { // 向栈中压入一个LUA_VUSERDATA类型数据,此栈单元的body部分就是以cfg为参数构造的vconfig。 new(L) vconfig(cfg); // luaL_setmetatable依次执行两个操作。 // 1)luaL_getmetatable。查询注册表,获取key=&vconfigKey记录的值,即第一步保存的vconfig元表,并压栈。 // 2)lua_setmetatable。从栈出弹出vconfig表,并将其设置为-2索引上对象的元表,这个对象即body是“vconfig(cfg)”的userdata。 luaL_setmetatable(L, vconfigKey); // 相比刚进入函数时的栈,至此多了一个LUA_VUSERDATA、且绑定了vconfig元表的栈单元。 }
经过luaW_pushvconfig,栈中多了一个LUA_VUSERDATA、且绑定了vconfig元表的栈单元。
这里详细解释下“new(L) vconfig(cfg)”。
void* operator new(size_t sz, lua_State* L) { return lua_newuserdata(L, sz); } LUA_API void *lua_newuserdatauv (lua_State *L, size_t size, int nuvalue) { ... }
rose重载了operator new函数,自个版本的new多了一个“lua_State*”参数,要用它调用lua_newuserdata。operator new的另一个参数sz则是要创建对象的字节数,此时就是sizeof(vconfig),sz细节参考C++语法。
- operator new(12, L)。lua_newuserdata会分配一块“udatamemoffset(1) + sizeof(vsize))”的UData内存块,并以LUA_VUSERDATA类型压栈。假设UData内存块地址是0x0e668380。
- 调用vconfig的构造函数创建vconfig对象,该对象位在的地址(this,以及返回值t)是0x0e668380 + 0x28(40) = 0x0e6683a8。数值40字节的计算方法是offsetof(Udata, uv) + (sizeof(UValue) * (1))。
- vconfig被分配了空间并构造完成,返回指向一个vconfig对象的指针this。
总之,执行“new(L) vconfig(cfg)”后,向栈中压入一个LUA_VUSERDATA类型数据,返回值指向这个数据的body部分,其类型是“vconfig*”。即满足以下语句。
vconfig* v = new(L) vconfig(cfg)
和operator new配对,存在个operator delete。
void operator delete(void*, lua_State *L) { // Not sure if this is needed since it's a no-op // It's only called if a constructor throws while using the above operator new // By removing the userdata from the stack, this should ensure that Lua frees it lua_pop(L, 1); }
但实际使用时应该不会调用到operator delete。栈是lua管理的,它要删除一个栈单元时,在释放内存前会先调用元方法__gc。对于vconfig,__gc对应的是impl_vconfig_collect。
static int impl_vconfig_collect(lua_State *L) { vconfig *v = static_cast<vconfig *>(lua_touserdata(L, 1)); v->vconfig::~vconfig(); return 0; }
它得到userdata的body,调用vconfig析构函数。之后由lua释放该userdata栈单元对应的内存。
回到operator delete,它的操作就是弹出栈顶单元,弹出时会执行删除,也就重复impl_vconfig_collect的逻辑。但它要求vconfig一定在栈顶单元,由于是lualib“透明”管理栈单元,于是基本不会直接调用“operator delete”
4.4.2.3 (lua)执行cfg.__shallow_literal,触发元操作__index对应的impl_vconfig_get
function helper.shallow_literal(cfg) if type(cfg) == "userdata" then return cfg.__shallow_literal else return cfg or {} end end
cfg是lua中被调函数的第一个参数。为让lua可使用config,须先调用cfg.__shallow_literal。这条语用的作用是获取cfg中key="__shallow_literal"的value。由于cfg不存在key="__shallow_literal"的记录,于是触发元方法“__index”。
在C++,cfg已设置vconfig元表,触发元方法“__index”等于调用C函数impl_vconfig_get。
4.4.2.4 (C++)impl_vconfig_get生成lua可使用的lua-config
static int impl_vconfig_get(lua_State *L) { vconfig *v = static_cast<vconfig *>(lua_touserdata(L, 1)); ... bool shallow_literal = strcmp(m, "__shallow_literal") == 0; if (shallow_literal || strcmp(m, "__shallow_parsed") == 0) { // 生成的lua-config是一个表。 lua_newtable(L); // 表分两部分,第一部分是attribute,记录式单元。 BOOST_FOREACH (const config::attribute &a, v->get_config().attribute_range()) { // 假设attribute:rand = 1..150 if (shallow_literal) luaW_pushscalar(L, a.second); else luaW_pushscalar(L, v->expand(a.first)); lua_setfield(L, -2, a.first.c_str()); // lua-config表添加了一条记录式单元:{rand = 1..150}。 } // 表的第二部分是child,数组式单元。 vconfig::all_children_iterator i = v->ordered_begin(), i_end = v->ordered_end(); if (shallow_literal) { i.disable_insertion(); i_end.disable_insertion(); } for (int j = 1; i != i_end; ++i, ++j) { // 假设有下面这么个child。 // [set_variable] // ... // [/set_variable] // 每一个child是一个表,表有两个数组式单元。 lua_createtable(L, 2, 0); lua_pushstring(L, i.get_key().c_str()); lua_rawseti(L, -2, 1); // child内容是一个config,再次用luaW_pushvconfig压栈、设置元表。 luaW_pushvconfig(L, i.get_child()); lua_rawseti(L, -2, 2); lua_rawseti(L, -2, j); // lua-config表添加了一条数组式单元:{"set_variable", vconfig} } return 1; } ... }
为直观,让再举一个config示例,对比它的WML格式和lua格式。
[event] name = prestart side = 2 [objectives] ... [/objectives] [set_variable] [/set_variable] [/event]
经过impl_vconfig_get后,生成下面的lua表。
{name = "prestart", side = 2, {"objectives", userdata(vconfig)}, {"set_variable", userdata(vconfig)} };
4.4.2.5 (lua)使用lua-config
local function handle_event_commands(cfg) local cmds = helper.shallow_literal(cfg) for i = 1,#cmds do local v = cmds[i] // #cmds只计算数组式单元,因而此处只会得到child。 // 数组式的child是一个表,它有两个单元,cmd是child名称:set_variable,arg是child的内容。 local cmd = v[1] local arg = v[2] // arg是child的内容,lua想要是访问它,又将触发__index,进而回调impl_vconfig_get。 ... end end