Cocos2dx 3.0下的C/C++和Lua通讯以及Object C与Lua通讯

网上关于Cocos2dx开发过程中Lua的使用以及原理教程已经很多了,结合我的开发经验,我在这里稍微整理下。

可以说Cocos2dx-Lua提供了一种很轻便的开发模式,省去了冗长的编译时间,同时让热更成为了很容易的一件事情,不仅仅是在Android上,iOS上也轻易绕开了官方的审查,毕竟Lua在iOS系统看来都只是资源,就像txt文档。

然而Cocos2d-x是用C++开发的,那么,Lua究竟是怎么和C++通信的呢?在发布iOS平台的时候Lua有时候又需要和Object C交互,又应该怎么处理呢?

C/C++和Lua的通讯是通过维护一个Lua堆栈和Lua全局表进行的。Lua堆栈也是满足堆栈先进后出的特性,同时也支持使用索引进行访问,索引方式可以是正数也可以是负数,索引正数1表示栈底,索引负数-1表示栈顶。Lua启动或调用C语言时,Lua堆栈至少会有20个空闲槽位。Lua堆栈不是一个全局性的结构,每个函数都有自己的局部私有堆栈,当Lua调用C/C++函数的时候第一个参数总是这个局部栈的索引1。当C/C++函数把返回值压入Lua堆栈之后,该栈会被自动清空。

Lua堆栈
3               栈顶               -1
2                                     -2
1               栈底               -3

Lua全局表是一个类似于Map哈希表的结构,可以通过Key读取到对应的Value,比如Lua中定义有一个变量:

id = 10086

这就相当于在Lua全局表中存放了一个Key为'id',Value为‘10086’的映射关系,之后我们可以通过Key在Lua全局表中查到对应的Value‘10086’。

那么C\C++和Lua拥有不同的数据类型,要实现两者之间的数据通信该怎么办?Lua虚拟机提供Lua_State这样一种数据结构。任何一种数据从C\C++传入Lua虚拟机中,Lua都会将这类数据转换为一种通用的结构lua_TValue,并且将数据复制一份,将其压入Lua堆栈中。在Lua中,number、boolean、nil、light userdata四种类型的值是直接存在栈上元素里的,和Lua的垃圾回收无关,string、table、closure、userdata、hread存在栈上元素里的只是指针,他们都会在生命周期结束后被Lua垃圾回收。Lua有自己的GC,C\C++由自己申请和释放内存,所以两者之间的内存管理是独立的。从C\C++中传递数据到Lua虚拟机会发生数据拷贝,从Lua虚拟机中传递出来是直接从虚拟栈中取值或者地址,所以数据从虚拟栈中pop之后,是否依然是有效引用需要额外注意。

了解了上面的基础知识之后,接下来了解下Lua提供了哪些C API来对Lua堆栈进行操作,以实现C\C++和Lua的通讯。首先了解下操作Lua堆栈的基本步骤,主要有一下几步:

  1. 创建Lua实例:luaL_newstate();
  2. 加载Lua文件:luaL_loadfile(L,"hello.lua");
  3. 运行Lua文件:lua_pcall(L,参数个数,返回值个数,错误处理函数索引);
  4. 操作Lua堆栈:(参考Lua 5.1 参考手册)
lua_getglobal(L,“name”) 把全局变量name压入堆栈
lua_setglobal(L,“name”) 从堆栈上弹出一个值并将其设置到全局变量name中
lua_getfield(L,index,“key”) 把t[key]的值压入栈,t是有效索引 index 指向的值
lua_gettable(L,index) 把value出栈并把t[value]值压入栈,t是有效索引 index 指向的值,value是栈顶存放的值
lua_tonumber(L,index) 把有效索引 index 指向的栈值转换成C类型的lua_Number值
lua_tostring(L,index) 把有效索引 index 指向的栈值转换成C字符串
lua_pushnumber(L,num) 把数字num压栈
lua_pushstring(L,str) 把指针str指向的以零结尾的字符串做一次内存拷贝压栈,因此函数返回后可释放或重用这些字符串
lua_pushvalue(L,index) 把堆栈上给定有效索引index 处的元素做一个拷贝压栈
lua_insert(L,index) 把栈顶元素插入到指定有效索引 index 处,并依次上移这个索引之上的元素
lua_replace(L,index) 把栈顶元素移动到给定的有效索引 index 的位置,并把栈顶值弹出,不移动任何元素
lua_remove(L,index) 从给定有效索引 index 处移除一个元素,把这个元素之上的所有元素下移填补这个空隙

      5. 销毁Lua实例:lua_close(L);

接下来看下代码:以下是一个hello.lua文件

str = "I am so cool"  
tbl = {name = "shun", id = 20114442}  
function add(a,b)  
    return a + b  
end

以下是CallLua.cpp的主要代码:

void main()  
{  
    //1.创建Lua状态  
    lua_State *L = luaL_newstate();  
    if (L == NULL)  
    {  
        return ;  
    }  
   
    //2.加载Lua文件  
    int bRet = luaL_loadfile(L,"hello.lua");  
    if(bRet)  
    {  
        cout<<"load file error"<

接下来看Lua怎么调用C/C++。Lua调用C/C++有三种实现方法,下面依次介绍怎么调用。

方法一:注册到Lua模块中,可以将函数写lua.c中,然后重新编译Lua文件。注册到Lua模块主要有几个步骤:

  1. 定义函数:满足注册到Lua模块函数的原型;
  2. 注册函数:lua_pushcfunction(L,C函数名);
  3. 出栈函数:lua_setglobal(L,“Lua函数名”);

所有注册到Lua的函数都具有相同的原型,该原型定义在lua.h中的lua_CFunction:

typedef int (*lua_CFunction) (lua_State *L);

该函数只有一个参数:Lua状态,返回值是一个整数,表示压入栈中返回值个数。这个函数无需在压入结果前清空栈,在它返回之后Lua会自动删除Lua栈结果之下的内容。

// This is my function  
static int getTwoVar(lua_State *L)  
{  
    // 向函数栈中压入2个值  
    lua_pushnumber(L, 10);  
    lua_pushstring(L,"hello");  
   
    return 2;  
}  
 
// 在pmain函数中,luaL_openlibs函数后加入以下代码:
// 注册函数  
lua_pushcfunction(L, getTwoVar); //将函数放入栈中  
lua_setglobal(L, "getTwoVar");   //设置lua全局变量getTwoVar
// 上面的注册函数可以这样子写:
// lua_register(L,"getTwoVar",getTwoVar);

方法二:使用静态依赖的方式,大概步骤是:在C/C++中定义一个函数,将函数注册到Lua模块中,然后由C/C++去执行我们的Lua文件,然后在Lua中调用刚刚注册的函数。代码如下:

新建avg.lua文件:

avg, sum = average(10, 20, 30, 40, 50)  
print("The average is ", avg)  
print("The sum is ", sum)

然后再C/C++中调用Lua文件并执行: 

#include   
extern "C" {  
#include "lua.h"  
#include "lualib.h"  
#include "lauxlib.h"  
}  
   
/* 指向Lua解释器的指针 */  
lua_State* L;  
static int average(lua_State *L)  
{  
    /* 得到参数个数 */  
    int n = lua_gettop(L);  
    double sum = 0;  
    int i;  
   
    /* 循环求参数之和 */  
    for (i = 1; i <= n; i++)  
    {  
        /* 求和 */  
        sum += lua_tonumber(L, i);  
    }  
    /* 压入平均值 */  
    lua_pushnumber(L, sum / n);  
    /* 压入和 */  
    lua_pushnumber(L, sum);  
    /* 返回返回值的个数 */  
    return 2;  
}  
   
int main ( int argc, char *argv[] )  
{  
    /* 初始化Lua */  
    L = lua_open();  
   
    /* 载入Lua基本库 */  
    luaL_openlibs(L);  
    /* 注册函数 */  
    lua_register(L, "average", average);  
    /* 运行脚本 */  
    luaL_dofile(L, "avg.lua");  
    /* 清除Lua */  
    lua_close(L);  
   
    /* 暂停 */  
    printf( "Press enter to exit…" );  
    getchar();  
    return 0;  
}

方法三:使用动态链接的方式,步骤主要有:创建一个新的工程,命名为mLualib,然后将编写需要注册到Lua中的代码,编译生成动态链接库,将其导出到项目工程中,最后在Lua代码中加载这个动态链接库即可。代码如下:

在新建项目工程中编写需要注册到Lua中的代码,.h头文件:

#pragma once  
extern "C" {  
#include "lua.h"  
#include "lualib.h"  
#include "lauxlib.h"  
}  
   
#ifdef LUA_EXPORTS  
#define LUA_API __declspec(dllexport)  
#else  
#define LUA_API __declspec(dllimport)  
#endif  
   
extern "C" LUA_API int luaopen_mLualib(lua_State *L);//定义导出函数

.cpp文件:

#include   
#include "mLualib.h"  
static int averageFunc(lua_State *L)  
{  
    int n = lua_gettop(L);  
    double sum = 0;  
    int i;  
   
    /* 循环求参数之和 */  
    for (i = 1; i <= n; i++)  
        sum += lua_tonumber(L, i);  
   
    lua_pushnumber(L, sum / n);     //压入平均值  
    lua_pushnumber(L, sum);         //压入和  
   
    return 2;                       //返回两个结果  
}  
   
static int sayHelloFunc(lua_State* L)  
{  
    printf("hello world!");  
    return 0;  
}  
   
static const struct luaL_Reg myLib[] =   
{  
    {"average", averageFunc},  
    {"sayHello", sayHelloFunc},  
    {NULL, NULL}       //数组中最后一对必须是{NULL, NULL},用来表示结束      
};  
   
int luaopen_mLualib(lua_State *L)  
{  
    // luaL_register会根据给定名称“ss”创建(或复用)一个table,并用myLib中的信息填充这个table,
    // luaL_register函数返回时,会将这个table留在Lua堆栈中
    luaL_register(L, "ss", myLib);  
    return 1;       // 把myLib表压入了栈中,所以就需要返回1  
}

最后在项目工程的Lua代码中加载这个动态链接库:

require "mLualib"  
local ave,sum = ss.average(1,2,3,4,5)//参数对应堆栈中的数据  
print(ave,sum)  -- 3 15  
ss.sayHello()   -- hello world!

至此,关于C/C++是如何和Lua的通讯就基本结束了。那么在Cocos2dx开发的过程中,我们其实并不需要自己去手动编写代码把C/C++接口暴露给到Lua,Cocos2dx引擎的开发者已经将大部分的C/C++接口暴露给到了Lua这边,他们在工程中集成了工具tolua++,通过这个工具只需要做一些简单的配置,就可以把C/C++函数暴露给Lua使用。所以,当我们要需求需要把自己编写的接口暴露给Lua调用的时候,也可以模仿这种方式,直接配置一些导出即可。具体关于tolua++工具的环境搭建和参数配置,可以参考这篇文章。

使用tolua导出自定义类的主要步骤有以下几点:

  1. 首先按照 cocos2d-x-3.0rc0\tools\tolua\README.mdown说的步骤安装必要的库和工具包,配置好相关环境变量,搭建好环境;
  2. 编写需要导出自定义类的C/C++文件,注意:该类可继承Cocos2d::Ref类,以便使用Cocos2dx的内存回收机制;
  3. 编写.ini配置文件:在frameworks/cocos2d-x/tools/tolua/目录下可以看到genbindings.py脚本和一大堆.ini文件,复制一个并重命名为新的配置文件,同时修改配置文件中的一些关键配置:

           [配置文件名]
           prefix = 配置文件名
           target_namespace =
           headers = %(cocosdir)s/../自定义类头文件目录
           classes = 导出类名 

      4.编写生成脚本:同样在frameworks/cocos2d-x/tools/tolua/目录下,可以看到genbindings.py文件,这个就是生成脚本了,复制并重命名一个文件,并修改里面的一些配置:

          output_dir = '%s/cocos/scripting/lua-bindings/auto' % project_root(修改成导出目录)

          cmd_args = {'配置文件名.ini' : ('配置文件名', '导出文件名') } 

      5.编译生成脚本:python 生成脚本.py。

这个时候在导出目录下,你就可以看到两个以导出文件名命名的.hpp文件和.cpp文件,把这两个文件导入到项目工程中。最后把这个自定义类注册到Lua环境中,打开AppDelegate.cpp文件,包含导出文件.hpp头文件,接着找到:

LuaEngine* engine = LuaEngine::getInstance();

在它后面写注册函数,打开导出文件.cpp文件,找到注册函数定义,注册函数名为:register_all_导出类名(lua_State* tolua_s)。编译工程重新生成链接库,这样就可以在Lua中使用自定义类了。

这里提一下在Cocos2dx开发过程中遇到的一个问题,就是将Lua回调函数传递给到C/C++。通过工具tolua++是无法正确生成可以给Lua调用的接口的,需要手动编写绑定方法,在tolua++生成绑定的cpp文件中作出 一点修改就好了:
注释掉的那句是本来生成的代码,改用tolua_fix中的 toluafix_ref_function() 方法即可, 第二个参数用注释掉的那个函数调用的第二个参数。

LUA_FUNCTION arg0;
//ok &= luaval_to_int32(tolua_S, 2,(int *)&arg0, "TcpHelper:test");
arg0 = toluafix_ref_function(tolua_S, 2, 0);

下面是C++中的代码:

void test(LUA_FUNCTION listener){
        LuaStack *stack = LuaEngine::getInstance()->getLuaStack();
        stack->clean();
        LuaValueDict dict;

        dict["name"] = LuaValue::stringValue("success");
        stack->pushLuaValueDict(dict);
        stack->executeFunctionByHandler(listener, 1);

    }

Lua中的调用:

    local entity = TestClass:new()
    entity:test(function(arg)
        print("!!!!")
        dump(arg)
        end)

最后说下Object C和Lua之间的通讯。Cocos2dx提供了一个luaoc.lua类给我们使用,具体的如何使用可以参考这篇文章。这种方法是引擎提供的,但是如果要一些情况这个类无法满足的时候,其实还有另外一种思路可以提供参考。Object C要和Lua进行通讯需要一个中间人,这个就是C/C++。C/C++可以和Object C混编,要实现C/C++和Objcet C混编需要将.m文件的后缀名改为.mm后缀。

 

参考:

Lua和C/C+的通讯:

https://www.cnblogs.com/sevenyuan/p/4511808.html

https://blog.csdn.net/shun_fzll/article/details/39120965

https://segmentfault.com/a/1190000000631630

https://blog.csdn.net/beautyleaf/article/details/51759742

Lua回调函数传递给到C/C++:

https://www.cnblogs.com/cnxkey/articles/7789231.html

http://www.cnblogs.com/boliu/p/4091274.html

你可能感兴趣的:(游戏开发,Cocos2dx,Lua,Object,C,C,C++)