因为一些原因,需要一个脚本系统,用来进行快速辅助开发。我本来的选择有三个,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必须的。