lua c++ 互操作
因为一些原因,需要一个脚本系统,用来进行快速辅助开发。我本来的选择有三个,JavaScript、python、lua。JavaScript因为异步运行,在编写配置型逻辑时非常混乱,所以放弃。python和lua中,我选择了更为小巧的lua。不过大家开发中,如果使用java作为宿主语言,自带js引擎(javax.script.engine)。如果使用C++且已经配置好了boost,则推荐python(Boost.Python)。
该项目的源代码。
1. 实现简单的lua解释器
我把lua5.1.5的所有代码都编到项目中了,把全部源码编进来是为了没有外部依赖,同时提升可移植性。
顺便吐个槽,lua源码虽然不大,但是单论文件数量,比Boost.Python的源文件的个数反而要多不少。
我在这里使用lua5.1而不用更高版本是为了给以后项目迁移到luagit留条后路。如果不考虑luagit的话建议使用lua5.3,拥有更高的运行和内存效率,原生的64位支持对于移动平台绝对是天大利好。
#include <iostream>
#include <fstream>
#include <string>
using namespace std;
extern "C"
{
#include <lua.h>
#include <lauxlib.h>
#include <lualib.h>
};
void MyLuaInterpreter();
int main()
{
MyLuaInterpreter();
return 0;
}
void MyLuaInterpreter()
{
lua_State *L = luaL_newstate();
// luaopen_base(L);
// luaopen_table(L);
// luaopen_string(L);
// 在lua5.1中不需要luaopen_*系列函数
luaL_openlibs(L); // 使用luaopen打开lua标准库
string str;
while (true)
{
cout << "请输入 lua 代码,输入 quit() 退出 lua 解释器" << endl;
cout << "> ";
getline(cin, str, '\n');
if (str == "quit()") break;
if (luaL_loadstring(L, str.c_str()) || lua_pcall(L, 0, 0, 0))
{
const char * msg = lua_tostring(L, -1);
cout << string(msg) << endl;
}
}
lua_close(L);
}
也可用
lua_open()
代替luaL_newstate()
lua的官方解释器代码为src/lua.c
luaL_loadstring()
函数读取代码并检查错误。lua_pcall()
函数运行代码,并将所有全局变量保存在_G
中。
2. 从文件中读取代码并运行
修改解释器中的循环如下:
while (true)
{
cout << "请输入 lua 代码路径,输入 quit() 退出 lua 解释器" << endl;
cout << "> ";
getline(cin, str, '\n');
if (str == "quit()") break;
if (luaL_loadfile(L, str.c_str()) || lua_pcall(L, 0, 0, 0))
{
const char * msg = lua_tostring(L, -1);
cout << string(msg) << endl;
}
}
luaL_loadfile()
函数和luaL_loadstring()
功能相同,不过是从文件中读取。
3. 从lua中读取数据
本例用来测试的lua脚本如下:
a = 234
b = 432
strc = "hello message from test2.lua"
table = {
tabA = 123,
tabStr = "msg in table"
}
function add(a, b)
return a + b
end
同时将MyLuaInterpreter()
修改为如下:
void MyLuaInterpreter()
{
lua_State *L = luaL_newstate();
luaL_openlibs(L); // 使用luaopen打开lua标准库
string str;
while (true) {
cout << "请输入 lua 代码路径" << endl;
cout << "> ";
getline(cin, str, '\n');
if (luaL_loadfile(L, str.c_str()) || lua_pcall(L, 0, 0, 0)) {
const char *msg = lua_tostring(L, -1);
cout << string(msg) << endl;
} else {
break;
}
}
double a, b;
// 1. 获取a的值
lua_getglobal(L, "a");
a = lua_tonumber(L, -1);
// 2. 获取b的值
lua_getglobal(L, "b");
b = lua_tonumber(L, -1);
cout << "a = " << a << " b = " << b << endl;
cout << "a + b = " << a + b << endl;
// 3. 读取字符串
lua_getglobal(L, "strc");
string strc = lua_tostring(L, -1);
cout << "strc: " << strc << endl;
// 4. 读取table
lua_getglobal(L, "table");
lua_getfield(L, -1, "tabA");
double tabA = lua_tonumber(L, -1);
cout << "table.tabA = " << tabA << endl;
lua_getfield(L, -2, "tabStr");
string tabStr = lua_tostring(L, -1);
cout << "table.tabStr = " << tabStr << endl;
// 5. 读取函数
lua_getglobal(L, "add"); // 获取函数
lua_pushnumber(L, 11); // 压入第一个参数
lua_pushnumber(L, 22); // 压入第二个参数
int iRet = lua_pcall(L, 2, 1, 0); // 调用函数,调用完成以后,会将返回值压入栈中,2表示参数个数,1表示返回结果个数。
if(iRet) // 错误处理
{
const char *pErrorMsg = lua_tostring(L, -1);
cout << pErrorMsg << endl;
lua_close(L);
return ;
}
if (lua_isnumber(L, -1))
{
double res = lua_tonumber(L, -1);
cout << "Func add result is : " << res << endl;
}
lua_close(L);
}
3.1 读取变量
lua一共有5种变量类型,分别是nil,Boolean,string,Number和table。Number和c/c++中的double类型相对应。
首先使用lua_getglobal()
函数将想要获得的lua值放入栈顶,之后使用lua_tonumber(L,-1)
函数将栈顶的数字传给c++变量,使用lua_tostring(L, -1)
将栈顶的字符串传给c++变量。如果不放心,可以在取值前先使用lua_isnumber(L, –1)
和lua_isstring(L, –1)
来检查栈顶的值是否是数字或者字符串。
lua.h中定义了一系列lua\_to*()
函数用来取值,定义了一系列lua_is*()
函数用来检测栈中的值是否是对应的类型。
3.2 读取table中的值
我读取的table是:
table = {
tabA = 123,
tabStr = "msg in table"
}
读取的c++代码是
// 4. 读取table
lua_getglobal(L, "table");
lua_getfield(L, -1, "tabA");
double tabA = lua_tonumber(L, -1);
cout << "table.tabA = " << tabA << endl;
lua_getfield(L, -2, "tabStr");
string tabStr = lua_tostring(L, -1);
cout << "table.tabStr = " << tabStr << endl;
使用lua_getglobal(L, "table")
函数把table置于栈顶后,内部的各个变量从栈顶往下以此排列,即tabA
位于栈顶(即-1),而tabStr
位于栈倒数第二的位置(即-2)。
3.3 从lua中读取函数
要读取的lua函数为:
function add(a, b)
return a + b
end
读取函数的c++代码为:
// 5. 读取函数
lua_getglobal(L, "add"); // 获取函数
lua_pushnumber(L, 11); // 压入第一个参数
lua_pushnumber(L, 22); // 压入第二个参数
int iRet = lua_pcall(L, 2, 1, 0); // 调用函数,调用完成以后,会将返回值压入栈中,2表示参数个数,1表示返回结果个数。
if(iRet) // 错误处理
{
const char *pErrorMsg = lua_tostring(L, -1);
cout << pErrorMsg << endl;
lua_close(L);
return ;
}
if (lua_isnumber(L, -1))
{
double res = lua_tonumber(L, -1);
cout << "Func add result is : " << res << endl;
}
读取函数第一步和读取变量相同,都是使用lua_getglobal()
函数把函数放到栈顶,使用lua_tocfunction()
函数可以保存给函数指针变量,但如果要运行的话,使用lua_pushnumber
函数按顺序吧lua的参数传入,之后运行lua_pcall()
函数来执行函数,并将返回值保存在栈顶。lua_pcall()
函数的第二个参数表示该函数需要的参数, 第三个参数表示函数返回值的个数(lua支持多返回值,按顺序添加到栈顶)。
4. C++将变量和函数传至lua
假设要传的变量名为valueFromCpp
,要传的函数名为averageAndSum
,其实现如下:
static int averageAndSum(lua_State *L)
{
// 得到参数个数
int n = lua_gettop(L);
double sum = 0;
// 循环求参数之和
for (int i = 1; i <=n; i++)
{
sum += lua_tonumber(L, i); // lua的参数从1开始
}
// 返回第一个返回值(平均数)
lua_pushnumber(L, sum / n);
// 返回第二个返回值(和)
lua_pushnumber(L, sum);
// 返回返回值的个数
return 2;
}
修改MyLuaInterpreter()
如下:
void MyLuaInterpreter()
{
lua_State *L = luaL_newstate();
luaL_openlibs(L); // 使用luaopen打开lua标准库
// 1. 把值传入lua
int valueFromCpp = 100;
// 将值压入栈顶
lua_pushnumber(L, valueFromCpp);
// 给栈顶的值命名
lua_setglobal(L, "valueFromCpp");
// 2. 把函数传入lua
lua_register(L, "averageAndSum", averageAndSum);
// 等价于
// lua_pushcfunction(L, averageAndSum);
// lua_setglobal(L, "averageAndSum");
string str;
while (true) {
cout << "请输入 lua 代码路径" << endl;
cout << "> ";
getline(cin, str, '\n');
if (luaL_loadfile(L, str.c_str()) || lua_pcall(L, 0, 0, 0)) {
const char *msg = lua_tostring(L, -1);
cout << string(msg) << endl;
} else {
break;
}
}
lua_close(L);
}
向lua中传变量,只需要lua_pushnumber()
将值压入栈顶后,使用lua_setglobal()
函数给栈顶变量命名即可。
而想lua中传函数,除了需要按照lua的方式编写外,使用lua_register()
函数即可,函数第二个参数为lua中的名称,第三个参数就是函数实现。
使用如下lua代码可以进行测试:
-- 1. 从C++中读取值
print("C++中的值 = " .. valueFromCpp)
-- 2. 从C++中读取函数
avg, sum = averageAndSum(10, 20, 30, 40, 50)
print("The average is ", avg)
print("The sum is ", sum)
5. 编写动态库供lua调用
生成的动态库的名称为libmyLuaLib,动态库的源代码如下:
#include <stdio.h>
extern "C"
{
#include "lua.h"
#include "lualib.h"
#include "lauxlib.h"
};
#ifdef _MSC_VER
#define PUGILUA __declspec(dllexport)
#else
#define PUGILUA
#endif
static int averageAndSumFunc(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!\n");
return 0;
}
static const luaL_Reg myLib[] = {
{"averageAndSum", averageAndSumFunc},
{"sayHello", sayHelloFunc},
{NULL, NULL} //数组中最后一对必须是{NULL, NULL},用来表示结束
};
extern "C" PUGILUA int luaopen_libmyLuaLib(lua_State *L)
{
printf("luaopen_LuaDll invoked\n");
luaL_register(L, "myModule", myLib);
return 1;
}
其中
#ifdef _MSC_VER
#define PUGILUA __declspec(dllexport)
#else
#define PUGILUA
#endif
这段代码的作用是判断编译器是否是msvc,msvc和其他编译器编译动态库不同,需要在入口函数加上__declspec(dllexport)
(和msvc的自动动态链接功能有关)。
首先定义一个luaL_Reg
变量来保存要注册的函数与变量,最后一组必须为{NULL, NULL}
表示注册结束。
动态库的入口函数必须为luaopen_[动态库全名](lua_State *L)
lua是c写的,因此导出函数需要extern "C"
标记
把生成的dll文件放在lua解释器同目录下后,可以使用如下lua脚本检查:
require("libmyLuaLib")
local ave,sum = myModule.averageAndSum(1,2,3,4,5)
print(ave,sum) -- 3 15
myModule.sayHello() -- hello world!
如果输出为
luaopen_LuaDll invoked
3 15
hello world!
则运行成功,luaopen_LuaDll invoked是模块加载成功时输出的。
总结
lua和c/c++之间的互操作,主要是依靠lua的栈,理解lua栈是学好lua必须的。