cocos C/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++下的类及方法相关。

而对于tolua++生成的绑定代码从实质上来说就是C++代码按照Lua支持的C语言API进行的一层包装,通过extern "C" 可以看出来。

一般脚本语言的运行需要宿主的存在,且要有对应的虚拟机。在cocos中,我们可以认为C/C++就是lua的宿主,而虚拟机说白了就是要提供一个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");
}

在如上的代码中我们发现:C++在获取不同文件下的方法时,是通过include引用后,然后就直接调用;而我们的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框架

前面说过,cocos引擎通过tolua++工具,将C++按照Lua C API指定格式生成绑定代码,以便于Lua通过这些绑定代码更快捷的访问C++下的类及方法相关。此处我们并不再介绍关于tolua++的语法,也不再介绍关于cocos引擎中关于tolua工具的使用。

仅仅介绍lua在cocos引擎中的封装相关及使用相关,我们先以Lua中setPositionX为例,其绑定接口为:lua_cocos2dx_Node_setPositionX 

// lua_cocos2dx_auto.cpp 第7079行
int lua_cocos2dx_Node_setPositionX(lua_State* tolua_S)
{
    int argc = 0;
    cocos2d::Node* cobj = nullptr;
    bool ok  = true;

#if COCOS2D_DEBUG >= 1
    tolua_Error tolua_err;
#endif


#if COCOS2D_DEBUG >= 1
    // 获取lua栈中数据是否为cocos指定的对象类型
    if (!tolua_isusertype(tolua_S,1,"cc.Node",0,&tolua_err)) goto tolua_lerror;
#endif
    // 将lua栈中数据转换为对应的Node指针对象
    cobj = (cocos2d::Node*)tolua_tousertype(tolua_S,1,0);

#if COCOS2D_DEBUG >= 1
    if (!cobj) {
        // 获取失败,报错提示:无效的方法对象
        tolua_error(tolua_S,"invalid 'cobj' in function 'lua_cocos2dx_Node_setPositionX'", nullptr);
        return 0;
    }
#endif
    // 获取栈顶索引,即参数个数,之所以减1,是因为对象类型也在栈中
    argc = lua_gettop(tolua_S)-1;
    if (argc == 1) {
        double arg0;
        // 获取参数值
        ok &= luaval_to_number(tolua_S, 2,&arg0, "cc.Node:setPositionX");
        if(!ok) {
            // 获取失败,报错提示:无效的参数
            tolua_error(tolua_S,"invalid arguments in function 'lua_cocos2dx_Node_setPositionX'", nullptr);
            return 0;
        }
        // 调用Node中的setPositionX方法
        cobj->setPositionX(arg0);
        // 设置lua的虚拟栈
        lua_settop(tolua_S, 1);
        return 1;
    }
    // 获取参数个数失败,报错提示:错误的参数个数
    luaL_error(tolua_S, "%s has wrong number of arguments: %d, was expecting %d \n", "cc.Node:setPositionX",argc, 1);
    return 0;
}

上面的代码虽然看似使用了很多tolua的接口,但其实质上来说,无非还是这么几步:

1. 从lua栈中获取C++对象类型,并转换

2. 从lua栈中获取C++参数,校验后然后转换

3. 调用对象的类方法接口

libluacocos2d:

接下来我们看下Lua在cocos引擎封装相关,它主要被放置在cocos引擎的libluacocos2d

auto: 使用tolua++工具自动生成的C++代码相关

manual:放置了cocos扩展的一些功能,比如事件,加密解密,节点,Lua封装以及Lua调用Java和Object-C的接口等

luajit:  高效版的lua库,额外添加了lua没有的cocos库,并在对浮点计算,循环等进行了优化

luasocket: 网络库相关

tolua: tolua++库相关

xxtea: 加密相关

Lua环境初始化

cocos2d-x对Lua相关API的封装主要通过LuaEngineLuaStack进行了管理,其中LuaStack用于对Lua_State进行了封装,而LuaEngine则是一个管理LuaStack的单例。在我们游戏启动前,我们可以看下其流程:

bool AppDelegate::applicationDidFinishLaunching()
{// 初始化LuaEngine,用于构建Lua环境并注册cocos相关的API到lua环境中
    auto engine = LuaEngine::getInstance();
    ...
}

在调用LuaEngine::getInstance()的过程中,代码会最后进入LuaStack::init中,我们看下代码:

bool LuaStack::init(void)
{
    // 初始化Lua环境并打开标准库
    _state = lua_open();        // #define lua_open()    luaL_newstate()
    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);
    ...
    return true;
}

我们再看下register_all_cocos2dx接口

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_Node(tolua_S);

    // ...
tolua_endmodule(tolua_S);
}

而lua_register_cocos2dx_Node则封装了Node类对应的方法相关

int lua_register_cocos2dx_Node(lua_State* tolua_S)
{
    tolua_usertype(tolua_S,"cc.Node");
    tolua_cclass(tolua_S,"Node","cc.Node","cc.Ref",nullptr);

    tolua_beginmodule(tolua_S,"Node");
        tolua_function(tolua_S,"setPositionX",lua_cocos2dx_Node_setPositionX);
        ...
    tolua_endmodule(tolua_S);
    std::string typeName = typeid(cocos2d::Node).name();
    g_luaType[typeName] = "cc.Node";
    g_typeCast["Node"] = "cc.Node";
    return 1;
}

到此处就首尾连接上了。

本人能力有限,以后会慢慢补充!!! 

猜你喜欢

转载自www.cnblogs.com/SkyflyBird/p/11938373.html