Lua和C是天生的好基友,语言开发者提供了一系列API,让他们通过栈进行交流。用Lua做游戏逻辑开发有些时日了,下面主要针对Lua C API的应用进行总结。
一、扩展Lua
Lua核心很小,主要包含一个解释器,其他功能可以通过动态库的形式作为插件来扩展,io、string、math、table等内置库都是通过此方式来实现,只是他们被集成到了一个lua.dll中罢了。制作一个动态库形式的module,需要在代码中通过luaL_Reg数组指定lua function到c function的映射,接着实现c function,最后在luaopen_xxx(xxx为module name)注册这个luaL_Reg。这里给出一个非常简单的例子,它使用VC++创建一个Console DLL:
#include "lua.h"
#include "lualib.h"
#include "lauxlib.h"
#include <math.h>
int mysin (lua_State* L);
static const struct luaL_Reg mymathlib [] =
{
{ "sin" , mysin } ,
{ NULL , NULL }
};
static int mysin( lua_State* L )
{
double d = luaL_checknumber( L , 1);
lua_pushnumber(L , sin( d));
return 1;
}
__declspec(dllexport) int luaopen_mymathlib(lua_State * L)
{
luaL_register(L , "mymathlib" , mymathlib);
return 1;
}
编译成dll后放到lua解释器目录下。Lua test code:
require "mymathlib"
local a = mymathlib.sin(0.5)
print(a)
二、作为脚本系统
Lua应用最多的领域当属游戏开发,WOW的UI和插件让它名声大噪。在这种应用中,Lua作为应用程序的一个子系统,用作配置或者业务处理。在将Lua与应用集成起来时,必须用到Lua C API,根据其规范,你需要写一系列的static函数,作为Lua与应用程序的粘合代码。如果要在Lua使用C++对象,可将其作为userdata,为它创建一个metatable,并将粘合函数放入其中,关键是要让__index指向metatable自身,这样当Lua访问userdata的field时,__index会引导它去搜索metatable自身,从而获得注册的粘合函数。
有很多开源的粘合代码生成器,他们都是在precompile时做了一些工作,因而不是使用macro就是template,这两种方法的代表是toLua++和luaBind。个人更倾向使用toLua++,一方面这种方式比较直白,另一方面本人弱于使用template。在WGAME中也将原来写得不是很好的bind代码替换成了toLua++,目前UI和关卡逻辑重度使用了Lua,产生的bind代码会有几万行,由于项目采用事件驱动的方式,在profile时看到对游戏整体性能影响非常小。luaBind大概了解过,没有在实际项目中用过,在此就不做评论了。
在使用toLua++时我最好奇的是它对C++关键特性是如何支持的。除了上面说的成员函数外,对于多态的支持,它是通过在static函数后加编号,调用时判断参数是否对应来遍历找到正确static函数的;对于复杂成员变量,它会自动生成get/set方法;而继承关系,则是通过子类将父类作为metatable来实现。秉着重新发明车轮的精神,我试着写了一个简化的自动生成器[我在github上]。我定义了几个关键字作为类与方法的导出标识:
{module_begin = "LUACBIND_MODULE_BEGIN" , module_end = "LUACBIND_MODULE_END" , method_begin = "LUACBIND_METHOD_BEGIN" , method_end = "LUACBIND_METHOD_END"}
util.h定义了产生bind代码需要的宏,parser.lua对指定的.h文件进行扫描产生bind代码,在main函数中register后,就可以在lua中使用了。
三、调试器
Lua的C API和Debug库提供了实现调试器的必要方法,对应了两种实现方式:一种是Remdebug所采用的,直接用lua实现;另外一种是使用C API。不管哪种方式,使用HOOK都是必须的,但使用Lua debug库会比C API更方便,因为不用考虑栈平衡问题。在用C API实现调试器时,可用lua_newthread创建一个coroutine,之后yield/resume/getstack/getlocal都作用它上面,breakpoint通常会采用在hook中yield的方式来实现,但不能等hook返回之后去进行栈回溯,因为traceexec根据hook mask调用对应hook函数后,如果state是为LUA_YIELD状态,将会调用luaD_throw,最终使用longjmp导致无法进行回溯。
利用春节值班两天清闲时光,基于lua 5.2实现了一个命令行调试器[我在github上],目前仅有几个简单的功能:加载/运行lua脚本、设置/清除断点、单步、查看简单类型变量值,命令格式可参考README。