cocos C++与Lua的交互
环境: cocos3.10 Lua5.1.4 Visual Studio 2013
简介
Lua作为一种脚本语言(https://www.lua.org/),它提供了很多的 C API使得C/C++与Lua之间进行通信交互。
在cocos2d-x中lua与C++的交互,主要借助于第三方工具tolua++来实现。
该工具会将C++按照Lua支持的C API指定生成绑定代码,以便于Lua通过这些绑定代码更快捷的访问C++下的类及方法相关。
Lua_State
一般脚本语言的运行需要宿主的存在,且要有对应的虚拟机。
在cocos中,我们可以认为C/C++就是lua的宿主,而虚拟机说白了就是要提供一个lua运行的环境,该环境下需要保存Lua脚本运行的内存空间,全局变量,库文件等, 该环境被称为Lua_State。
在Lua_State环境下,若实现Lua与C/C++的数据交互,我们需要有个容器来对数据进行传递,这个容器就是Lua虚拟栈。
Lua虚拟栈
栈的特点是先进后出的,在Lua的虚拟栈中,栈中数据通过索引值进行定位,索引值可为正数,也可为负数。
通俗的来说,正数为1的永远表示栈底,负数为-1的永远表示栈顶。
(来源:https://blog.csdn.net/zhuzhuyule/article/details/41086745)
假设我们的C++想访问lua文件中的数据:
-- 文件命名为:test.lua str = "Get Lua Data Sucess!!!" function Add(num1, num2) return num1 + num2 end
以C++获取lua变量str的数据为例,其简单的通信流程:
1. C/C++将参数str放入Lua堆栈(栈顶)中
2. Lua从堆栈中获取参数str,并将栈顶置为空
3. Lua从全局表中查找参数str对应的数据
4. 全局表将参数str的数据反馈给Lua
5. Lua将参数str的返回值放入堆栈中,此时返回值位于栈顶
6. C++从堆栈中获取返回值
/* 环境配置 1. 新建项目,选择Empty Project,在项目的Source Files新增.cpp文件 2. 若有Lua的相关环境,可将Lua/5.1目录下的include,lib文件夹拷贝到与.cpp文件同目录下 若无,则推荐LuaForWindows 其网址为:http://files.luaforge.net/releases/luaforwindows/luaforwindows 它会自动配置lua的环境,并安装SciTE工具相关,以后就可以在控制台,SciTE输入lua相关代码进行调试 属性配置,打开项目属性: 1. C/C++ -> General -> Additional Include Directories 将include目录添加进去 2. Linker -> General -> Additional Library Directories 将lib目录添加进去 3. 再通过Linker -> Input -> Additional Dependencies 添加lua5.1.lib, lua51.lib */ #include <iostream> #include <string.h> extern "C" { #include "lua.h" // 提供了Lua的基本函数,在lua.h中的函数均已"lua_"为前缀 #include "lualib.h" // 定义lua的标准库函数,比如table, io, math等 #include "lauxlib.h" // 提供了辅助库相关,以"luaL_"为前缀 } void main(){ // 创建lua环境,并加载标准库 lua_State* pL = lua_open(); luaL_openlibs(pL); // 加载lua文件,返回0表示成功 int code = luaL_loadfile(pL, "test.lua"); if (code != 0){ return; } // 执行lua文件,参数分别为,lua环境,输入参数个数,返回值个数 lua_call(pL, 0, 0); // 重置栈顶索引,设置为0表示栈清空 lua_settop(pL, 0); // ------------- 读取变量 ------------- //lua_getglobal 主要做了这么几件事: 将参数压入栈中,lua获取参数的值后再将返回的结果压入栈中 lua_getglobal(pL, "str"); // 判定栈顶值类型是否为string,返回1表示成功,0表示失败 int isStr = lua_isstring(pL, 1); if (isStr == 1) { // 获取栈顶值,并将lua值转换为C++类型 std::string str = lua_tostring(pL, 1); std::cout << "str = " << str.c_str() << std::endl; } // ------------- 读取函数 ------------- lua_getglobal(pL, "Add"); // 将函数所需要的参数入栈 lua_pushnumber(pL, 1); // 压入第一个参数 lua_pushnumber(pL, 2); // 压入第二个参数 /* lua_pcall与lua_call类似,均用于执行lua文件,其方法分别为: void lua_call(lua_State *L, int nargs, int nresults); int lua_pcall(lua_State *L, int nargs, int nresults, int errfunc); 两者的区别在于: 前者在出现错误,程序会崩溃。后者多了一个errfunc索引,用于准确定位错误处理函数。 函数执行成功返回0,失败后可通过获取栈顶信息获取错误数据 两者的共同之处在于: 会根据nargs将参数按次序入栈,并根据nresults将返回值按次序填入栈中 若返回值结果数目大于nresults时,多余的将被丢弃;若小于nresults时,则按照nil补齐。 */ int result = lua_pcall(pL, 2, 1, 0); if (result != 0) { const char *pErrorMsg = lua_tostring(pL, -1); std::cout << "ERROR:" << pErrorMsg << std::endl; lua_close(pL); return; } /* 此处的栈中情况: ------------- 栈顶 ------------- 正索引 负索引 类型 返回值 2 -1 number 3 1 -2 string "Get Lua Data Sucess!!!" ------------- 栈底 ------------- 因此如下的索引获取数字索引可以使用-1或者2 */ int isNum = lua_isnumber(pL, -1); if (isNum == 1) { double num = lua_tonumber(pL, -1); std::cout << "num = " << num << std::endl; } // 关闭state环境,即销毁Lua_State对象,并释放Lua动态分配的空间 lua_close(pL); system("pause"); }
在如上的代码中我们发现:
1. C++在获取不同文件下的方法时,是通过include引用后,然后就直接调用;
2. Lua却是在通过luaL_loadfile进行加载,然后再通过lua_call/lua_pcall进行执行后才能获取对应的变量或者函数返回值。
其原因在于:lua的脚本若为执行在其全局变量表中是不会存储相关数据的,这一点千万要注意。
接下来我们介绍一些lua C API常用方法:
/* 获取栈顶索引即栈中元素的个数,因为栈底为1,所以栈顶索引为多少,就代表有多少个元素 */ int lua_gettop(lua_State *L); /* 将栈顶索引设置为指定的数值 若设置的index比原栈顶高,则以nil补足。若index比原栈顶低,高出的部分舍弃。 比如: 栈中有8个元素,若index为7,则表示删除了一个栈顶的元素。若index为0,表示清空栈 注意,index可为正数也可为负数,但若index为正数表示相对于栈底设置的,若为负数则相对于栈顶而设置的 */ void lua_settop(lua_State *L, int index); /* 将栈中索引元素的副本压入栈顶 比如:从栈底到栈顶,元素状态为10,20,30,40;若索引为3则元素状态为:10,20,30,40,30 类似的还有: lua_pushnil: 压入一个nil值 lua_pushboolean: 压入一个bool值
lua_pushnumber: 压入一个number值 */ void lua_pushvalue(lua_State *L, int index); /* 删除指定索引元素,并将该索引之上的元素填补空缺 比如:从栈底到栈顶,元素状态为10,20,30,40;若索引为-3则元素状态为10,30,40 */ void lua_remove(lua_State *L, int index); /* 将栈顶元素替换索引位置的的元素 比如:从栈底到栈顶,元素状态为10,20,30,40,50;若索引为2则,元素状态为10,50,30,40 即索引为2的元素20被栈顶元素50替换 */ void lua_replace(lua_State *L, int index); /* 获取栈中指定索引元素的类型,若失败返回类型LUA_TNONE 其它类型有: LUA_TBOOLEAN, LUA_TNUMBER, LUA_TSTRING, LUA_TTABLE LUA_TFUNCTION, LUA_USERDATA等 */ int lua_type(lua_State *L, int idx); /* 检测栈中元素是否为某个类型,成功返回1,失败返回0 类似的还有: lua_isnumber, lua_isstring, lua_iscfunction, lua_isuserdata */ int lua_isXXX(lua_State *L, int index); /* 将栈中元素转换为C语言指定类型 */ lua_Number lua_tonumber(lua_State *L, int idx); lua_Integer lua_tointeger(lua_State *L, int idx); int lua_toboolean(lua_State *L, int idx); const char* lua_tolstring(lua_State *L, int idx, size_t *len); lua_CFunction lua_tocfunction(lua_State *L, int idx); void* lua_touserdata(lua_State *L, int idx);
如上仅仅简单介绍了下C/C++与Lua交互的基本原理,我们真正的目的是为了了解Lua是如何调用cocos引擎对应的类方法的。
cocos Lua框架
Lua在cocos引擎封装相关,它主要被放在cocos引擎的libluacocos2d中
auto: 使用tolua++工具自动生成的C++代码相关
manual:放置了cocos扩展的一些功能,比如LuaEngine, LuaStack, LuaBridge(android, ios sdk交互相关)等
luajit: 高效版的lua库,额外添加了lua没有的cocos库,并在对浮点计算,循环等进行了优化
luasocket: 网络库相关
tolua: tolua++库相关,实质是对Lua C库进行的再封装
xxtea: 加密相关
而cocos引擎主要通过LuaEngine和LuaStack对Lua进行管理,LuaEngine是一个管理LuaStack的单例,而LuaStack则用于对Lua_State进行了封装。以cocos2d-lua在启动时调用main.lua为例,简单的说主要分为三个步骤:
1. 初始化LuaEngine,获取LuaState环境
2. 注册C++模块相关到Lua中
3. 执行Lua脚本
以cocos2d-lua启动游戏时,运行main.lua为例,主要接口在AppDelegate::applicationDidFinishLauching()中:
bool AppDelegate::applicationDidFinishLaunching() { // 初始化LuaEngine,在getInstance中会初始化LuaStack,LuaStack初始化Lua环境相关 auto engine = LuaEngine::getInstance(); // 将LuaEngine添加到脚本引擎管理器ScriptEngineManager中 ScriptEngineManager::getInstance()->setScriptEngine(engine); // 获取Lua环境 lua_State* L = engine->getLuaStack()->getLuaState(); // 注册额外的C++ API相关,比如cocosstudio, spine, audio相关 lua_module_register(L); // register_all_packages(); // 设置cocos自带的加密相关 // 在LuaStack::executeScriptFile执行脚本文件时,会通过LuaStack::luaLoadBuffer对文件进行解密 LuaStack* stack = engine->getLuaStack(); stack->setXXTEAKeyAndSign("2dxLua", strlen("2dxLua"), "XXTEA", strlen("XXTEA")); // 执行Lua脚本文件 if (engine->executeScriptFile("main.lua")) { return false; } return true; }
通过LuaEngine::getInstance(),我们了解下LuaStack::init()的相关实现:
extern "C" { #include "lua.h" #include "tolua++.h" #include "lualib.h" #include "lauxlib.h" } bool LuaStack::init(void) { // 初始化Lua环境并打开标准库 _state = lua_open(); luaL_openlibs(_state); toluafix_open(_state); // 注册全局函数print到lua中,它会覆盖lua库中的print方法 const luaL_reg global_functions [] = { {"print", lua_print}, {"release_print",lua_release_print}, {nullptr, nullptr} }; // 注册全局变量 luaL_register(_state, "_G", global_functions); // 注册cocos2d-x引擎的API到lua环境中 g_luaType.clear(); register_all_cocos2dx(_state); ... #if (CC_TARGET_PLATFORM == CC_PLATFORM_IOS || CC_TARGET_PLATFORM == CC_PLATFORM_MAC) // 导入ios下调用object-c相关API LuaObjcBridge::luaopen_luaoc(_state); #endif #if (CC_TARGET_PLATFORM == CC_PLATFORM_ANDROID) // 导入android下调用java相关API LuaJavaBridge::luaopen_luaj(_state); #endif // 添加Lua的加载器,该方法将cocos2dx_lua_loader方法添加到Lua全局变量package下的loaders成员中
// 当requires加载脚本时,Lua会使用package下的loaders中的加载器,即cocos2dx_lua_loader来加载
// 设定cocos2dx_lua_loader,可以使得我们自定义设置搜索路径相关,且拓展实现对脚本的加密解密相关 addLuaLoader(cocos2dx_lua_loader); return true; }
我们看下cocod2dx_lua_loader的实现相关:
extern "C" { int cocos2dx_lua_loader(lua_State *L) { // 后缀为luac和lua static const std::string BYTECODE_FILE_EXT = ".luac"; static const std::string NOT_BYTECODE_FILE_EXT = ".lua"; // require传入的要加载的文件名,比如:require "cocos.init" 下的"cocos.init" std::string filename(luaL_checkstring(L, 1)); // 去掉后缀名".luac"或“.lua” size_t pos = filename.rfind(BYTECODE_FILE_EXT); if (pos != std::string::npos) { filename = filename.substr(0, pos); } else { pos = filename.rfind(NOT_BYTECODE_FILE_EXT); if (pos == filename.length() - NOT_BYTECODE_FILE_EXT.length()) { filename = filename.substr(0, pos); } } // 将"."替换为"/" pos = filename.find_first_of("."); while (pos != std::string::npos) { filename.replace(pos, 1, "/"); pos = filename.find_first_of("."); } Data chunk; std::string chunkName; FileUtils* utils = FileUtils::getInstance(); // 获取package.path的变量 lua_getglobal(L, "package"); lua_getfield(L, -1, "path"); // 通过package.path获取搜索路径相关,该路径为模版路径,格式类似于: // ?; ?.lua; c:\windows\?; /usr/local/lua/lua/?/?.lua 以“;”作为分割符 std::string searchpath(lua_tostring(L, -1)); lua_pop(L, 1); size_t begin = 0; size_t next = searchpath.find_first_of(";", 0); // 遍历package.path中的所有路径,查找文件是否存在,若文件存在则通过getDataFromFile读取文件数据 do { if (next == std::string::npos) next = searchpath.length(); std::string prefix = searchpath.substr(begin, next); if (prefix[0] == '.' && prefix[1] == '/') { prefix = prefix.substr(2); } pos = prefix.find("?.lua"); // 将?替换为文件名,获取搜索路径名,比如:?.lua替换为cocos/init.lua chunkName = prefix.substr(0, pos) + filename + BYTECODE_FILE_EXT; if (utils->isFileExist(chunkName)) { chunk = utils->getDataFromFile(chunkName); break; } else { chunkName = prefix.substr(0, pos) + filename + NOT_BYTECODE_FILE_EXT; if (utils->isFileExist(chunkName)) { chunk = utils->getDataFromFile(chunkName); break; } } // 指定搜素路径下不存在该文件,则下一个 begin = next + 1; next = searchpath.find_first_of(";", begin); } while (begin < (int)searchpath.length()); // 判定文件内容是否获取成功 if (chunk.getSize() > 0) { // 加载文件 LuaStack* stack = LuaEngine::getInstance()->getLuaStack(); stack->luaLoadBuffer(L, reinterpret_cast<const char*>(chunk.getBytes()), static_cast<int>(chunk.getSize()), chunkName.c_str()); } else { CCLOG("can not get file data of %s", chunkName.c_str()); return 0; } return 1; } }
通过此处的代码,我们可以了解到cocos2dx是如何搜索指定的lua文件。同时也会明白require为何可以使用"."来设定文件路径了,比如:
local cocosTest = require("app.Demo_Cocos.MainTest") local TetrisTest = require("app.Demo_Tetris.UITetrisMain")
下面我们看下LuaStack::luaLoadBuffer的实现:
int LuaStack::luaLoadBuffer(lua_State *L, const char *chunk, int chunkSize, const char *chunkName) { int r = 0; // 判定是否加密,若lua脚本加密,则解密后在加载脚本文件 // luaL_loadbuffer 用于加载并编译Lua代码,并将其压入栈中 if (_xxteaEnabled && strncmp(chunk, _xxteaSign, _xxteaSignLen) == 0) { // decrypt XXTEA xxtea_long len = 0; unsigned char* result = xxtea_decrypt((unsigned char*)chunk + _xxteaSignLen, (xxtea_long)chunkSize - _xxteaSignLen, (unsigned char*)_xxteaKey, (xxtea_long)_xxteaKeyLen, &len); skipBOM((const char*&)result, (int&)len); r = luaL_loadbuffer(L, (char*)result, len, chunkName); free(result); } else { skipBOM(chunk, chunkSize); r = luaL_loadbuffer(L, chunk, chunkSize, chunkName); } // 判定内容是否存在错误 #if defined(COCOS2D_DEBUG) && COCOS2D_DEBUG > 0 if (r) { switch (r) { case LUA_ERRSYNTAX: // 语法错误 CCLOG("[LUA ERROR] load \"%s\", error: syntax error during pre-compilation.", chunkName); break; case LUA_ERRMEM: // 内存分配错误 CCLOG("[LUA ERROR] load \"%s\", error: memory allocation error.", chunkName); break; case LUA_ERRRUN: // 运行错误 CCLOG("[LUA ERROR] load \"%s\", error: run error.", chunkName); break; case LUA_ERRFILE: // 文件错误 CCLOG("[LUA ERROR] load \"%s\", error: cannot open/read file.", chunkName); break; case LUA_ERRERR: // 运行错误处理函数时发生错误 CCLOG("[LUA ERROR] load \"%s\", while running the error handler function.", chunkName); default: // 未知错误 CCLOG("[LUA ERROR] load \"%s\", error: unknown.", chunkName); } // 通过lua的堆栈,获取栈顶的错误信息,将错误日志打印出来(-1永远表示栈顶) const char* error = lua_tostring(L, -1); CCLOG("[LUA ERROR] Error Result: %s", error); lua_pop(L, 1); } #endif return r; }
通过代码,我们可以了解到以下几方面的内容:
1. 了解LuaEngine在cocos2d-x中的通过LuaStack对 LuaState环境, LuaBridge等初始化
2. 了解lua脚本的加载,通过cocos2d-x自定义的cocos2dx_lua_loader实现,该接口
3. 了解Lua脚本的加密解密相关,及对lua脚本内容检测错误相关,通过luaLoadBuffer来实现。
tolua++接口相关:
我们以LuaStack下的register_all_cocos2dx()接口为例,tolua++相关的代码前缀都是“tolua_”
TOLUA_API int register_all_cocos2dx(lua_State* tolua_S) { tolua_open(tolua_S); tolua_module(tolua_S,"cc",0); tolua_beginmodule(tolua_S,"cc"); lua_register_cocos2dx_Ref(tolua_S); lua_register_cocos2dx_Node(tolua_S); // 省略... tolua_endmodule(tolua_S); return 1; }
以lua_register_cocos2dx_Ref为例,看下实现代码:
int lua_register_cocos2dx_Ref(lua_State* tolua_S) { tolua_usertype(tolua_S,"cc.Ref"); tolua_cclass(tolua_S,"Ref","cc.Ref","",nullptr); // tolua_function 表示对应的Ref所持有的public接口相关 tolua_beginmodule(tolua_S,"Ref"); tolua_function(tolua_S,"release",lua_cocos2dx_Ref_release); tolua_function(tolua_S,"retain",lua_cocos2dx_Ref_retain); tolua_function(tolua_S,"getReferenceCount",lua_cocos2dx_Ref_getReferenceCount); tolua_endmodule(tolua_S); std::string typeName = typeid(cocos2d::Ref).name(); g_luaType[typeName] = "cc.Ref"; g_typeCast["Ref"] = "cc.Ref"; return 1; }
再看下关于getReferenceCount的实现
int lua_cocos2dx_Ref_getReferenceCount(lua_State* tolua_S) { int argc = 0; cocos2d::Ref* cobj = nullptr; bool ok = true; #if COCOS2D_DEBUG >= 1 tolua_Error tolua_err; #endif // 从Lua栈中获取cocos对象类型,是否为cc.Ref #if COCOS2D_DEBUG >= 1 if (!tolua_isusertype(tolua_S,1,"cc.Ref",0,&tolua_err)) goto tolua_lerror; #endif // 将数据转换为Ref对象,若失败则提示:无效的对象 cobj = (cocos2d::Ref*)tolua_tousertype(tolua_S,1,0); #if COCOS2D_DEBUG >= 1 if (!cobj) { tolua_error(tolua_S,"invalid 'cobj' in function 'lua_cocos2dx_Ref_getReferenceCount'", nullptr); return 0; } #endif // 获取参数数目,-1的原因在于对象类型Ref也在栈中 argc = lua_gettop(tolua_S)-1; if (argc == 0) { if(!ok) { tolua_error(tolua_S,"invalid arguments in function 'lua_cocos2dx_Ref_getReferenceCount'", nullptr); return 0; } unsigned int ret = cobj->getReferenceCount(); tolua_pushnumber(tolua_S,(lua_Number)ret); return 1; } luaL_error(tolua_S, "%s has wrong number of arguments: %d, was expecting %d \n", "cc.Ref:getReferenceCount",argc, 0); return 0; #if COCOS2D_DEBUG >= 1 tolua_lerror: tolua_error(tolua_S,"#ferror in function 'lua_cocos2dx_Ref_getReferenceCount'.",&tolua_err); #endif return 0; }
其他的cocos2d-x提供的Lua可调用方法不再赘述,与之类似。