詳解如何把C++對象綁定到Lua輕量級
游戲中的使用腳本語言已經成為了一個標準應用。腳本語言能夠在游戲開發中扮演一個重要的角色,并且讓數據結構化,計劃事件,測試和調試這些工作更加容易。腳本語言也能夠允許像美術,策劃這些非程序專家通過一個高層的抽象腳本來為游戲編寫代碼。這個抽象層的一部分也能夠允許提供給玩家來定制整個游戲。
從程序員的角度上來看,把一個腳本語言嵌入到游戲中最主要的問題是如果為腳本語言提供對宿主對象的訪問(通常是C/C++對象)。在選擇一個腳本語言的時候有兩個關鍵的特性:嵌入相關問題和綁定相關問題。而這些是Lua語言的一些設計的初衷。可是,Lua語言并沒有提供任何自動創建綁定的工具,因為這是出于另外一個設計初衷:Lua只是提供機制,而不是策略。
因而,就有許多種策略可以用來在Lua中綁定宿主對象。每一種策略都有它的優點和缺點,游戲開發者必須在得到在腳本環境中所需要的功能需求之后確定***的策略。一些開發者可能只是把C/C++對象映射成簡單的數值,但是其他人可能需要實現運行期類型檢查機制,甚至是在Lua中擴展宿主的應用。另外一個需要處理的重要問題是,是否允許Lua來控制宿主對象的生命周期。在這篇文章中,我們將探究使用Lua的API來實現不同的宿主對象綁定策略。
綁定函數
為了說明不同策略的實現,讓我們考慮把一個簡單的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輕量級的內容介紹完了,希望通過本文的學習能對你有所幫助!