可编程的log—初识lua的强大

作者: 马楠

论文摘要

n 本文介绍了使用 lua 实现的一组可用于实现 log 功能的接口。在源代码中,它与普通的 Log 语句相同 ( 在固定的地方写上固定的 log 语句 ) ,但是,它的最终功能是通过 lua 脚本来实现的。通过 lua 脚本,我们可以得到如下好处:

log 内容的可定制性。无需修改源代码中的 log 语句,即可定制不同的 log 内容。

可用于 MT 测试。由于 C MT 测试可以用 Cunit ,这个做法用处并不是很大。

可用于 IT/ST 测试。对于 IT/ST 的回归测试来说,这个功能应该是个很有用的功能。

关键字

可编程 log lua

参考资料

n Programming in lua, Roberto Ierusalimschy

1.引言

你碰到过这种情况么?在调试的时候,你无法使用IDE的断点/单步调试功能,而不得不借用printf函数在源代码添加调试信息以观察程序运行的结果。但是糟糕的是,当你调试第二个功能的时候,你发现调试第一个功能添加的语句是在是太乱了,所以你又不得不删掉。但是这样一来,更糟糕的结果是,第一个用例就无法运行了。在调试多线程程序和何时间相关的程序的时候,这种情况很常见。本文为这个问题提供了一种解决方案。

本文就是利用lua这个强大的功能来实现一个可编程的log功能。Lua是一个扩展程序设计语言,它有如下有点:

它是一个“嵌入式”,它可以嵌入到C/C++程序中使用。

和C/C++程序的可交互性。可以在C/C++程序中调用lua的函数和变量,也可以在lua脚本中调用C/C++程序的函数和变量。

安全性。在C/C++中对Lua所进行的所有的操作都是通过lua的栈来进行的,也就是说,在C/C++中使用的所有的和lua相关的变量和函数,其内存都在是由lua来维护,所以使用起来很安全。

关于lua,网上有很多介绍和教程,这里不再细说。

为了方便,我为这个可编程的log起了一个名字:ULD(Using Lua Debug),叫它ULD而不是ULL(Using Lua Log)的原因是实现它的本意是为了调试,而不是为了Log,但是后来发现将它实现为Log功能是再合适不过了。

2.ULD构造

ULD的使用设想如下:

void func()

{

int i;

int j;

uld_docatch(“func”, “enter”);

...// 处理

uld_docatch(“func”,”exit”);

}

if(uld_catch(“func”, “enter”))

{

uld_callcatch();

}

func = {}

func.enter = function(self)

print(“func”, “enter”)

--do something

end

调用 func.enter函数

主要原理是在执行一个uld_docatch语句时,先判断该函数是否在lua脚本中存在,如果存在则调用那个函数,否则忽略。

为了能够在lua脚本中做更多的事情,ULD还提供了注册全局变量和局部变量的功能。

2.1 ULD API说明

2.1.1. uld_Handle ULD实例句柄

一个uld_Handle实例句柄对应一个lua脚本文件。

2.1.2. uld_open 初始化ULD

int uld_open(uld_Handle* h);

【参数】

1. h ULD 句柄

【返回值】

TRUE :初始化成功

FALSE

2.1.3. uld_close 关闭ULD

void uld_close(uld_Handle* h);

【参数】

1. h ULD 句柄

【返回值】

2.1.4. uld_runfile以文件形式执行lua脚本

int uld_runfile(uld_Handle* h, const char* file);

【参数】

1. h ULD 句柄

2. file :文件名,不能为 NULL

【返回值】

非0:成功

0:失败

2.1.5. uld_catch判断是否捕捉了该log语句

int uld_catch(uld_Handle* h, const char* func_name, const char* step);

【参数】

1. h:ULD句柄

2. func_name :函数名,在 lua 中它对应一个 Table

3. step :该 log 语句在函数中的位置名称,在 lua 中它对应了 func_name Table 中的一个 key 的名字

【返回值】

0:lua 脚本中找到了该 func_name.step 函数

0: lua 脚本中没有找到 func_name.step 函数

【说明】

Ø ULD 设置了默认的 func_name step 其搜索的顺序如下:

func_name.step

func_name.__all

__catch_all_func.__all

Ø 函数名 func_name lua 脚本中,可以使用该 Table __func 得到,它是一个 string

Ø Step 名在 lua 脚本中,可以使用 Table __step 得到,它是一个 string

2.1.6. uld_callcatch调用lua中的函数

int uld_callcatch(uld_Handle* h, const char* str);

【参数】

1. h ULD 句柄

2. str :希望输出的字符串,可以为 NULL

【返回值】

0 :成功

0 :失败

【说明】

uld_callcatch 函数在调用 lua 中的 func_name.step 函数的同时,还可以传入一个字符串用于输出,这个字符串存储在 func_name.__msg

uld_callcatch 函数必须在 uld_catch 函数成功后调用。

2.1.7. uld_catch_setvalue_str设置一个变量和值

int uld_catch_setvalue_str(uld_Handle* h, const char* var_name, const char* value)

【参数】

1. h ULD 句柄

2. var_name :变量名

3. value :变量值,必须是字符串

【返回值】

0 :成功

0 :失败

【说明】

这个函数也必须在 uld_catch 函数成功后调用。设置成功后,可以在 Lua 中使用 func_name.var_name 得到 value

2.1.8. uld_catch_clearvalue清除一个变量

int uld_catch_clearvalue(uld_Handle* h, const char* var_name)

【参数】

1. h ULD 句柄

2. var_name :变量名

【返回值】

0 :成功

0 :失败

【说明】

这个函数也必须在 uld_catch 函数成功后调用。设置成功后,在 Lua 中使用 func_name.var_name = nil

2.1.9. uld_docatch(macro)捕捉并执行一条简单的log语句

#define uld_docatch(h, func, step) if(uld_catch(h, func, step)){uld_callcatch(h, NULL);}

2.1.10. uld_catch_block_begin(macro)开始一个语句块的捕捉uld_catck_block_end(macro) 结束一个语句块的捕捉

有的时候,我们需要一组比较复杂的语句进行输出,这个时候可以用这两个宏函数来处理。

uld_catch_block_begin(h, func_name, step);

...

uld_catch_block_end(h);

2.2 C程序中变量的引用

ULD 中,我们的目标就是如何在 lua 中访问 C 程序中的变量,我们并不考虑在 lua 中修改它。

2.3 类型的声明

ULD 提供了一组宏用来注册类型。

1. ULD_DECLARE_TYPE(type) 用来声明一个类型描述。 type 是该 struct 的名

2. ULD_IMPL_BEGIN_TYPE(type) 开始一个 struct 类型的定义

3. ULD_IMPL_END_TYPE(type) 结束一个 struct 的定义

4. ULD_IMPL_MEMBER(parent_type, type, member_name, is_pointer)

parent_type :该结构体的名字

type :成员变量的类型,如果不是结构体类型,它必须是以下类型的一种

type

C 程序中的类型

char

char

uchar

unsigned char

short

short

ushort

unsigned short

int

int

uint

unsigned int

float

float

double

double

string

char*( 0 结尾的字符串 )

member_name :该成员变量的名字

is_pointer :非 0 表示该成员变量是一个指针。

这个函数必须在 ULD_IMPL_BEGIN_TYPE ULD_IMPL_END_TYPE 的中间

5. ULD_IMPL_METHOD (parent_type, method_name, method) 定义一个成员函数

parent_type :该成员函数所属的结构体名

method_name :成员函数的名

method :成员函数的函数指针,它与 lua_CFunction 类型相同,其中第一个变量就是该结构体的指针。

2.4 类型声明的一个例子

假设 C 代码的结构体定义如下:

//struct define

typedef struct a_type

{

int b;

}a_type;

typedef struct g_type

{

int* f;

int* e;

a_type a;

}g_type;

则对应的声明如下:

//type's description variable declaration

ULD_DECLARE_TYPE (g_type);

ULD_DECLARE_TYPE (a_type);

//definition of a type description

ULD_IMPL_BEGIN_TYPE (a_type)

ULD_IMPL_MEMBER(a_type, int, b, 0)

ULD_IMPL_END_TYPE (a_type)

ULD_IMPL_BEGIN_TYPE (g_type)

ULD_IMPL_MEMBER(g_type, int, f, TRUE)

ULD_IMPL_MEMBER(g_type, int, e, TRUE)

ULD_IMPL_MEMBER(g_type, a_type, a, 0)

ULD_IMPL_METHOD(g_type, getf, g_get_f)

ULD_IMPL_END_TYPE (g_type)

2.5 Lua脚本说明

假设我们在 C 程序中写了下面这样一个函数。

1

void test(uld_Handle* h)

2

{

3

int i = 0;

4

int k = 3;

5

int *j = &k;

6

7

g.f = malloc(sizeof(int));

8

*g.f=1;

9

10

ULD_REGPARAM(h, "func",i, int, FALSE);

11

ULD_REGPARAM(h, "func",j, int, TRUE);

12

13

uld_docatch(h, "func", "first");

14

15

i = 100;

16

*j = 100;

17

*g.f = 2;

18

strcpy(p, "a");

19

uld_docatch(h, "func", "second");

20

}

如果我们需要在第 13 行输出信息的话,那么 lua 脚本应该写成下面这样。

func = {}

function func:first()

print("first") -- 打印 first

print("g:f()=", g:f()) -- 打印 g.f 变量

print("g:f()._ptr=", g:f():_ptr()) -- 打印 g.f 的指针值

print("g:a():b()=", g:a():b()) -- 打印 g.a.b 的值

print("g:getf()=", g:getf()) -- 调用 getf 函数

end

2.6 小结

lua 的强大之处在于,它能够和 C/C++ 程序进行交互。这样,我们就可以将很多需要定制的工作交给 lua 脚本来完成,而 lua 的强大的功能库还可以让你做更多的事情。例如,在上面的例子中,我们可以通过修改 lua 脚本将信息写入文件中:

f = io.open(“log.txt”, “w”)

func = {}

function func:first()

f:write("first") -- 打印 first

f:write ("g:f()=", g:f()) -- 打印 g.f 变量

f:write ("g:f()._ptr=", g:f():_ptr()) -- 打印 g.f 的指针值

f:write ("g:a():b()=", g:a():b()) -- 打印 g.a.b 的值

f:write ("g:getf()=", g:getf()) -- 调用 getf 函数

end

如果是非 GUI 测试的话,我们可以利用它来实现 ST 测试。

func = {}

function func:first()

if(g:f() == 2) then

print(“OK”)

else

print(“NG”)

end

end

另外, lua 的执行效率完很高,如果是一些数据处理的话,它甚至比我们自己写代码执行效率还高。在网上有人经过测试,它处理一个 2M 的数据文件时间不超过 2s

在做 diameter 协议编译器的时候,如果我们知道 lua 这个语言,那么我们的工作将会轻松的多。 Diameter 协议编译器的输出就是一组编解码器的源文件。在最初的时候,我们也是像 asn1c 编译器那样使用 fprintf 函数将一条条信息写到文件里,但是后来我们发现,这种做法的一个弊端就是生成的文件格式很难控制,要想修改一个缩进都不是那么容易,于是自己发明了一个非常拙劣的脚本语言。虽然这个脚本语言功能有限,设计的也很拙劣,但是它却极大的提高了程序的可理解性和扩展性。在后一期的项目中,也体现出了它的优势。事实上,这个拙劣的脚本语言的目的,就是希望能够和 C 程序交互,如果使用 lua ,它将变得更完美。

lua 是使用标准 C 写的,所以不论是在 windows 下还是在 linux 下,它都能够很好的运行。如果你担心需要携带一个库的话,你也可以直接将它的源代码集成到你的工程中。而且它是完全免费的。

lua 并不是一门新兴的语言,它在 1993 年就已经开始存在了。现在,主要是游戏中使用 lua ,不过,如果我们能够深刻理解它的优点的话,我想它能够极大的提高我们的工作效率。