匯出 C/C++ API 給 Lua 使用
cocos2d-x 和 quick-cocos2d-x 的底層程式碼都是使用 C++ 語言開發的。為了使用 Lua 指令碼語言進行開發,我們利用 tolua++ 工具,將大量的 C/C++ API 匯出到了 Lua 中。
使用 tolua++ 的基本步驟:
- 從 C/C++ 原始碼複製標頭檔案的內容到 .tolua(tolua++ 文件中稱為 .pkg)檔案中。
- 修改 .tolua 檔案內容,去掉 tolua++ 無法識別的內容,以及不需要匯出到 Lua 的定義。
- 執行 tolua++ 工具,根據 .tolua 檔案生成 luabinding 介面檔案(由 .cpp 檔案和 .h 檔案自稱)。
- 在 AppDelegate.cpp 中載入 luabinding 檔案。
- 在 AppDelegate 初始化 Lua 虛擬機器後,呼叫 luabinding 介面檔案中的 luaopen 函式,註冊 C/C++ API。
根據實踐,我們建議採用如下的方案來完成整個匯出工作。
從 C/C++ 原始檔建立 .tolua 檔案
假設我們的 MyClass.h 標頭檔案內容如下:
#ifndef __MY_CLASS_H_
#define __MY_CLASS_H_
class MyClass
{
public:
static void addTwoNumber(float number1, float number2);
private:
MyClass(void) {}
};
#endif // __MY_CLASS_H_
為了便於維護,應該將 .h 檔案對應的 tolua 命名為 XXX_luabinding.tolua。這樣生成的 luabinding 介面檔名就是 XXX_luabinding.cpp 和 XXX_luabinding.h,不會和已有的 C/C++ 原始檔衝突。
建立 MyClass_luabinding.tolua 檔案,並修改內容為:
class MyClass : public CCObject
{
public:
static void addTwoNumber(float number1, float number2);
};
這裡可以看到 .tolua 的內容有明顯簡化。詳細的內容修改規則,會在本文後續部分說明。
生成 luabinding 介面檔案
quick 為了簡化這一步工作,提供了相應工具,我們只需要建立一個指令碼檔案來呼叫工具即可。
-
建立 build_luabinding.sh 檔案,內容如下:
#!/usr/bin/env bash DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" cd "$DIR" OUTPUT_DIR="$DIR" MAKE_LUABINDING="$QUICK_COCOS2DX_ROOT"/bin/compile_luabinding.sh $MAKE_LUABINDING -E MyClass -d "$OUTPUT_DIR" MyClass_luabinding.tolua
記得在命令列中 chmod 755 build_luabinding.sh,否則無法執行該指令碼。
Windows 版的批處理內容如下:
@echo off set DIR=%~dp0 set OUTPUT_DIR=%DIR% set MAKE_LUABINDING="%QUICK_COCOS2DX_ROOT%\bin\compile_luabinding.bat" pushd cd /d "%DIR%" call %MAKE_LUABINDING% -E MyClass -d %OUTPUT_DIR% MyClass_luabinding.tolua
注意:執行指令碼前請確保已經下載了 quick-cocos2d-x,並且正確設定了 QUICK_COCOS2DX_ROOT 環境變數。環境配置請參考《入門指引》。
-
在命令列下執行我們建立的指令碼,如果一切順利,我們會看到如下輸出資訊:
creating file: MyClass_luabinding.cpp creating file: MyClass_luabinding.h // add to AppDelegate.cpp #include "MyClass_luabinding.h" // add to AppDelegate::applicationDidFinishLaunching() CCLuaStack* stack = CCScriptEngineManager::sharedManager() ->getScriptEngine() ->getLuaStack(); lua_State* L = stack->getLuaState(); luaopen_MyClass_luabinding(L);
載入 luabinding 介面檔案
開啟我們的專案,將 MyClass_luabinding.cpp 和 MyClass_luabinding.h 檔案加入工程。然後修改 AppDelegate.cpp 檔案:
-
在 AppDelegate.cpp 頭部區域新增:
#include "MyClass_luabinding.h"
-
在 AppDelegate::applicationDidFinishLaunching() 函式內新增:
luaopen_MyClass_luabinding(L);
注意這一行程式碼應該新增在其他 luaopen 函式後面,例如:
// register lua engine CCLuaEngine *pEngine = CCLuaEngine::defaultEngine(); CCScriptEngineManager::sharedManager()->setScriptEngine(pEngine); CCLuaStack *pStack = pEngine->getLuaStack(); lua_State* L = pStack->getLuaState(); // load lua extensions luaopen_lua_extensions(L); // load cocos2dx_extra luabinding luaopen_cocos2dx_extra_luabinding(L); // thrid_party luaopen_third_party_luabinding(L); // CCBReader tolua_extensions_ccb_open(L); // MyClass luaopen_MyClass_luabinding(L);
應該將我們的程式碼追加到 tolua_extensions_ccb_open() 後面。
經過上述修改後,重新編譯執行專案應該就可以在 Lua 指令碼中使用我們匯出的 MyClass 物件極其方法了。
.tolua 檔案內容的修改規則
前面的 MyClass 是一個非常簡單的例子,但我們實際遊戲中的 C/C++ API 可能比較複雜。在修改 .tolua 檔案內容時,應該仔細閱讀以下內容。
刪除所有無需在 Lua 中使用的內容
匯出的 API 越多,在 Lua 虛擬機器中佔用的符號表空間就越多。因此我們第一步要做的就是刪除所有無需在 Lua 中使用的內容。
-
對於 enum、巨集定義,如果需要匯出,原文保留即可。但巨集定義只能匯出數值定義,例如:
#define kCCHTTPRequestMethodGET 0 #define kCCHTTPRequestMethodPOST 1
而非數值的巨集定義無法匯出,以下內容會匯出失敗:
#define kMyConstantString "HELLO"
-
刪除所有無法識別的巨集,例如 CC_DLL。
-
刪除 C++ class 中所有非 public 的定義。
-
刪除 C++ class 中的類成員變數。
-
刪除 inline 關鍵詞,以及 inline function 的實現,只保留宣告。
處理 CCObject 繼承類
CCObject 及其繼承類都具備“引用計數”和“自動釋放”機制。如果你的 C++ 物件是從 CCObject 繼承的,那麼必須告訴 tolua++ 做相應處理,否則可能出現記憶體洩漏等問題。
對於 quick,只需要在 build 指令碼中通過 -E CCOBJECTS 引數指定這些 class 的名字即可。
例如前面 MyClass 的示例中,用 -E CCOBJECTS=MyClass 告訴 tolua++ 應該將 MyClass 當作 CCObject 的繼承類進行處理。
如果有多個類,那麼每個類名之間用“,”分隔即可,例如:
$MAKE_LUABINDING -E CCOBJECTS=MyClass,MyClass2,MyClass3 -d "$OUTPUT_DIR" MyClass_luabinding.tolua
展開巨集
有些巨集是不能直接刪除的,例如 CC_PROPERTY。對於這類巨集,需要根據巨集定義,將巨集展開為宣告。
CC_PROPERTY(float, m_fDuration, Duration)
展開為:
float getDuration();
void setDuration(float v);
需要如此處理的巨集包括:CC_PROPERTY_READONLY, CC_PROPERTY, CC_PROPERTY_PASS_BY_REF, CC_SYNTHESIZE_READONLY, CC_SYNTHESIZE_READONLY_PASS_BY_REF, CC_SYNTHESIZE, CC_SYNTHESIZE_PASS_BY_REF, CC_SYNTHESIZE_RETAIN。
幸運的是這些巨集大多隻用在 cocos2d-x 基礎程式碼裡,我們自己的 C++ class 還是不要用這些巨集了。
處理名字空間
如果使用了名字空間,那麼在 .tolua 的頭部應該加入:
$using namespace myname;
這裡用到的“$”符號,後續內容會原樣放入 luabinding 檔案。
新增必要的 #include 指令
如果生成的 luabinding 介面檔案無法編譯,需要檢查是否是需要 include 相應的標頭檔案,並新增如下程式碼:
$#include "MyClass.h"
修改函式引數和返回值型別,去除 const 修飾符
一些函式的引數或返回值,使用了 const 修飾符。由於 tolua++ 的限制,並不能很好的處理這類定義,所以我們要從 .tolua 檔案中移除 const 修飾符。唯一例外的就是 const char* 不需要修改為 char*。
例如:
CCPoint convertToNodeSpace(const CCPoint& worldPoint);
應該修改為:
CCPoint convertToNodeSpace(CCPoint& worldPoint);
這樣修改的原因是 tolua++ 把 const CCPoint 和 CCPoint 當做兩個不同的型別來處理。如果不做修改,那麼呼叫函式時會報告引數型別不符。
從 C/C++ 函式返回多個值
如果一個函式的所有引數都是引用或指標型別,並且不是 const char*,那麼在 luabinding 介面檔案中,該函式會返回多個值。
例如:
void getPosition(float* x = 0, float* y = 0);
在 Lua 中呼叫這個函式,會得到兩個返回值:
local x, y = node:getPosition()
將 Lua 函式傳入 C/C++
quick 裡,允許將 Lua 函式傳入 C/C++,只要求 C/C++ 函式中使用 int 做引數型別。但在 .tolua 檔案裡,則必須使用 LUA_FUNCTION 做引數型別。
例如:
static CCHTTPRequest* createWithUrlLua(int listener,
const char* url,
int method = kCCHTTPRequestMethodGET);
listener 引數用於儲存傳入的 Lua 函式,所以 .tolua 檔案裡要改寫為:
static CCHTTPRequest* createWithUrlLua(LUA_FUNCTION listener,
const char* url,
int method = kCCHTTPRequestMethodGET);
具體用法請參考 lib/cocos2dx_extra/extra/network/CCHTTPRequest 中的 createWithUrlLua() 方法。
在 Lua 和 C/C++ 間交換二進位制資料
要從 C/C++ 返回二進位制資料給 Lua,函式返回值型別必須是 int,而 .tolua 檔案中修改返回值為 LUA_STRING。函式中,需要用 CCLuaStack::pushString() 將二進位制資料放入 Lua stack。然後返回“需要傳遞給 Lua 的值”的數量。
具體用法請參考 lib/cocos2dx_extra/extra/network/CCHTTPRequest 中的 getResponseDataLua() 方法。
從 Lua 傳遞二進位制資料給 C/C++ 很簡單,使用 const char* 引數型別和 int 型別引數分別指定二進位制資料的指標和資料長度。
具體用法請參考 lib/cocos2dx_extra/extra/crypto/CCCrypto 中的 decryptXXTEALua() 方法。
更多用法
關於利用 tolua++ 的更多用法,建議參考 lib/cocos2dx_extra 中的 CCCrypto、CCNative、CCHTTPRquest 等 class。這些 class 對 Lua 提供了良好的支援,具體用法上也覆蓋了絕大多數 C/C++ 和 Lua 互動的需求。
- END -