int(T::*)(lua_State*)
的类成员函数才能注册。但正如我将展示的那样,可以克服此限制。最终结果是用于注册类的简洁界面,以及 Lua 中类的熟悉 Lua 表语义。此处解释的解决方案基于我编写的模板类,名为 Luna。
int()(lua_State*)
的 C 函数,即,将 lua_State
指针作为参数并返回整数的函数。实际上,这是 Lua 在注册中支持的唯一 C 数据类型。要注册任何其他类型,你必须使用 Lua 提供的扩展机制、标记方法、闭包等。在构建允许我们将 C++ 类注册到 Lua 的解决方案时,我们必须利用这些扩展机制。
通过使用类的名称注册表构造函数来完成类注册。表构造函数是模板类的静态方法,它返回一个表对象。
注意:静态类成员函数与 C 函数兼容,假设它们的签名相同,因此我们可以将它们注册到 Lua 中。以下代码片段是模板类的成员函数,“T”是要绑定的类。
static void Register(lua_State* L) { lua_pushcfunction(L, &Luna<T>::constructor); lua_setglobal(L, T::className); if (otag == 0) { otag = lua_newtag(L); lua_pushcfunction(L, &Luna<T>::gc_obj); lua_settagmethod(L, otag, "gc"); /* tm to release objects */ } }对象实例化通过将用户传递给表构造函数的任何参数传递给 C++ 对象的构造函数来完成,创建一个表示对象的表,将类的任何成员函数注册到该表,最后将表返回给 Lua。对象指针存储在表中索引为 0 的用户数据中。成员函数映射中的索引存储为每个函数的闭包值。稍后会详细介绍成员函数映射。
static int constructor(lua_State* L) { T* obj= new T(L); /* new T */ /* user is expected to remove any values from stack */ lua_newtable(L); /* new table object */ lua_pushnumber(L, 0); /* userdata obj at index 0 */ lua_pushusertag(L, obj, otag); /* have gc call tm */ lua_settable(L, -3); /* register the member functions */ for (int i=0; T::Register[i].name; i++) { lua_pushstring(L, T::Register[i].name); lua_pushnumber(L, i); lua_pushcclosure(L, &Luna<T>::thunk, 1); lua_settable(L, -3); } return 1; /* return the table object */ }与 C 函数不同,C++ 成员函数需要类的对象才能调用函数。成员函数调用由一个函数完成,该函数通过获取对象指针和成员函数指针并进行实际调用来“thunk”调用。成员函数指针通过闭包值从成员函数映射中索引,对象指针从索引为 0 的表中索引。请注意,Lua 中的所有类函数都使用此函数注册。
static int thunk(lua_State* L) { /* stack = closure(-1), [args...], 'self' table(1) */ int i = static_cast<int>(lua_tonumber(L,-1)); lua_pushnumber(L, 0); /* userdata object at index 0 */ lua_gettable(L, 1); T* obj = static_cast<T*>(lua_touserdata(L,-1)); lua_pop(L, 2); /* pop closure value and obj */ return (obj->*(T::Register[i].mfunc))(L); }垃圾回收通过为表中的用户数据设置垃圾回收标记方法来完成。当垃圾回收器运行时,将调用“gc”标记方法,该方法只是删除对象。“gc”标记方法在类注册期间使用新标记注册。在上面的对象实例化中,用户数据被标记为标记值。
static int gc_obj(lua_State* L) { T* obj = static_cast<T*>(lua_touserdata(L, -1)); delete obj; return 0; }考虑到这一点,您可能已经注意到一个类必须符合一些要求
lua_State*
的公共构造函数
int(T::*)(lua_State*)
className
的 public static const char[]
成员
Register
的 public static const Luna<T>::RegType[]
成员
Luna<T>::RegType
是一个函数映射。name
是成员函数 mfunc
将注册为的函数的名称。
struct RegType { const char* name; const int(T::*mfunc)(lua_State*); };以下是将 C++ 类注册到 Lua 的示例。调用
Luna<T>::Register()
将注册该类;这是模板类的唯一公共接口。要使用 Lua 中的类,可以通过调用其表构造函数来创建它的实例。
class Account { double m_balance; public: Account(lua_State* L) { /* constructor table at top of stack */ lua_pushstring(L, "balance"); lua_gettable(L, -2); m_balance = lua_tonumber(L, -1); lua_pop(L, 2); /* pop constructor table and balance */ } int deposit(lua_State* L) { m_balance += lua_tonumber(L, -1); lua_pop(L, 1); return 0; } int withdraw(lua_State* L) { m_balance -= lua_tonumber(L, -1); lua_pop(L, 1); return 0; } int balance(lua_State* L) { lua_pushnumber(L, m_balance); return 1; } static const char[] className; static const Luna<Account>::RegType Register }; const char[] Account::className = "Account"; const Luna<Account>::RegType Account::Register[] = { { "deposit", &Account::deposit }, { "withdraw", &Account::withdraw }, { "balance", &Account::balance }, { 0 } }; [...] /* Register the class Account with state L */ Luna<Account>::Register(L); -- In Lua -- create an Account object local account = Account{ balance = 100 } account:deposit(50) account:withdraw(25) local b = account:balance()Account 实例的表如下所示
0 = userdata(6): 0x804df80 balance = function: 0x804ec10 withdraw = function: 0x804ebf0 deposit = function: 0x804f9c8
Thunk 机制是该类的核心,因为它“Thunk”调用。它通过从函数调用关联的表中获取对象指针,并为成员函数指针索引成员函数映射来实现这一点。(Lua 表函数调用 table:function()
是 table.function(table)
的语法糖。当进行调用时,Lua 首先将表压入堆栈,然后压入任何参数)。成员函数索引是一个闭包值,最后压入堆栈(在任何参数之后)。最初,我将对象指针作为闭包,这意味着对于每个实例化的类,每个函数都有 2 个闭包值,一个指向对象(void*)的指针和一个成员函数索引(int);这似乎相当昂贵,但可以快速访问对象指针。此外,表中需要一个用于垃圾回收目的的用户数据对象。最后,我选择为对象指针索引表并节省资源,从而增加了函数调用开销;对象指针的表查找。
综合考虑所有事实,该实现仅使用 Lua 的一些可用扩展机制,用于保存成员函数索引的闭包,“gc”标记方法用于垃圾回收,以及用于表构造函数和成员函数调用的函数注册。
为什么只允许注册具有签名 int(T::*)(lua_State*)
的成员函数?这允许您的成员函数直接与 Lua 交互;检索参数并将值返回 Lua,调用任何 Lua API 函数等。此外,它提供了 C 函数注册到 Lua 时具有的相同接口,从而使希望使用 C++ 的人更容易上手。
创建对象时只需新建,用户应该对如何创建对象有更多控制权。例如,用户可能希望注册一个单例类。一种解决方案是让用户实现一个静态的 create()
成员函数,该函数返回指向对象的指针。这样,用户就可以实现一个单例类,只需通过 new 分配对象或其他任何方式。可以修改 constructor
函数以调用 create()
而不是 new
来获取对象指针。这将更多策略推送到类中,但灵活性更高。垃圾回收的“挂钩”也可能对某些人有用。
模板类的完整源代码(约 70 行源代码)可从 Lua 加载项页面获得。