使用 Lua 扩展你的程序 (1)

归档: 软件开发 | 标签:

Lua(参考资料[1])是巴西里约热内卢天主教大学的一个研究小组于 1993 年开发的。小组主要由 Roberto Ierusalimschy, Waldemar Celes 和 Luiz Henrique de Figueiredo 组成,最开始的原型为了处理数据而开发的一种脚本语言,后来逐渐演变成现在的样子。Lua 虽然小巧,但是也包含分支循环结构,动态类型,闭包等特性,有兴趣的可以看看参考资料[2]。只用 Lua 提供的原生 C API 写出来的程序和汇编差不多,需要精确控制栈的内容。为了解放程序员,网上有一些封装好了的 Lua 库,用起来也很方便。

这里主要记录自己使用 Lua 对程序进行扩展的一些尝试,用到相关的 Lua 特性时会展开介绍一下。特别说一下,虽然主要使用 Lua 作为配置文件的语言,但是用其它的语言(Python, Perl, Ruby, …)也可以达到同样的效果,只要程序能够解析所使用的语言,甚至 XML 也可以用来实现后文提到的一些功能,不过前提是你的解析器要支持 XML 来定义分支和循环等功能 :)

简单的 key/value 配置文件

需要配置参数取值的一个常见例子是数据源问题。例如程序需要从一个文件获取运行时需要的数据,测试和正式发布所使用的数据不一样,为了验证程序的正确性也准备了多套不同的数据。为了方便测试,程序使用了一个配置文件 conf.lua,其中可以指定数据源文件:

-- data source configurations

src = "data.txt"

主程序中可以解析这个配置文件获取其中的内容:

#include <stdio.h>

#include <lua.h>
#include <lauxlib.h>
#include <lualib.h>

int main(void)
{
    lua_State* l;

    l = luaL_newstate();

    if (luaL_dofile(l, "./conf.lua") != 0) {
        fprintf(stderr, "luaL_dofile err: %s.\n", lua_tostring(l, -1));
        goto end;
    }

    lua_getglobal(l, "src");
    if (!lua_isstring(l, -1))
        fprintf(stderr, "'src' should be a string.\n");
    else
        printf("src -> %s\n", lua_tostring(l, -1));
    lua_pop(l, 1);

end:
    lua_close(l);
    return 0;
}

配置文件中定义了一个全局变量 src,指定了数据源文件 data.txt。如果需要从另一个文件中读取数据只需修改配置文件并重启即可,不需要修改源代码和重新编译。在这里 Lua 的作用只是提供一系列的 key/value 配置项,这样的配置文件和下面的 XML 配置文件提供的功能一样:

<?xml version="1.0"?>

<root>
    <src>data.txt</src>
</root>

大部分程序虽然使用的配置文件格式各异,但是使用到的功能都只是这种简单的 key/value 式的配置,可以自定义一些参数取值,例如网络程序可以配置 IP 和端口,GUI 程序可以配置窗口的长宽和颜色等。这种配置形式里的配置项(key)都是固定的,而配置项的取值(value)也是固定的(允许的数值取值范围,预先定义好的字符串等)。

复杂一点的配置

程序所需要的数据除了来自普通文件,也可能来自其它不同的数据源如网络,数据库等;而不同的数据源也有不同的格式,例如不同的文件格式(二进制文件,文本文件等),不同的客户端和不同的数据库表等。程序不可能预先把所有的格式解析都实现了,因此程序支持使用不同的数据源:

#include <stdio.h>
#include <string.h>

#include <lua.h>
#include <lauxlib.h>
#include <lualib.h>

/* ------------------------------------------------------------------------- */

static int getconf_file(lua_State* l)
{
    printf("src type is file.\n");
    return 0;
}

static int getconf_mysql(lua_State* l)
{
    printf("src type is mysql.\n");
    return 0;
}

static int getconf_network(lua_State* l)
{
    printf("src type is network.\n");
    return 0;
}

/* ------------------------------------------------------------------------- */

typedef int (*getconf_t)(lua_State* l);

static struct {
    const char* type;
    getconf_t parser;
} data_src[] = {
    {"file",    getconf_file},
    {"mysql",   getconf_mysql},
    {"network", getconf_network},
    {"", NULL},
};

static inline int __getconf(lua_State* l)
{
    int i;
    const char* type;

    lua_getfield(l, -1, "type");
    if (!lua_isstring(l, -1)) {
        fprintf(stderr, "`type' should be a string.\n");
        return -1;
    }

    type = lua_tostring(l, -1);
    for (i = 0; ; ++i) {
        if (strcmp(type, data_src[i].type) == 0) {
            int ret = data_src[i].parser(l);
            lua_pop(l, 1);
            return ret;
        }
    }

    return -1;
}

/* ------------------------------------------------------------------------- */

int main(void)
{
    lua_State* l;

    l = luaL_newstate();

    if (luaL_dofile(l, "./conf.lua") != 0) {
        fprintf(stderr, "luaL_dofile err: %s.\n", lua_tostring(l, -1));
        goto end;
    }

    lua_getglobal(l, "src");
    if (!lua_istable(l, -1))
        fprintf(stderr, "'src' should be a table.\n");
    else
        __getconf(l);
    lua_pop(l, 1);

end:
    lua_close(l);
    return 0;
}

程序支持多种数据源,每个数据源的配置是一个 table,table 有一个固定的字段 type,用来指定所使用的数据源类型。对于不同数据源类型,配置项可能是不一样的,例如对于数据源是文件类型的配置文件:

-- data source configurations

src = {
    type = "file",
    name = "data.txt",
}

只需要一个文件名即可;如果数据通过网络发送,则需要 IP 和端口:

-- data source configurations

src = {
    type = "network",
    ip = "127.0.0.1",
    port = 12345,
}

如果数据源是数据库,还需要用户名和密码等登录信息:

-- data source configurations

src = {
    type = "mysql",
    ip = "127.0.0.1",
    port = 3306,
    user = "ouonline.net",
    password = "12345",
    db = "data",
    charset = "utf8",
}

对于配置文件中的某个配置项,取值一般都是固定的,例如取 “a” 表示用模块 A,”b” 表示用模块 B 等,并且程序已经实现了所有的功能,只是具体使用哪项功能可以通过配置来控制。除此之外,文件格式,网络传输格式和数据库表定义等都是固定的,如果需要从另外的格式导入数据,则需要将数据先转换成定义的格式。

自定义配置项取值(1)

源文件的格式千奇百怪,我们不可能预先实现所有的数据源格式解析。上面的例子中,如果需要增加新的格式支持,除了编写相应的解析模块外,还要修改源代码,往其中的 data_src 结构体增加新模块的名称及对应的实现函数。我们希望在不用改动源代码的前提下可以由自定义解析函数模块,只需在 Lua 中指定相应的配置项,主程序就可以使用这些模块。

首先我们为数据源定义一个统一的接口。接口很简单,只有一个函数 echo() 打印自身的一些信息(在 data_source.hpp 中定义):

#ifndef __DATA_SOURCE_HPP__
#define __DATA_SOURCE_HPP__

class DataSource {

    public:

        virtual void echo() = 0;
};

#endif

具体使用的数据源通过配置文件配置。配置文件主要包含字段“module”指定要加载的 .so 的路径,以及要传递给该数据源的配置文件“conf”:

src = {
    module  =   "./libsrc1.so",
    conf    =   "src1.lua",
}

在主程序中解析这个配置文件,加载相应的数据源模块并生成实例:

#include <memory>
#include <iostream>
using namespace std;

#include <dlfcn.h>

#include <lua.hpp>
#include "data_source.hpp"

static inline
shared_ptr<DataSource> create_data_source(const char* module,
                                          const char* conf)
{
    void* handle;
    typedef shared_ptr<DataSource> (*ctor_t)(const char*);
    ctor_t ctor;

    handle = dlopen(module, RTLD_NOW);
    if (!handle) {
        cerr << "dlopen(): " << dlerror() << "." << endl;
        return nullptr;
    }

    ctor = (ctor_t)dlsym(handle, "constructor");
    if (!ctor) {
        cerr << "dlsym(): constructor(): " << dlerror() << "." << endl;
        dlclose(handle);
        return nullptr;
    }

    return ctor(conf);
}

int main(void)
{
    lua_State* l;
    const char* module;
    const char* conf;

    l = luaL_newstate();

    if (luaL_dofile(l, "./conf.lua") != 0) {
        cerr << "luaL_dofile err: " << lua_tostring(l, -1) << "." << endl;
        goto err;
    }

    lua_getglobal(l, "src");
    if (!lua_istable(l, -1)) {
        cerr << "`src' should be a table." << endl;
        goto err;
    }

    lua_getfield(l, -1, "module");
    if (!lua_isstring(l, -1)) {
        cerr << "src::module should be a string." << endl;
        goto err;
    }
    module = lua_tostring(l, -1);

    lua_getfield(l, -2, "conf");
    if (!lua_isstring(l, -1)) {
        cerr << "src::conf should be a string." << endl;
        goto err;
    }
    conf = lua_tostring(l, -1);

    {
        auto src = create_data_source(module, conf);
        if (src)
            src->echo();
        else
            cerr << "create_data_source error." << endl;
    }

err:
    lua_close(l);
    return 0;
}

一个继承 DataSource 的数据源 DataSource1 重写了虚函数 echo():

#include <memory>
#include <iostream>
using namespace std;

#include "data_source.hpp"
#include <lua.hpp>

class DataSource1 : public DataSource {

    public:

        void echo()
        {
            cout << "I'm DataSource1." << endl;
        }
};

extern "C" {
    shared_ptr<DataSource> constructor(const char* conf)
    {
        lua_State* l;
        string datafile;

        l = luaL_newstate();

        if (luaL_dofile(l, conf) != 0) {
            cerr << "src1 init failed." << endl;
            goto err;
        }

        lua_getglobal(l, "datafile");
        if (!lua_isstring(l, -1)) {
            cerr << "'datafile' should be a string." << endl;
            goto err;
        }
        datafile = lua_tostring(l, -1);

        lua_close(l);
        return make_shared<DataSource1>();

err:
        lua_close(l);
        return nullptr;
    }
}

其中 src1 用到的配置文件:

datafile = "data.txt"

如果需要增加新的数据格式,只需编写用来解析数据的 .so,然后在配置文件中指定 .so 的路径和需要的配置文件即可,完全不需要修改主程序。

这里用到了 C++0x 的一些特性,编译时要加上“-std=c++0x”;由于在主程序和 .so 中都分别用到了 Lua,需要加上链接选项“-Wl,-E”,否则会因为加载了 liblua.a 多次而崩溃(因为 Lua 中用到了一个全局变量,具体原因可以见参考资料[3])。

参考资料

[1] Lua (programming language)
[2] Programming in Lua, Third Edition.
[3] 一个链接 lua 引起的 bug , 事不过三

发表于 2014年7月24日
本文目前尚无任何评论.

发表评论

XHTML: 您可以使用这些标签: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>