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++的用法來節省工作量的
第一層:純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標准庫
lua_register(L, "foo", foo); //注冊c函數到lua環境
luaL_dofile(L, "a.lua"); //執行lua腳本
lua_close(L); //關閉環境
return0;
}
第二層:在cocos2d-x環境下,把C函數注冊進Lua環境
AppDelegate.cpp文件中的關鍵代碼如下:
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函數的位置,在注釋代碼后面這么寫就可以了:
lua_State *L = stack->getLuaState();
lua_register(L, "test_lua_bind", test_lua_bind);
也可以通過ScriptEngineManager
類從頭取得當前的LuaEngine
對象,然后再getLuaStack()
方法得到封裝的LuaStack
對象,再調用getLuaState()
得到原始的lua_State
結構指針。。
2、接下來,找個地方把test_lua_bind
函數定義寫進去就算大功告成了。
int test_lua_bind(lua_State *L){
int number = lua_tonumber(L, 1);
number = number + 1;
lua_pushnumber(L, number);
return1;
}
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);
return1;
}
普通的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
文件后,將這倆文件添加進工程中就行了。
再去命令行執行cocos compile -p win
,編譯就能成功了。
第三層:了解為什么要使用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環境
理解toLua++本身的用法還是必要的,知道了toLua++原本的用法,才能更好地理解cocos2d-x是怎么把自己的C++類都注冊進Lua環境的。
使用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++的流程,注意這是在純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 c++類的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 橋接類的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))
先在命令行下執行:(生成橋接文件的實現體MyLuaModule.cpp)
tolua++ -o MyLuaModule.cpp MyLuaModule.pkg
意命令行中-o參數的順序不能隨意擺放,
生成好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 3.x開始,用bindings-generator腳本代替了toLua++。
bindings-generator腳本的工作機制是:
1、不用挨個類地寫橋接.pkg和.h文件了,直接定義一個ini文件,告訴腳本哪些類的哪些方法要暴露出來,注冊到Lua環境里的模塊名是什么
2、摸清了toLua++工具的生成方法,改由Python腳本動態分析C++類,自動生成橋接的.h和.cpp代碼,不調用tolua++命令了
3、雖然不再調用tolua++命令了,但是底層仍然使用toLua++的庫函數,比如tolua_function
,bindings-generator腳本生成的代碼就跟使用toLua++工具生成的幾乎一樣
bindings-generator腳本掌握了生成toLua++橋接代碼的主動權,不僅可以省下大量的.pkg和.h文件,而且可以更好地插入自定義代碼,達到cocos2d-x環境下的一些特殊目的,比如內存回收之類的。
接下來說怎么用bindings-generator腳本:
1、寫自己的C++類,按照cocos2d-x的規矩,繼承cocos2d::Ref類,以便使用cocos2d-x的內存回收機制。
2、編寫一個.ini文件,讓bindings-generator可以根據這個配置文件知道C++類該怎么暴露出來
3、修改bindings-generator腳本,讓它去讀取這個.ini文件
4、執行bindings-generator腳本,生成橋接C++類方法
5、將自定義的C++類和生成的橋接文件加入工程,不然編譯不到
6、修改AppDelegate.cpp,執行橋接方法,自定義的C++類就注冊進Lua環境里了
看着步驟挺多,其實都狠簡單。下面一步一步來。
1.首先是自定義的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;
}
2.然后編寫.ini文件。在frameworks/cocos2d-x/tools/tolua/
目錄下能看到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環境的模塊名。
然后修改genbindings.py
MyClass.ini文件加進去:
3.frameworks/cocos2d-x/tools/tolua/genbindings.py
cmd_args = {'cocos2dx.ini' : ('cocos2d-x', 'lua_cocos2dx_auto'), \
'MyClass.ini' : ('MyClass', 'lua_MyClass_auto'), \
...
4.至此,生成橋接文件的准備工作就做好了,執行genbindings.py腳本:
python ./genbindings.py
成功執行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環境的關鍵函數:
5.編輯frameworks/runtime-src/Classes/AppDelegate.cpp
文件,首先在文件頭加入對lua_MyClass_auto.hpp
文件的引用:
然后在正確的代碼位置加入對register_all_MyClass
函數的調用:
如何是lua工程則在:lua_module_register.h 中添加上述調用。
最后在執行編譯前,將新加入的這幾個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))
6.android上運行的話需要做的事情是要將生成的橋接文件lua_MyClass_auto.cpp放到android.mk中。
配置ini時需要注意的選項:
-
[title]:要配置將被使用的工具/ tolua的/ gengindings.py腳本的稱號。一般來說,標題可以是文件名。
-
prefix:要配置一個函數名的前綴,通常,我們還可以使用文件名作為前綴 生成函數一次為前綴。
-
target_namespace:要配置在腳本層模塊的名字。在這里,我們使用cc作為模塊名,當你想在腳本層REF的名稱,您必須將一個名為前綴,CC在名稱的前面。例如,CustomClass可以參考作為
cc.CustomClass
。 -
headers:要配置所有需要解析的頭文件和%(cocosdir)s是的Cocos2d-x的引擎的根路徑。
-
classes:要配置所有綁定所需的類。在這里,它支持正則表達式。因此,我們可以設置MyCustomClass。*在這里,用於查找多個特定的用法,你可以對照到
tools/tolua/cocos2dx.ini
。 -
skip:要配置需要被忽略的功能。現在綁定發電機無法解析的void *類型,並委托類型,所以這些類型的需要進行手動綁定。而在這種情況下,你應該忽略所有這些類型,然后再手動將它們綁定。你可以對照到配置文件路徑下的
cocos/scripting/lua-bindings/auto
。 -
rename_functions:要配置的功能需要在腳本層進行重命名。由於某些原因,開發者希望更多的腳本友好的API,所以配置選項就是為了這個目的。
-
rename_classes:不在使用。
-
remove_prefix:不在使用。
- base_classes_to_skip = #當被它們的子類發現的時候會跳過的基類
-
classes_have_no_parents:要配置是過濾器所需要的父類。這個選項是很少修改。
-
abstract_classes:要配置的公共構造並不需要導出的類。
-
script_control_cpp:是的。要配置腳本層是否管理對象的生命周期。如果沒有,那么C++層關心他們的生命周期。
現在,它是不完善的,以控制原生對象的續航時間在腳本層。所以,你可以簡單地把它設置為no。