原地址: http://blog.segmentfault.com/hongliang/1190000000631630
Cocos2d-x下Lua調用自定義C++類和函數的最佳實踐
關於cocos2d-x下Lua調用C++的文檔看了不少,但沒有一篇真正把這事給講明白了,我自己也是個初學者,摸索了半天,總結如下:
cocos2d-x下Lua調用C++這事之所以看起來這么復雜、網上所有的文檔都沒講清楚,是因為存在5個層面的知識點:
1、在純C環境下,把C函數注冊進Lua環境,理解Lua和C之間可以互相調用的本質
2、在cocos2d-x項目里,把純C函數注冊進Lua環境,理解cocos2d-x是怎樣創建Lua環境的、以及怎樣得到這個環境並繼續自定義它
3、了解為什么要使用toLua++來注冊C++類
4、在純C++環境下,使用toLua++來把一個C++類注冊進Lua環境,理解toLua++的用法
5、在cocos2d-x項目里,使用cocos2d-x注冊自身的方式把自定義的C++類注冊進Lua環境,理解cocos2d-x是怎樣通過bindings-generator腳本來封裝toLua++的用法來節省工作量的
只有理解了前4層,在最后使用bindings-generator腳本的時候心里才會清清楚楚。而網上的文檔,要么是只解釋了第1層,要么是只填鴨式地告訴你第5層怎么用bindings-generator腳本,不僅中間重要的知識點一概不提,示例代碼往往也寫的不夠簡潔,這讓我這種看見C++就眼暈的人理解起來大為頭疼(不是我不會C++,而是我非常不接受C++的設計哲學,能避就避)。所以接下來的講解我會對每一層知識點逐一講解,示例代碼也不求完整嚴謹,而是盡量用最簡潔的方式把程序的關鍵點說明白。
第一層:純C環境下,把C函數注冊進Lua環境
直接看代碼比啰哩啰嗦講一大堆概念要清晰明了的多。建立一個a.lua和一個a.c文件,內容如下,一看就明白是怎么回事了:
a.lua
print(foo(99))
a.c
#include <lua.h>
#include <lualib.h> #include <lauxlib.h> int foo(lua_State *L) { int n = lua_tonumber(L, 1); lua_pushnumber(L, n + 1); return 1; } int main() { lua_State *L = lua_open(); luaL_openlibs(L); lua_register(L, "foo", foo); luaL_dofile(L, "a.lua"); lua_close(L); return 0; }
怎么樣,這代碼簡單吧?一看就明白,簡單的不能再簡單了。我特別煩示例代碼里又是判斷錯誤又是加代碼注釋的,本來看自己不會的代碼就夠吃力的了,還加那么多花花綠綠的干擾項,純粹增加學習負擔。
在命令行下用gcc來編譯並執行吧:
gcc a.c -llua && ./a.out
注意-llua
選項是必要的,因為要連接lua的庫。
看完上面那段代碼,再解釋起來就容易多了:
1、要想注冊進Lua環境,函數需要定義為這個樣:int xxx(lua_State *L)
2、使用lua_tonumber
、lua_tostring
等函數,來取得傳入的參數,比如lua_tonumber(L, 1)
就是得到傳入的第一個參數,且類型為數字
3、使用lua_pushnumber
、lua_pushstring
等函數,來將返回值壓入Lua的環境中,因為Lua支持函數返回多個值,所以可以push多個返回值進Lua環境
4、最終函數返回的數字表示有多少個返回值被壓入了Lua環境
5、使用lua_register
宏定義來將這個函數注冊進Lua環境,Lua腳本里就可以用它了,大功告成!就這么簡單!
第二層:在cocos2d-x環境下,把C函數注冊進Lua環境
也簡單:
1、在frameworks/runtime-src/Classes/
目錄下,找到AppDelegate.cpp
文件。如果frameworks目錄不存在,則需要參考這篇Blog:用Cocos Code IDE寫Lua,如何與項目中的C++代碼和諧相處
AppDelegate.cpp文件中的關鍵代碼如下:
```c++
auto engine = LuaEngine::getInstance();
ScriptEngineManager::getInstance()->setScriptEngine(engine);
LuaStack* stack = engine->getLuaStack();
stack->setXXTEAKeyAndSign("2dxLua", strlen("2dxLua"), "XXTEA", strlen("XXTEA")); //register custom function //LuaStack* stack = engine->getLuaStack(); //register_custom_function(stack->getLuaState());
可以看到cocos2d-x已經為我們留出了注冊自定義C函數的位置,在注釋代碼后面這么寫就可以了:
```cpp
lua_State *L = stack->getLuaState(); lua_register(L, "test_lua_bind", test_lua_bind);
也可以通過ScriptEngineManager
類從頭取得當前的LuaEngine
對象,然后再getLuaStack()
方法得到封裝的LuaStack
對象,再調用getLuaState()
得到原始的lua_State
結構指針。只要知道了入口位置,其他一切就不成問題了,還是挺簡單的。
感興趣的話可以去看一下ScriptEngineManager
類的詳細定義,在frameworks/cocos2d-x/cocos/base/CCScriptSupport.h
文件中。
BTW:這里還有一個小知識點,插入在AppDelegate.cpp中的自定義代碼盡量寫在COCOS2D_DEBUG
宏定義的判斷前面,因為在調試環境下和真機環境下后續執行的代碼是不一樣的:
#if (COCOS2D_DEBUG>0) if (startRuntime()) return true; #endif // 調試環境下代碼就不會走到這里了 engine->executeScriptFile(ConfigParser::getInstance()->getEntryFile().c_str()); return true;
2、接下來,找個地方把test_lua_bind
函數定義寫進去就算大功告成了。如果追求文件組織的優雅,按理說應該新建一個.c文件,但這樣的話搞不好會把自己陷入到編譯階段的泥潭里,所以先不追求優雅,而就在AppDelegate.cpp文件末尾寫上函數的定義就可以了,簡單清楚明了:
int test_lua_bind(lua_State *L) { int number = lua_tonumber(L, 1); number = number + 1; lua_pushnumber(L, number); return 1; }
3、大功告成,現在就可以在main.lua文件里使用test_lua_bind()
函數了:
local i = test_lua_bind(99) print("lua bind: " .. tostring(i))
4、如果是新建一個.c文件呢?把AppDelegate.cpp
文件里test_lua_bind
函數定義的代碼刪掉,在頭部#include
后面加入:
#include "test_lua_bind.h"
在frameworks/runtime-src/Classes
目錄下創建test_lua_bind.h
文件,內容如下:
extern "C" { #include "lua.h" #include "lualib.h" } int test_lua_bind(lua_State *L);
再創建test_lua_bind.c
文件,內容不變:
#include "test_lua_bind.h"
int test_lua_bind(lua_State *L) { int number = lua_tonumber(L, 1); number = number + 1; lua_pushnumber(L, number); return 1; }
此時用cocos compile -p mac
命令編譯,會發現test_lua_bind.c
文件並沒有被編譯。這是當然的,普通的C/C++項目都是用Makefile來指定編譯哪些.c/cpp文件的,當前的cocos2d-x項目雖然沒有Makefile文件,但也是遵循這個原則的,也即肯定是有一個地方來指定所有要編譯的文件的,需要在這個地方把test_lua_bind.c
加進去,使得整個項目編譯時把它也作為項目的一部分。
答案是,cocos2d-x項目沒有使用Makefile,而是非常聰明地使用了與具體環境相關的工程文件來作為命令行編譯的環境,比如在編譯iOS或Mac時就使用Xcode工程文件,在編譯Android時就使用Android.mk
文件。
所以,添加好了test_lua_bind.h
和test_lua_bind.c
文件后,用Xcode打開項目,將這倆文件添加進工程中就行了。
注意,千萬不要勾選“Copy items into destination group's folder(if needed)”,因為cocos2d-x的Xcode工程目錄組織不是常規的結構,一旦勾選這個,會導致這兩個文件被拷貝至frameworks/runtime-src/proj.ios_mac
目錄下,原來frameworks/runtime-src/Classes
目錄下的文件就廢掉了,這樣的組織方式會亂,而且會影響Android那邊對這倆文件的引用。
把test_lua_bind.h
和test_lua_bind.cpp
這倆文件添加進Xcode工程后,再去命令行執行cocos compile -p mac
,編譯就能成功了。
網上有其他文章說還要修改Xcode工程的“User Headers Path”,這個經過試驗是不需要的,哪怕把這倆文件放進新建的文件夾里也不需要,只要加入了Xcode工程即可,因為Xcode內部根本就不是按照文件夾的形式來組織文件的,它自己有一套叫做“Group”的東西。搞了好幾年iOS開發,對Xcode的這個特性還是熟悉的。
說到這就不禁要插一句對網上所有cocos2d-x文檔的吐槽了,學習cocos2d-x的人水平實在是良莠不齊,大部分人似乎都是對游戲熱衷的編程初學者,他們大多底子薄基礎差,甚至一大部分人之前都沒做過移動APP的開發,他們學習cocos2d-x只想知其然而不想知其所以然,給他們講他們也看不明白(因為編程基礎差),所以網上不少cocos2d-x文章都是只講123步驟,而不告訴你為什么這么做,包括cocos2d-x官方的大量文檔也是基於這個思路寫的,中文和英文都一樣。我看這些文章就特別痛苦,一邊看一邊心里就總是在想,“憑什么要這么做啊”、“這一步是為了什么啊”、“怎么這么麻煩啊”、“這個步驟明顯不是最佳實踐啊”、“解決這事為啥要這么麻煩”、“有更好的方法嗎”,所以我這種初學者來看cocos2d-x文檔就變成了不是單純的學習,而是學習、質疑、求證、反思、優化的過程,對別人來說cocos2d-x的入門比較容易,到我這里反倒成了入門比較難、入門之后比較容易了,因為文檔中的垃圾信息和無效信息實在是太多了,別人可以照單全收、以后懂了之后再慢慢剔除,我是必須從一開始就自己甄別垃圾、只保留最佳實踐,這也是這篇Blog寫的比較長的原因。
扯遠了。反正經過以上步驟,就完成了在cocos2d-x項目中把C函數注冊進Lua環境這件事。至此,算是徹底搞懂了Lua和C函數之間的互相調用關系,也能在cocos2d-x的Lua環境中使用自定義的C函數了。但這還不夠,因為一個正規的項目是需要狠好的組織結構的,全局C函數滿天飛肯定是不行的,好一點的情況是把所有的C函數都在Lua中組織為模塊注冊進去,更好一點的情況是把C++類注冊進Lua、並且C++類也是以Lua模塊為組織方式注冊進Lua環境的。這其實就是cocos2d-x自己把自己注冊進Lua環境的方式。
第三層:了解為什么要使用toLua++來注冊C++類
因為Lua的本質是C,不是C++,Lua提供給C用的API也都是基於面向過程的C函數來用的,要把C++類注冊進Lua形成一個一個的table環境是不太容易一下子辦到的事,因為這需要繞着彎地把C++類變成各種其他類型注冊進Lua,相當於用面向過程的思維來維護一個面向對象的環境。這其中的細節就不去深究了,總之正是因為如此,所以單純地手寫lua_register()
等代碼來注冊C++類是行不通的、代價高昂的,所以需要借助toLua++這個工具。
這一層的知識點看似簡單,但其實是非常重要的,只有理解了手工用lua_register()
去注冊C++類的難度,才能理解使用toLua++這類工具的必要性。只有理解了使用toLua++工具的必要性,才會潛下心來冷靜地接受toLua++本身的優點和缺點。只有看到了toLua++本身的缺點和使用上的麻煩,才會真心理解cocos2d-x使用bindings-generator
腳本帶來的好處。只有理解了bindings-generator
腳本帶來的好處,才能諒解這個腳本本身在使用上的一些不便之處。
第四層:在純C++環境下,使用toLua++來把一個C++類注冊進Lua環境
雖然終極方法是用bindings-generator
腳本來注冊C++類進cocos2d-x的Lua環境,但理解toLua++本身的用法還是狠有必要的,只有知道了toLua++原本的用法,才能更好地理解cocos2d-x是怎么把自己的C++類都注冊進Lua環境的,這不僅能讓編程時的思路更加清晰,也能為日后在源碼中尋找各種接口文檔的過程中不至於看不懂那一大堆tolua_beginmodule
、tolua_function
是什么意思。影響程序員學習提高的一大障礙就是忽略那些一知半解的代碼,不去刨根究底地搞明白。
使用toLua++的標准做法是:
1、准備好自己的C++類,該怎么寫就怎么寫
2、仿造這個類的.h文件,改一個.pkg文件出來,具體格式要按照toLua++的規定,比如移除所有的private成員等
3、建一個專門用來橋接C++和Lua之間的C++類,使用特殊的函數簽名來寫它的.h文件,.cpp文件不寫,等着toLua++來生成
4、給這個橋接的C++類寫一個.pkg文件,按照toLua++的特殊格式來寫,目的是把真正做事的C++類給定義進去
5、在命令行下用toLua++生成橋接類的.cpp文件
6、程序入口引用這個橋接類,執行生成的橋接函數,Lua環境中就可以使用真正做事的C++類了
toLua++這種自己手寫.pkg文件的方式古老又難受,所以我沒有仔細地去學習,這套流程放在10年前的那個年代是沒有太大問題的,作者怎么規定就怎么用好了,但是放在2014年的今天,任何程序的架構設計都講究學習成本低、輕量化、符合以往的習慣,因此toLua++用起來我覺得其實是難受的。
下面我以盡量最少的代碼來走一遍toLua++的流程,注意這是在純C++環境下,跟任何框架都沒關系,也不考慮內存釋放等細節:
MyClass.h
class MyClass { public: MyClass() {}; int foo(int i); };
MyClass.cpp
#include "MyClass.h" int MyClass::foo(int i) { return i + 100; }
MyClass.pkg
class MyClass { MyClass(); int foo(int i); };
MyLuaModule.h
extern "C" { #include "tolua++.h" } #include "MyClass.h" TOLUA_API int tolua_MyLuaModule_open(lua_State* tolua_S);
MyLuaModule.pkg
$#include "MyLuaModule.h" $pfile "MyClass.pkg"
main.cpp
extern "C" {
#include <lua.h> #include <lualib.h> #include <lauxlib.h> } #include "MyLuaModule.h" int main() { lua_State *L = lua_open(); luaL_openlibs(L); tolua_MyLuaModule_open(L); luaL_dofile(L, "main.lua"); lua_close(L); return 0; }
main.lua
local test = MyClass:new() print(test:foo(99))
先在命令行下執行:
tolua++ -o MyLuaModule.cpp MyLuaModule.pkg
此命令用來生成橋接文件MyLuaModule.cpp。注意命令行中-o參數的順序不能隨意擺放,從這個小事也能看出tolua++的古老和難用
生成好MyLuaModule.cpp文件后,就能看到它里面的那一大堆橋接代碼了,比如tolua_beginmodule
、tolua_function
等。以后看到這些東西就不陌生了,就明白這些函數只是toLua++用來做橋接的必備代碼了,簡單看一下代碼,就理解toLua++是怎樣把MyClass這個C++類注冊進Lua中的了:
接下來,用g++來編譯:
g++ MyClass.cpp MyLuaModule.cpp main.cpp -llua -ltolua++
默認就生成了a.out
文件,執行,就能看到main.lua的執行結果了:
至此,對toLua++的運作原理心里就透亮了,無非就是:
1、把自己該寫的類寫好
2、寫個.pkg文件,告訴toLua++這個類暴露出哪些接口給Lua環境
3、再寫個橋接的.h和.pkg文件,讓toLua++去生成橋接代碼
4、在程序里使用這個橋接代碼,類就注冊進Lua環境里了
第五層:使用cocos2d-x的方式來將C++類注冊進Lua環境
cocos2d-x在2.x版本里就是用toLua++和.pkg文件這么把自己注冊進Lua環境里的。不過這種方法明顯笨拙,既要寫真正做事的.pkg文件,也要寫橋接的.pkg文件和.h文件,工作量又大又枯燥。所以從cocos2d-x 3.x開始,用bindings-generator腳本代替了toLua++。
bindings-generator腳本的工作機制是:
1、不用挨個類地寫橋接.pkg和.h文件了,直接定義一個ini文件,告訴腳本哪些類的哪些方法要暴露出來,注冊到Lua環境里的模塊名是什么,就行了,等於將原來的每個類乘以3個文件的工作量變成了所有類只需要1個.ini文件
2、摸清了toLua++工具的生成方法,改由Python腳本動態分析C++類,自動生成橋接的.h和.cpp代碼,不調用tolua++命令了
3、雖然不再調用tolua++命令了,但是底層仍然使用toLua++的庫函數,比如tolua_function
,bindings-generator腳本生成的代碼就跟使用toLua++工具生成的幾乎一樣
bindings-generator腳本掌握了生成toLua++橋接代碼的主動權,不僅可以省下大量的.pkg和.h文件,而且可以更好地插入自定義代碼,達到cocos2d-x環境下的一些特殊目的,比如內存回收之類的。所以cocos2d-x從3.x開始放棄了toLua++和.pkg而改用了自己寫的bindings-generator腳本是非常值得贊賞的聰明做法。
接下來說怎么用bindings-generator腳本:
1、寫自己的C++類,按照cocos2d-x的規矩,繼承cocos2d::Ref類,以便使用cocos2d-x的內存回收機制。當然不這么干也行,但是不推薦,不然在Lua環境下對象的釋放狠麻煩。
2、編寫一個.ini文件,讓bindings-generator可以根據這個配置文件知道C++類該怎么暴露出來
3、修改bindings-generator腳本,讓它去讀取這個.ini文件
4、執行bindings-generator腳本,生成橋接C++類方法
5、用Xcode將自定義的C++類和生成的橋接文件加入工程,不然編譯不到
6、修改AppDelegate.cpp,執行橋接方法,自定義的C++類就注冊進Lua環境里了
看着步驟挺多,其實都狠簡單。下面一步一步來。
首先是自定義的C++類。我習慣將文件保存在frameworks/runtime-src/Classes/
目錄下:
frameworks/runtime-src/Classes/MyClass.h
#include "cocos2d.h" using namespace cocos2d; class MyClass : public Ref { public: MyClass() {}; ~MyClass() {}; bool init() { return true; }; CREATE_FUNC(MyClass); int foo(int i); };
frameworks/runtime-src/Classes/MyClass.cpp
#include "MyClass.h" int MyClass::foo(int i) { return i + 100; }
然后編寫.ini文件。在frameworks/cocos2d-x/tools/lua/
目錄下能看到genbindings.py
腳本和一大堆.ini文件,這些就是bindings-generator的實際執行環境了。隨便找一個內容比較少的.ini文件,復制一份,重新命名為MyClass.ini。大部分內容都可以湊合不需要改,這里僅列出必須要改的重要部分:
frameworks/cocos2d-x/tools/tolua/MyClass.ini
[MyClass]
prefix = MyClass target_namespace = my headers = %(cocosdir)s/../runtime-src/Classes/MyClass.h classes = MyClass
也即在MyClass.ini中指定MyClass.h文件的位置,指定要暴露出來的類,指定注冊進Lua環境的模塊名。
注意,這個地方我踩了個坑。如果.ini配置文件中存在macro_judgement = ...
宏定義,要特別小心,我第一次是從cocos2dx_controller.ini
文件復制來的,結果沒注意macro_judgement
,導致生成的橋接類文件加入了不該加入的宏,只在iOS和Android平台上才起作用,對Mac平台無效,這個要特別注意。
然后修改genbindings.py
文件129行附近,將MyClass.ini文件加進去:
frameworks/cocos2d-x/tools/tolua/genbindings.py
cmd_args = {'cocos2dx.ini' : ('cocos2d-x', 'lua_cocos2dx_auto'), \ 'MyClass.ini' : ('MyClass', 'lua_MyClass_auto'), \ ...
(其實這一步本來是可以省略的,只要讓genbindings.py腳本自動搜尋當前目錄下的所有ini文件就行了,不知道將來cocos2d-x團隊會不會這樣優化)
至此,生成橋接文件的准備工作就做好了,執行genbindings.py腳本:
python genbindings.py
(在Mac系統上可能會遇到缺少yaml、Cheetah包的問題,安裝這些Python包狠簡單,先sudo easy_install pip
,把pip裝好,然后用pip各種pip search
、sudo pip install
就可以了)
成功執行genbindings.py腳本后,會在frameworks/cocos2d-x/cocos/scripting/lua-bindings/auto/
目錄下看到新生成的文件:
每次執行genbindings.py腳本時間都挺長的,因為它要重新處理一遍所有的.ini文件,建議大膽修改腳本文件,靈活處理,讓它每次只處理需要的.ini文件就可以了,比如像這個樣子:
在frameworks/cocos2d-x/cocos/scripting/lua-bindings/auto/
目錄下觀察一下生成的C++橋接文件lua_MyClass_auto.cpp
,里面的注冊函數名字為register_all_MyClass()
,這就是將MyClass類注冊進Lua環境的關鍵函數:
編輯frameworks/runtime-src/Classes/AppDelegate.cpp
文件,首先在文件頭加入對lua_MyClass_auto.hpp
文件的引用:
然后在正確的代碼位置加入對register_all_MyClass
函數的調用:
最后在執行編譯前,將新加入的這幾個C++文件都加入到Xcode工程中,使得編譯環境知道它們的存在:
這其中還有一個小坑,由於lua_MyClass_auto.cpp
文件要引用MyClass.h
文件,而這倆文件分屬於不同的子項目,互相不認識頭文件的搜尋路徑,因此需要手工修改一下cocos2d_lua_bindings.xcodeproj
子項目的User Header Search Paths
配置。特別注意一共有幾個../
:
最后,就可以用cocos compile -p mac
命令重新編譯整個項目了,不出意外的話編譯一定是成功的。
修改main.lua文件中,嘗試調用一下MyClass類:
local test = my.MyClass:create() print("lua bind: " .. test:foo(99))
然后執行程序(用cocos rum -p mac
或在Cocos Code IDE中均可),見證奇跡的時刻~~~~咦我擦?!程序崩潰!為毛?
這是我作為cocos2d-x初學者遇到的最大的坑,坑了我整整一天半,具體的研究細節就不詳細說了,總之罪魁禍首是cocos2d-x框架中的CCLuaEngine.cpp
文件的這段代碼:
原因是executeScriptFile
函數執行時,對當前Lua環境中的棧進行了清理,當register_all_MyClass
函數被調用時,Lua棧是全空的狀態,函數內部執行到tolua_module
函數調用時就崩潰了:
解決辦法是修改AppDelegate.cpp為這個樣子:
文本形式的代碼如下:
AppDelegate.cpp
lua_State *L = stack->getLuaState(); lua_getglobal(L, "_G"); register_all_MyClass(L); lua_settop(L, 0);
重新編譯並執行,程序就正確執行了:
至此,就徹底搞清楚應該怎樣在cocos2d-x項目里綁定一個C函數或者C++類到Lua環境中了,感興趣的話可以再進一步深入研究Lua內部metatable的運作原理、類對象的生成與釋放、以及垃圾回收。我自己也是剛接觸cocos2d-x不到一個星期,理解不深,以上難免會有用詞不當或理解錯誤的地方,如有錯誤請多包涵。