zoukankan      html  css  js  c++  java
  • 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引擎主要通过LuaEngineLuaStack对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 
    ", "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可调用方法不再赘述,与之类似。

  • 相关阅读:
    openstack命令行
    Hub, bridge, switch, router, gateway的区别
    openstack奠基篇:devstack (liberty)于centos 7安装
    git常用命令备忘
    如何利用gatling创建一个性能测试例
    如何通过源码生成Gatling可执行工具
    博客园的第一篇博文
    Spring Oauth2 with JWT Sample
    你首先是一个人,然后你才是程序员。
    Maven——快速入门手册(学习记录) ****
  • 原文地址:https://www.cnblogs.com/SkyflyBird/p/11938373.html
Copyright © 2011-2022 走看看