Lua 是一个小巧的脚本语言,它本身就是作为嵌入脚本而设计的,在目前所有脚本引擎中,Lua的速度是最快的。而且它的解释器非常轻量,其解释器不过200k(不同版本可能略有差异)。
Lua项目包含许多技术点,花些时间研究可以有不少收获,学到很多东西。包括与宿主语言的交互、内存管理、虚拟机实现、协程、闭包、异常捕获机制等等,后续有时间慢慢研究下。
如题所示,本系列主要记录Lua封装相关笔记,主要是记录C++11的相关学习与实践。Lua相关原理并不懂,在笔记中也暂不提,后续有时间深入学习Lua时,再做相关笔记。
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++时,在引入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函数。
// 这是原来的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_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]