Lua輕量級繫結C++物件
每一種策略都有它的優點和缺點,遊戲開發者必須在得到在指令碼環境中所需要的功能需求之後確定最好的策略。一些開發者可能只是把C/C++物件對映成簡單的數值,但是其他人可能需要實現執行期型別檢查機制,甚至是在Lua中擴充套件宿主的應用。
遊戲中的使用指令碼語言已經成為了一個標準應用。指令碼語言能夠在遊戲開發中扮演一個重要的角色,並且讓資料結構化,計劃事件,測試和除錯這些工作更加容易。指令碼語言也能夠允許像美術,策劃這些非程式專家通過一個高層的抽象指令碼來為遊戲編寫程式碼。這個抽象層的一部分也能夠允許提供給玩家來定製整個遊戲。
從程式設計師的角度上來看,把一個指令碼語言嵌入到遊戲中最主要的問題是如何為指令碼語言提供對宿主物件的訪問(通常是C/C++物件)。在選擇一個指令碼語言的時候有兩個關鍵的特性:嵌入相關問題和繫結相關問題。而這些是Lua語言的一些設計的初衷。可是,Lua語言並沒有提供任何自動建立繫結的工具,因為這是出於另外一個設計初衷:Lua只是提供機制,而不是策略。
因而,就有許多種策略可以用來在Lua
繫結函式
為了說明不同策略的實現,讓我們考慮把一個簡單的C++類繫結到Lua中。實現的目標是在Lua中實現對類的訪問,因此允許指令碼通過匯出的函式來使用宿主所提供的服務。這裡主要的想法是使用一個簡單的類來引導我們的討論。下面討論的是一個虛構遊戲中的英雄類,有幾個將會被對映到Lua中的公用方法。
class Hero{
public:
Hero( const char* name );
~Hero();
const char* GetName();
void SetEnergy( double energy );
double GetEnergy();
};
要把類方法繫結到Lua中,我們必須使用Lua的API來編寫繫結功能。每一個繫結函式都負責接收Lua的值作為輸入引數,同時把它們轉化成相應的C/C++數值,並且呼叫實際的函式或者方法,同時把它們的返回值給回到Lua中。從標準釋出版本的Lua中,Lua API和輔助庫提供了不少方便的函式來實現Lua到C/C++值的轉換,同樣,也為C/C++到Lua值的轉換提供了函式。例如,luaL_checknumber提供了把輸入引數轉換到相對應的浮點值的功能。
如果引數不能對應到Lua中的數值型別,那麼函式將丟擲一個異常。相反的,lua_pushnumber把給定的浮點值新增到Lua引數棧的頂端。還有一系列相類似的函式來對映其他的基本的Lua型別和C/C++資料型別。我們目前最主要的目標提出不同的策略來擴充套件標準Lua庫和它為轉換C/C++型別物件所提供的功能。為了使用C++的習慣,讓我們建立一個叫做Binder的類來封裝在Lua和宿主物件中互相轉化值的功能。這個類也提供了一個把將要匯出到Lua中的模組初始化的方法。
class Binder
{
public:
// 建構函式
Binder( lua_state *L );
// 模組(庫) 初始化
int init( const char* tname, const luaL_reg* first );
// 對映基本的型別
void pushnumber( double v );
double checknumber( int index );
void pushstring( const char s );
const char* checkstring( int index );
….
// 對映使用者定義型別
void pushusertype( void* udata, const char* tname );
void* checkusertype( int index, const char* tname );
};
類的建構函式接收Lua_state來對映物件。初始化函式接收了將被限制的型別名字,也被表示為庫的名稱(一個全域性變數名來表示在Lua中的類表),並且直接呼叫了標準的Lua庫。例如,對映一個數值到Lua中,或者從Lua映射出來的方法可能是這樣的:
void Binder::pushnumber( double v )
{
lua_pushnumber( L,v );
}
double Binder::checknumber( int index )
{
return luaL_checknumber( L,index );
}
真正的挑戰來自把使用者自定義型別互相轉換的函式:pushusertype和checkusertype。這些方法必須保證對映物件的繫結策略和目前使用中的一致。每一種策略都需要不同的庫的裝載方法,因而要給出初始化方法init的不同實現。
一旦我們有了一個binder的實現,那麼繫結函式的程式碼是非常容易寫的。例如,繫結函式相關的類的建構函式和解構函式是如下程式碼:
static int bnd_Create( lua_state* L ){
LuaBinder binder(L);
Hero* h = new Hero(binder.checkstring(L,1));
binder.pushusertype(h,”Hero”);
return i;
}
static int bnd_Destroy( lua_state* L ){
LuaBinder binder(L);
Hero * hero = (Hero*)binder.checkusertype( 1, “Hero” );
delete hero;
return 0;
}
同樣的,和GetEnergy和SetEnergy方法的繫結函式能夠像如下編碼:
static int bnd_GetEnergy( lua_state* L ){
LuaBinder binder(L);
Hero* hero = (Hero*)binder.checkusertype(1,”Hero”);
binder.pushnumber(hero->GetEnergy());
return 1;
}
static int bnd_SetEnery( lua_State* L ){
LuaBinder binder(L);
Hero* hero = (Hero*)binder.checkusertype(1,”Hero”);
Hero.setGetEnergy( binder.checknumer(2) );
return 1;
}
注意繫結函式的封裝策略將被用於對映物件:宿主物件使用對應的check和push方法組來進行對映,同時這些方法也用於以接收關聯型別為輸入引數。在我們為所有的繫結函式完成編碼。我們可以來編寫開啟庫的方法:
static const luaL_reg herolib[] = {
{ “Create”, bnd_Create },
{“Destroy”, bnd_Destory },
{“GetName”, bnd_GetName},
…
};
int luaopen_hero( lua_State *L ) {
LuaBinder binder(L);
Binder.init( “hero”, herolib );
return i;
}
繫結宿主物件和Lua數值
把C/C++物件和Lua繫結的方法就是把它的記憶體地址對映成輕量的使用者資料。一個輕量的使用者資料可以用指標來表示(void *)並且它在Lua中只是作為一個普通的值。從指令碼環境中,能夠得到一個物件的值,做比較,並且能夠把它傳回給宿主。我們要在binder類中所實現的這個策略所對應的方法通過直接呼叫在標準庫中已經實現的函式來實現:
void Binder::init( const char *tname, const luaL_reg *flist ){
luaL_register( L, tname, flist );
}
void Binder::pushusertype( void* udata, const char* tname ){
lua_pushlightuserdata( L, udata );
}
void *Binder::checkusertype( int index, const char* tname ){
void *udata = lua_touserdata( L, index );
if ( udata ==0 ) luaL_typerror( L, index, tname );
return udata;
}
函式luaL_typerror在上面的實現中用於丟擲異常,指出輸入引數沒有一個有效的相關物件。
通過這個對映我們英雄類的策略,以下的Lua便是可用的:
Local h = Hero.Create(“myhero”)
Local e = Hero.GetEnergy(h)
Hero.SetEnergy(h, e-1)
Hero.Destroy()
把物件對映成簡單值至少有三個好處:簡單,高效和小的記憶體覆蓋。就像我們上面所見到的,這種策略是很直截了當的,並且Lua和宿主語言之間的通訊也是最高效的,那是因為它沒有引入任何的間接訪問和記憶體分配。然而,作為一個實現,這種簡單的策略因為使用者資料的值始終被當成有效的引數而變得不安全。傳入任何一個無效的物件都將回導致宿主程式的直接崩潰。
加入型別檢查
我們能夠實現一個簡單的實時的型別檢查機制來避免在Lua環境中導致宿主程式崩潰。當然,加入型別檢查會降低效率並且增加了記憶體的使用。如果指令碼只是用在遊戲的開發階段,那麼型別檢查機制可以在釋出之前始終關閉。
換句話說,如果指令碼工具要提供給終端使用者,那麼型別檢查就變得非常重要而且必須和產品一起釋出。
要新增型別檢查機制到我們的繫結到值的策略中,我們能夠建立一個把每一個物件和Lua相對應型別名字對映的表。(在這篇文章中所有提到的策略裡,我們都假定地址是宿主物件的唯一標識)。在這張表中,輕量的資料可以作為一個鍵,而字串(型別的名稱)可以作為值。
初始化方法負責建立這張表,並且讓它能夠被對映函式呼叫到。然而,保護它的獨立性也是非常重要的:從Lua環境中訪問是必須不被允許的;另外,它仍然有可能在Lua指令碼中使宿主程式崩潰。使用登錄檔來儲存來確保它保持獨立性是一個方法,它是一個全域性的可以被Lua API單獨訪問的變數。然而,因為登錄檔是唯一的並且全域性的,用它來儲存我們的對映物件也阻止了其他的C程式庫使用它來實現其他的控制機制。
另一個更好的方案是隻給繫結函式提供訪問型別檢查表的介面。直到Lua5.0,這個功能才能夠被實現。在Lua5.1中,有一個更好的(而且更高效)方法:環境表的使用直接和C函式相關。我們把型別檢查表設定成繫結函式的環境表。這樣,在函式裡,我們對錶的訪問就非常高效了。每一個函式都需要註冊到Lua中,從當前的函式中去繼承它的環境表。因而,只需要改變初始化函式的環境表關聯就足夠了――並且所有註冊過的辦定函式都會擁有同樣一個關聯的環境表。
現在,我們可以對binder類的執行型別檢測的方法進行編碼了:
void Binder::init(const char* tname, const luaL_reg* flist){
lua_newtable(L); //建立型別檢查表
lua_replace(L,LUA_ENVIRONINDEX ); // 把表設定成為環境表
luaL_register( L,tname, flist ); //建立庫表
}
void Binder::pushusertype(void *udata, const char* tname){
lua_pushlightuserdata(L,udata); //壓入地址
lua_pushvalue(L,-1); //重複地址
lua_pushstring(L,tname); //壓入型別名稱
lua_rawset(L,LUA_ENVIRONINDEX); //envtable[address] = 型別名稱
}
void* Binder::checkusertype( int index, const char* tname ){
void* udata = lua_touserdata( L,index );
if ( udata ==0 || !checktype(udata, tname) )
luaL_typeerror(L,index,tname);
return udata;
}
面程式碼使用一個私有的方法來實現型別檢查:
int Binder::checktype(void *udata, const char* tname){
lua_pushlightuserdata(L,udata); //壓入地址
lua_rawget( L, LUA_ENVIRONINDEX); //得到env[address]
const char* stored_tname = lua_tostring(t,-1);
int result = stored_tname && strcmp(stored_tname, tname) ==0;
lua_pop(L,1);
return result;
}
通過這些做法,我們使得繫結策略仍然非常高效。同樣,記憶體負載也非常低――所有物件只有一個表的實體。然而,為了防止型別檢查表的膨脹,我們必須在銷燬物件的繫結函式中釋放這些表。在bnd_Destroy函式中,我們必須呼叫這個私有方法:
void Binder::releaseusertype( void* udata ){
lua_pushlightuserdata(L,udata);
lua_pushnil(L);
lua_settable(L,LUA_ENVIRONINDEX);
}
小結:詳解如何把C++物件繫結到Lua輕量級的內容介紹完了,希望通過本文的學習能對你有所幫助!