詳解如何把C++對(duì)象綁定到Lua輕量級(jí)
游戲中的使用腳本語言已經(jīng)成為了一個(gè)標(biāo)準(zhǔn)應(yīng)用。腳本語言能夠在游戲開發(fā)中扮演一個(gè)重要的角色,并且讓數(shù)據(jù)結(jié)構(gòu)化,計(jì)劃事件,測試和調(diào)試這些工作更加容易。腳本語言也能夠允許像美術(shù),策劃這些非程序?qū)<彝ㄟ^一個(gè)高層的抽象腳本來為游戲編寫代碼。這個(gè)抽象層的一部分也能夠允許提供給玩家來定制整個(gè)游戲。
從程序員的角度上來看,把一個(gè)腳本語言嵌入到游戲中最主要的問題是如果為腳本語言提供對(duì)宿主對(duì)象的訪問(通常是C/C++對(duì)象)。在選擇一個(gè)腳本語言的時(shí)候有兩個(gè)關(guān)鍵的特性:嵌入相關(guān)問題和綁定相關(guān)問題。而這些是Lua語言的一些設(shè)計(jì)的初衷??墒?,Lua語言并沒有提供任何自動(dòng)創(chuàng)建綁定的工具,因?yàn)檫@是出于另外一個(gè)設(shè)計(jì)初衷:Lua只是提供機(jī)制,而不是策略。
因而,就有許多種策略可以用來在Lua中綁定宿主對(duì)象。每一種策略都有它的優(yōu)點(diǎn)和缺點(diǎn),游戲開發(fā)者必須在得到在腳本環(huán)境中所需要的功能需求之后確定***的策略。一些開發(fā)者可能只是把C/C++對(duì)象映射成簡單的數(shù)值,但是其他人可能需要實(shí)現(xiàn)運(yùn)行期類型檢查機(jī)制,甚至是在Lua中擴(kuò)展宿主的應(yīng)用。另外一個(gè)需要處理的重要問題是,是否允許Lua來控制宿主對(duì)象的生命周期。在這篇文章中,我們將探究使用Lua的API來實(shí)現(xiàn)不同的宿主對(duì)象綁定策略。
綁定函數(shù)
為了說明不同策略的實(shí)現(xiàn),讓我們考慮把一個(gè)簡單的C++類綁定到Lua中。實(shí)現(xiàn)的目標(biāo)是在Lua中實(shí)現(xiàn)對(duì)類的訪問,因此允許腳本通過導(dǎo)出的函數(shù)來使用宿主所提供的服務(wù)。這里主要的想法是使用一個(gè)簡單的類來引導(dǎo)我們的討論。下面討論的是一個(gè)虛構(gòu)游戲中的英雄類,有幾個(gè)將會(huì)被映射到Lua中的公用方法。
- class Hero{
- public:
- Hero( const char* name );
- ~Hero();
- const char* GetName();
- void SetEnergy( double energy );
- double GetEnergy();
- };
要把類方法綁定到Lua中,我們必須使用Lua的API來編寫綁定功能。每一個(gè)綁定函數(shù)都負(fù)責(zé)接收Lua的值作為輸入?yún)?shù),同時(shí)把它們轉(zhuǎn)化成相應(yīng)的C/C++數(shù)值,并且調(diào)用實(shí)際的函數(shù)或者方法,同時(shí)把它們的返回值給回到Lua中。從標(biāo)準(zhǔn)發(fā)布版本的Lua中,Lua API和輔助庫提供了不少方便的函數(shù)來實(shí)現(xiàn)Lua到C/C++值的轉(zhuǎn)換,同樣,也為C/C++到Lua值的轉(zhuǎn)換提供了函數(shù)。例如,luaL_checknumber提供了把輸入?yún)?shù)轉(zhuǎn)換到相對(duì)應(yīng)的浮點(diǎn)值的功能。
如果參數(shù)不能對(duì)應(yīng)到Lua中的數(shù)值類型,那么函數(shù)將拋出一個(gè)異常。相反的,lua_pushnumber把給定的浮點(diǎn)值添加到Lua參數(shù)棧的頂端。還有一系列相類似的函數(shù)來映射其他的基本的Lua類型和C/C++數(shù)據(jù)類型。我們目前最主要的目標(biāo)提出不同的策略來擴(kuò)展標(biāo)準(zhǔn)Lua庫和它為轉(zhuǎn)換C/C++類型對(duì)象所提供的功能。為了使用C++的習(xí)慣,讓我們創(chuàng)建一個(gè)叫做Binder的類來封裝在Lua和宿主對(duì)象中互相轉(zhuǎn)化值的功能。這個(gè)類也提供了一個(gè)把將要導(dǎo)出到Lua中的模塊初始化的方法。
- class Binder
- {
- public:
- // 構(gòu)造函數(shù)
- 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 );
- };
類的構(gòu)造函數(shù)接收Lua_state來映射對(duì)象。初始化函數(shù)接收了將被限制的類型名字,也被表示為庫的名稱(一個(gè)全局變量名來表示在Lua中的類表),并且直接調(diào)用了標(biāo)準(zhǔn)的Lua庫。例如,映射一個(gè)數(shù)值到Lua中,或者從Lua映射出來的方法可能是這樣的:
- void Binder::pushnumber( double v )
- {
- lua_pushnumber( L,v );
- }
- double Binder::checknumber( int index )
- {
- return luaL_checknumber( L,index );
- }
真正的挑戰(zhàn)來自把用戶自定義類型互相轉(zhuǎn)換的函數(shù):pushusertype和checkusertype。這些方法必須保證映射對(duì)象的綁定策略和目前使用中的一致。每一種策略都需要不同的庫的裝載方法,因而要給出初始化方法init的不同實(shí)現(xiàn)。
一旦我們有了一個(gè)binder的實(shí)現(xiàn),那么綁定函數(shù)的代碼是非常容易寫的。例如,綁定函數(shù)相關(guān)的類的構(gòu)造函數(shù)和析構(gòu)函數(shù)是如下代碼:
- 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方法的綁定函數(shù)能夠像如下編碼:
- 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;
- }
注意綁定函數(shù)的封裝策略將被用于映射對(duì)象:宿主對(duì)象使用對(duì)應(yīng)的check和push方法組來進(jìn)行映射,同時(shí)這些方法也用于以接收關(guān)聯(lián)類型為輸入?yún)?shù)。在我們?yōu)樗械慕壎ê瘮?shù)完成編碼。我們可以來編寫打開庫的方法:
- 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;
- }
綁定宿主對(duì)象和Lua數(shù)值
把C/C++對(duì)象和Lua綁定的方法就是把它的內(nèi)存地址映射成輕量的用戶數(shù)據(jù)。一個(gè)輕量的用戶數(shù)據(jù)可以用指針來表示(void *)并且它在Lua中只是作為一個(gè)普通的值。從腳本環(huán)境中,能夠得到一個(gè)對(duì)象的值,做比較,并且能夠把它傳回給宿主。我們要在binder類中所實(shí)現(xiàn)的這個(gè)策略所對(duì)應(yīng)的方法通過直接調(diào)用在標(biāo)準(zhǔn)庫中已經(jīng)實(shí)現(xiàn)的函數(shù)來實(shí)現(xiàn):
- 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;
- }
函數(shù)luaL_typerror在上面的實(shí)現(xiàn)中用于拋出異常,指出輸入?yún)?shù)沒有一個(gè)有效的相關(guān)對(duì)象。
通過這個(gè)映射我們英雄類的策略,以下的Lua便是可用的:
- Local h = Hero.Create(“myhero”)
- Local e = Hero.GetEnergy(h)
- Hero.SetEnergy(h, e-1)
- Hero.Destroy()
把對(duì)象映射成簡單值至少有三個(gè)好處:簡單,高效和小的內(nèi)存覆蓋。就像我們上面所見到的,這種策略是很直截了當(dāng)?shù)?,并且Lua和宿主語言之間的通信也是***效的,那是因?yàn)樗鼪]有引入任何的間接訪問和內(nèi)存分配。然而,作為一個(gè)實(shí)現(xiàn),這種簡單的策略因?yàn)橛脩魯?shù)據(jù)的值始終被當(dāng)成有效的參數(shù)而變得不安全。傳入任何一個(gè)無效的對(duì)象都將回導(dǎo)致宿主程序的直接崩潰。
加入類型檢查
我們能夠?qū)崿F(xiàn)一個(gè)簡單的實(shí)時(shí)的類型檢查機(jī)制來避免在Lua環(huán)境中導(dǎo)致宿主程序崩潰。當(dāng)然,加入類型檢查會(huì)降低效率并且增加了內(nèi)存的使用。如果腳本只是用在游戲的開發(fā)階段,那么類型檢查機(jī)制可以在發(fā)布之前始終關(guān)閉。
換句話說,如果腳本工具要提供給最終用戶,那么類型檢查就變得非常重要而且必須和產(chǎn)品一起發(fā)布。
要添加類型檢查機(jī)制到我們的綁定到值的策略中,我們能夠創(chuàng)建一個(gè)把每一個(gè)對(duì)象和Lua相對(duì)應(yīng)類型名字映射的表。(在這篇文章中所有提到的策略里,我們都假定地址是宿主對(duì)象的唯一標(biāo)識(shí))。在這張表中,輕量的數(shù)據(jù)可以作為一個(gè)鍵,而字符串(類型的名稱)可以作為值。
初始化方法負(fù)責(zé)創(chuàng)建這張表,并且讓它能夠被映射函數(shù)調(diào)用到。然而,保護(hù)它的獨(dú)立性也是非常重要的:從Lua環(huán)境中訪問是必須不被允許的;另外,它仍然有可能在Lua腳本中使宿主程序崩潰。使用注冊(cè)表來存儲(chǔ)來確保它保持獨(dú)立性是一個(gè)方法,它是一個(gè)全局的可以被Lua API單獨(dú)訪問的變量。然而,因?yàn)樽?cè)表是唯一的并且全局的,用它來存儲(chǔ)我們的映射對(duì)象也阻止了其他的C程序庫使用它來實(shí)現(xiàn)其他的控制機(jī)制。
另一個(gè)更好的方案是只給綁定函數(shù)提供訪問類型檢查表的接口。直到Lua5.0,這個(gè)功能才能夠被實(shí)現(xiàn)。在Lua5.1中,有一個(gè)更好的(而且更高效)方法:環(huán)境表的使用直接和C函數(shù)相關(guān)。我們把類型檢查表設(shè)置成綁定函數(shù)的環(huán)境表。這樣,在函數(shù)里,我們對(duì)表的訪問就非常高效了。每一個(gè)函數(shù)都需要注冊(cè)到Lua中,從當(dāng)前的函數(shù)中去繼承它的環(huán)境表。因而,只需要改變初始化函數(shù)的環(huán)境表關(guān)聯(lián)就足夠了――并且所有注冊(cè)過的辦定函數(shù)都會(huì)擁有同樣一個(gè)關(guān)聯(lián)的環(huán)境表。
現(xiàn)在,我們可以對(duì)binder類的執(zhí)行類型檢測的方法進(jìn)行編碼了:
- void Binder::init(const char* tname, const luaL_reg* flist){
- lua_newtable(L); //創(chuàng)建類型檢查表
- lua_replace(L,LUA_ENVIRONINDEX ); // 把表設(shè)置成為環(huán)境表
- luaL_register( L,tname, flist ); //創(chuàng)建庫表
- }
- void Binder::pushusertype(void *udata, const char* tname){
- lua_pushlightuserdata(L,udata); //壓入地址
- lua_pushvalue(L,-1); //重復(fù)地址
- 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;
- }
面代碼使用一個(gè)私有的方法來實(shí)現(xiàn)類型檢查:
- 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;
- }
通過這些做法,我們使得綁定策略仍然非常高效。同樣,內(nèi)存負(fù)載也非常低――所有對(duì)象只有一個(gè)表的實(shí)體。然而,為了防止類型檢查表的膨脹,我們必須在銷毀對(duì)象的綁定函數(shù)中釋放這些表。在bnd_Destroy函數(shù)中,我們必須調(diào)用這個(gè)私有方法:
- void Binder::releaseusertype( void* udata ){
- lua_pushlightuserdata(L,udata);
- lua_pushnil(L);
- lua_settable(L,LUA_ENVIRONINDEX);
- }
小結(jié):詳解如何把C++對(duì)象綁定到Lua輕量級(jí)的內(nèi)容介紹完了,希望通過本文的學(xué)習(xí)能對(duì)你有所幫助!