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++ 的人更容易使用。
建立物件時只是使用 new,使用者應該擁有更多控制權來決定如何建立物件。例如,使用者可能希望註冊單例類別。一個解決方案是讓使用者實作靜態 create()
成員函數,傳回物件指標。這樣一來,使用者可以實作單例類別,只需透過 new 分配物件,或其他任何方式。可以修改 constructor
函數,呼叫 create()
而不是 new
來取得物件指標。這會將更多政策推給類別,但靈活性也更高。垃圾回收的「掛鉤」對某些人來說也可能有用。
範本類別的完整原始碼,約 70 行原始碼,可從 Lua 外掛程式頁面取得。