Lua封装&C++实践——Lua和C/C++的基本交互

Posted 湖广午王

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Lua封装&C++实践——Lua和C/C++的基本交互相关的知识,希望对你有一定的参考价值。

Lua 是一个小巧的脚本语言,它本身就是作为嵌入脚本而设计的,在目前所有脚本引擎中,Lua的速度是最快的。而且它的解释器非常轻量,其解释器不过200k(不同版本可能略有差异)。

Lua项目包含许多技术点,花些时间研究可以有不少收获,学到很多东西。包括与宿主语言的交互、内存管理、虚拟机实现、协程、闭包、异常捕获机制等等,后续有时间慢慢研究下。

如题所示,本系列主要记录Lua封装相关笔记,主要是记录C++11的相关学习与实践。Lua相关原理并不懂,在笔记中也暂不提,后续有时间深入学习Lua时,再做相关笔记。

Lua与C/C++交互基础

Lua和C/C++语言通信的主要方法是通过Lua先进后出(FILO)的虚拟栈。在Lua中,Lua堆栈就是一个struct,堆栈索引的方式可是是正数也可以是负数,区别是:正数索引1永远表示栈底,负数索引-1永远表示栈顶。

Lua的使用,依赖于Lua的状态机lua_State,Lua的堆栈也存在于状态机中。这里的状态机,类似与Java的jvm,是解析、执行lua的基石。无论是Lua调用C/C++,还是C/C++调用Lua,都是A把数据压栈、B把数据从栈取出来,这里的数据可能是元数据也可能是一个地址。

在做Lua与C/C++交互时,方法、参数、返回值都需要压入堆栈。在后面会用例子来说明。

C/C++调用Lua

C/C++调用Lua相对来说比较简单,需要注意的是,当使用C++时,在引入Lua头文件时候,需要使用#include "lua.hpp",在lua.hpp内容如下,使用了extern c来告知编译器,以C Linkage方式编译,也就是抑制C++的name mangling机制。否则会编译出错。

extern "C" 
#include "lua.h"
#include "lualib.h"
#include "lauxlib.h"

然后我们需要有一个Lua文件:

function helloAdd(num1, num2)
    return (num1 + num2)
end

在C/C++中调用Lua的步骤如下:

void testCCallLua()
    int ret;
    //第一步:创建一个Lua的状态机
    lua_State * l = luaL_newstate();
    //第二步:打开需要使用的库。虽然例子没用到,这个还是全部打开。还有另外的方法打开指定的库luaopen_xxx(l)
    luaL_openlibs(l);
    //第三步:加载(执行)指定的lua文件,lua没有main函数。函数、table、全局变量什么的都到栈中了,函数外面的程序直接就执行了
    ret = luaL_dofile(l, "../res/test1.lua");
    std::cout<<"doFile : "<< ret<< std::endl;
    //第四步:获取指定的lua函数,这一步会把第二个参数压入堆栈中
    ret = lua_getglobal(l, "helloAdd");
    std::cout<<"getFunction : "<< ret<< std::endl;
    //第五步:把函数需要的参数压入到栈中
    lua_pushnumber(l, 10);
    lua_pushnumber(l, 5);
    //第六步:调用函数前面压入到栈中的函数,第二个参数为被调函数的参数个数,第三个参数为返回个数,因为lua是支持多个返回值的
    lua_call(l, 2, 1);
    //第六步:调用函数后,结果被放在栈顶,按照类型去取结果。多个返回值的时候,需要注意索引值,取完结果后弹出数据。结果也可以每取一个调用lua_pop弹出被取出的结果
    double iResult = lua_tonumber(l, -1);
    std::cout<<"result:" << iResult << std::endl;
    lua_pop(l,1);
    //最后一步: 使用完后,要注意关闭释放状态机
    lua_close(l);
    l = nullptr;

Lua调用C

Lua调用C可以通过注册库、然后在lua中加载库的方式来调用,也可以通过直接函数压栈的方式来调用。在这里记录的为函数压栈的方式。

这里,我们先准备好要被调用的C函数。

// 这是原来的C函数
double cFuncAdd(double a, double b)
    return a+b;


// C函数被调用,需要符合Lua的规则,所以需要做一个转调
// 返回值表示函数返回值的个数
int luaBindCFuncAdd(lua_State * l)
    double a = luaL_checknumber(l, 1);
    double b = luaL_checknumber(l, 2);
    lua_pushnumber(l, cFuncAdd(a, b));
    return 1;

C函数被调用,需要符合lua的规则。前面提到过,C和Lua的互调都是通过栈来完成的,Lua解释器在解释Lua中调用的函数时,也是通过函数名、函数参数等去和堆栈做交互的。所以在上面luaBindCFuncAdd中,实际执行也是从堆栈中依次取出两个参数,然后调用C函数执行,并把结果压入栈中,然后lua从栈中得到结果。

Lua只是嵌入脚本,它的执行依赖宿主程序,所以我们还是需要写C代码来执行lua,并且上面我们只是准备好了C函数,但是这个C函数并没有通过Lua状态机和lua建立联系。

void testLuaCallC()
    std::cout << "start test Lua Call C --------------------- " << std::endl;
    //前面步骤一样,创建状态机,打开lua库
    lua_State * l = luaL_newstate();
    luaL_openlibs(l);
    //把C函数压入栈中,第二个参数只接受输入参数是lua_State,输出位int值的函数。
    lua_pushcfunction(l,luaBindCFuncAdd);
    //对应C调用lua方法的那个getglobal,这里是把栈顶的函数取个名字
    lua_setglobal(l,"cFuncAdd");
    //然后执行lua程序,这里直接写了一串lua代码,输出函数执行结果
    int ret = luaL_dostring(l,"print('cFuncAdd ret :', cFuncAdd(98,9))");
    std::cout<<"doFile:" << ret << std::endl;
    lua_close(l);

C和Lua的相互调用这样其实就比较好理解了,把C的函数放入堆栈中被Lua调用,还是lua中的函数被lua调用,至少从表现上来说,并没有区别,可能都是执行前把方法压入了堆栈,执行时,根据名字,找到对于的函数放到栈顶,然后压入参数,执行函数,再从栈顶得到返回的结果。

Lua调用C++

这个就相对麻烦点了,上面的Lua调用C的时候,我们知道lua_pushcfunction只能传入固定格式的C函数。要让Lua可以调用C++,而且按照我们在C++中使用类的方式来进行调用,我们就需要对C++代码做更多的处理去满足lua的要求了。实际上,巧用Lua的userdata可以满足我们的各种需求。

在这里的示例中,实际上是通过userdata+metatable来实现C++到Lua的映射,把C++类映射为Lua中的table。
其中metatable在Lua中,允许我们改变table的行为。在Lua中,每个行为关联了对应的元方法。常见的元方法有:

__index //通过table获取table的属性及方法时会调用此方法
__gc	//table被回收时,会调用此方法
__newindex //对表更新,和__index类似,__index是访问旧值,对不存在的索引增加值时会调用此方法
//以下这些都对应着表的运算符,可以看做是用于运算符的重载。
__add、__sub、__mul、__div、__mod、__unm、__concat、__eq、__lt、__le
__call //在 Lua 调用一个值时调用
__tostring //用于修改表的输出行为,相当于java中的tostring方法重载

给一个table设置元表(metatable)后,在对table执行某个操作时,就会按照元表的定义来执行。比如,一个table设置了元表,元表中实现了__index元方法,则table.xxx和table:xxx都会先执行__index方法,通过__index决定应该做什么。

我们在后面就是用Lua的这个特性,来实现对C++的调用。

首先,写下我们最终期望的Lua代码:

-- 我们创建了一个类,然后去使用它的方法,并且也能访问它的属性
operate = OperateCpp()
print("OperateCpp:multiply ret : ", operate:multiply(5.0,12.0))
print("OperateCpp.errorCode is :", operate.errorCode)

准备C++的类,和转调用的C函数:


class OperateCpp
private:
    double x;
    double y;
    int type;
public:

    int errorCode = -1;

    OperateCpp() = default;
    ~OperateCpp()
        std::cout<<"OperateCpp destroy"<<std::endl;
    

    double multiply(double x, double y)
        return x * y;
    
;

//构造函数的转调函数
static int LuaCreateOperateCpp(lua_State * l)
	//Lua状态机是不知道C++类这个东西的,但是Lua中有userdata来支持扩展
	//所以,在这里,我们先指定大小,获取一块内存,并放进堆栈中,作为一个userdata。这块内存,宿主程序是可以直接使用的
    auto ** pData = (OperateCpp**)lua_newuserdata(l, sizeof(OperateCpp*));
    //我们把userdata中的内容,赋值为一个C++类实例的指针
    *pData = new OperateCpp();
    //然后我们去获取一个名为OperateCpp元表,然后放到栈顶。这个名字可以随意,但是要确保这个元表是存在的。
    //在关联的时候,我们会去创建一个这样的元表,确保在这里可以获取到。
    luaL_getmetatable(l, "OperateCpp");
    //把元表和指定位置的userdata关联起来,这里是-2,就是上面new出来的,-1被上面指定的元表占据了
    lua_setmetatable(l, -2);
    return 1;


//销毁对象
static int LuaDestroyOperateCpp(lua_State* L)
    // 释放对象
    delete *(OperateCpp**)lua_topointer(L, 1);
    return 0;


//函数的转调
static int LuaFuncMultiply(lua_State * l)
    auto * oc = *(OperateCpp **)lua_topointer(l, 1);
    auto x = lua_tonumber(l,2);
    auto y = lua_tonumber(l,3);
    auto ret = oc->multiply(x,y);
    lua_pushnumber(l,ret);
    return 1;


//注意这个函数在后面的用途,这个会被指定给元表的__index方法
static int LuaCallIndex(lua_State * l)
    auto * oc = *(OperateCpp **)lua_topointer(l, 1);
    auto filed = lua_tostring(l,2);
    if(strcmp(filed,"errorCode") == 0)
        lua_pushnumber(l, oc->errorCode);
    else if(strcmp(filed, "multiply") == 0)
        lua_pushcfunction(l, LuaFuncMultiply);
    
    return 1;


然后我们需要建立C++和Lua的联系,解释也直接在代码注释中给出:


void testLuaCallCpp()
    std::cout << "start test Lua Call Cpp --------------------- " << std::endl;
    lua_State * l = luaL_newstate();
    luaL_openlibs(l);
    //和前面一样,我们用OperateCpp来命名OperateCpp对象的构造函数的转调函数,命名位OperateCpp,在Lua中调用就是OperateCpp()了
    //创建函数被调用时,每次实际就是创建了一个table,然后关联一个元表
    lua_pushcfunction(l,LuaCreateOperateCpp);
    lua_setglobal(l,"OperateCpp");
    //创建元表,提供给每次创建对象时候用,这个名字可以随意,只要在创建函数中取元表的时候要和这个对应
    //这里的元表可以看到和上面的构建方法是一样的,这个无所谓,一样不一样都行
    luaL_newmetatable(l, "OperateCpp");
    //指定元表的__gc方法,关联这个元表的table,在被释放时就会调用指定的方法
    lua_pushstring(l,"__gc");
    lua_pushcfunction(l,LuaDestroyOperateCpp);
    //这里相当于把堆栈的指针给移回到metatable上,以便于继续设置其他元方法
    lua_settable(l, -3);
    //设置元表__index的元方法,关联这个元表的table,每次获取内部属性或方法时就会调用指定的方法
    lua_pushstring(l, "__index");
    lua_pushcfunction(l,LuaCallIndex);
    lua_settable(l,-3);
	
    std::string content = loadString("../res/test2.lua");
    int ret = luaL_dostring(l,content.c_str());
    std::cout<<"doFile:" << ret << std::endl;
    lua_close(l);


至此,Lua和C/C++基本的相互调用就完成了,但是这样使用起来难免会感觉比较麻烦,我只是想做个简单的交互就要做这么多事情,无意是痛苦的,所以后面我们就要开始对这写复杂的调用做封装,让Lua和C++的交互变的更简单一些。

其他

笔记相关的代码在Github上,代码会不断变动,有需要的可以直接看对应的提交。此博客仅作为个人学习笔记及有兴趣的朋友参考使用,虚心接受建议与指正,不接受吐槽和批评,引用设计思想或代码希望注明出处,欢迎Fork和Star。wLuaBind代码地址


欢迎转载,转载请保留文章出处。湖广午王的博客[http://blog.csdn.net/junzia/article/details/95001209]


以上是关于Lua封装&C++实践——Lua和C/C++的基本交互的主要内容,如果未能解决你的问题,请参考以下文章

Lua封装&C++实践——Lua和C/C++的基本交互

Lua封装&C++实践—— C++调用Lua函数的封装

Lua封装&C++实践—— C++调用Lua函数的封装

Lua封装&C++实践—— C++调用Lua函数的封装

Lua封装&C++实践——Lua注册C++构造函数

Lua封装&C++实践——Lua注册C++构造函数