Lua
Lua 与其他语言最大的不同点在于它的设计目标:不追求“大而全”,而是“小而美”。
Lua实现业务逻辑简单,可以配合在C++里使用,也可让C++配合Lua使用,C++可以实现底层和服务器一些高性能方面的应用,Lua可以实现一些复杂的业务逻辑,将两者配合使用,具体应用在不同的场景里,加快开发效率。
标准的 Lua(PUC-Rio Lua)使用解释器运行,速度虽然很快,但和 C/C++ 比起来还是有差距的。
使用LuaJIT代替标准Lua,效率还是很高的。
可以用 LuaJIT :https://luajit.org/ ,用了 JIT(Just in time)技术,能够把 Lua 代码即时编译成机器码,速度几乎可以媲美原生 C/C++ 代码。
缺点:更新较慢
建议:改用非官方的一个分支项目:OpenResty-LuaJIT
这个由 OpenResty 负责维护,非常活跃,修复了很多小错误。
git clone git@github.com:openresty/luajit2.git
make && make install
LuaBridge 是一个纯头文件的库,只要下载下来,把头文件拷贝到包含路径,就能够直接用:
git clone git@github.com:vinniefalco/LuaBridge.git
在Lua里调用C++
LuaJIT内置了一个 ffi 库(Foreign Function Interface),能够在 Lua 脚本里直接声明接口函数、直接调用,不需要任何的注册动作,更加简单方便。而且这种做法还越过了 Lua 传统的栈操作,速度也更快。
注意:只能识别纯 C 接口,不认识 C++
一定要将接口转换成C接口,内部可以用C++。
包含头文件的使用extern “C”
extern "C" { // 使用纯C语言的对外接口
int num = 10;
int my_add(int a, int b);
}
int my_add(int a, int b) // 一个简单的函数,供Lua调用
{
return a + b;
}
然后就可以用 g++ 把它编译成动态库
g++ lua_shared.cpp -std=c++11 -shared -fPIC -o liblua_shared.so
然后Lua脚本
首先要用 ffi.cdef 声明要调用的接口,
再用 ffi.load 加载动态库,
这样就会把动态库所有的接口都引进 Lua,
然后就能随便使用了:
local ffi = require "ffi" -- 加载ffi库
local ffi_load = ffi.load -- 函数别名
local ffi_cdef = ffi.cdef
ffi_cdef[[ // 声明C接口
int num;
int my_add(int a, int b);
]]
local shared = ffi_load("./liblua_shared.so") -- 加载动态库
print(shared.num) -- 调用C接口
local x = shared.my_add(1, 2) -- 调用C接口
在 ffi 的帮助下,让 Lua 调用 C 接口几乎是零工作量,但这并不能完全发挥出 Lua 的优势。
实际上,很少在Lua里调用C++,一般都是嵌入到别的语言宿主里使用。被宿主调用,然后再“回调”底层接口,利用它的“胶水语言”特性去粘合业务逻辑。
C++里调用Lua的使用方法:
方法一:
首先要调用函数 luaL_newstate(),创建出一个 Lua 虚拟机,所有的 Lua 功能都要在它上面执行。
然后,用完之后使用函数lua_close() 关闭。
可以使用RAII的方法写一个类来自动管理。
LuaBridge 没有对此封装,自己动手实现,借用课中例子:
用了智能指针 shared_ptr,在一个 lambda 表达式里创建虚拟机,顺便再打开 Lua 基本库:
1、创建Lua虚拟机
auto make_luavm = []() // lambda表达式创建虚拟机
{
std::shared_ptr<lua_State> vm( // 智能指针
luaL_newstate(), lua_close // 创建虚拟机对象,设置删除函数
);
luaL_openlibs(vm.get()); // 打开Lua基本库
return vm;
};
#define L vm.get() // 获取原始指针,宏定义方便使用
在 LuaBridge 里,一切 Lua 数据都被封装成了 LuaRef 类,完全屏蔽了 Lua 底层那难以理解的栈操作。
它可以隐式或者显式地转换成对应的数字、字符串等基本类型,如果是表,就可以用“[]”访问成员,如果是函数,也可以直接传参调用,非常直观易懂。
注意:使用 LuaBridge 访问 Lua 数据时,只能用函数 getGlobal() 看到全局变量。想在 C++ 里调用 Lua 功能,就一定不能加“local”修饰。
2、调用上述函数创建了一个 Lua 虚拟机,然后获取了 Lua 内置的 package 模块,输出里面的默认搜索路径 path 和 cpath:
auto vm = make_luavm(); // 创建Lua虚拟机
auto package = getGlobal(L, "package"); // 获取内置的package模块
string path = package["path"]; // 默认的lua脚本搜索路径
string cpath = package["cpath"]; // 默认的动态库搜索路径
或者,直接调用luaL_dostring() 和 luaL_dofile() 直接执行 Lua 代码片段或者外部的脚本文件。
注意:luaL_dofile() 每次调用都会从磁盘载入文件,所以效率较低。
如果频繁调用,存入内存。存成一个字符串,再用 luaL_dostring() 运行:
luaL_dostring(L, "print('hello lua')"); // 执行Lua代码片段
luaL_dofile(L, "./embedded.lua"); // 执行外部的脚本文件
方法二:
提前在脚本里写好一些函数,加载后在 C++ 里逐个调用,这种方式比执行整个脚本更灵活。
具体做法:
先用 luaL_dostring() 或者 luaL_dofile() 加载脚本,然后调用 getGlobal() 从全局表里获得封装的 LuaRef 对象,就可以像普通函数一样执行了。
string chunk = R"( -- Lua代码片段
function say(s) -- Lua函数1
print(s)
end
function add(a, b) -- Lua函数2
return a + b
end
)";
luaL_dostring(L, chunk.c_str()); // 执行Lua代码片段
auto f1 = getGlobal(L, "say"); // 获得Lua函数
f1("say something"); // 执行Lua函数
auto f2 = getGlobal(L, "add"); // 获得Lua函数
auto v = f2(10, 20); // 执行Lua函数
只要掌握了上面的这些基本用法,并合理地划分出 C++ 与 Lua 的职责边界,就可以搭建出“LuaJIT + LuaBridge + C++”的高性能应用,运行效率与开发效率兼得。比如说用 C++ 写底层的框架、引擎,暴露出各种调用接口作为“业务零件”,再用灵活的 Lua 脚本去组合这些“零件”,写上层的业务逻辑。