lua

Lua与C接口层开发

lua与c的交互

关于lua和c的交互,主要有两个方面,一是lua调用c的函数,而另一个则是c调用lua函数。而这些都是通过lua stack来进行的。

c调用lua

在c里面使用lua,主要是通过lua_call这类函数,下面来自lua manual的例子:

lua_getglobal(L, "f");                  /* function to be called */
lua_pushstring(L, "how");                        /* 1st argument */
lua_getglobal(L, "t");                    /* table to be indexed */
lua_getfield(L, -1, "x");        /* push result of t.x (2nd arg) */
lua_remove(L, -2);                  /* remove 't' from the stack */
lua_pushinteger(L, 14);                          /* 3rd argument */
lua_call(L, 3, 1);     /* call 'f' with 3 arguments and 1 result */
lua_setglobal(L, "a");                         /* set global 'a' */

该例子等同于直接在lua里面调用 a = f(“how”, t.x, 14)。
通过上面的例子可以看到,在c中使用lua是一件很容易的事情,首先获取需要调用的lua函数,然后将其需要的参数依次压入stack,然后通过lua_call调用,该函数调用的返回值也压入stack,供c去获取。

lua调用c

对于lua调用c,我们首先需要将c函数注册给lua,而注册给lua的函数,需要满足 int (*lua_CFunction)(lua_State* pState) 这种类型。如下例子:

int multi(lua_State* pState)
{
    int data1 = int(lua_tonumber(pState, 1));
    int data2 = int(lua_tonumber(pState, 2));
    lua_pushnumber(pState, data1 * data2);
    return 1;
}

lua_register(pState, multi, "multi");

#for use in lua
#a = multi(10, 20)

我们通过lua_register将mutli函数注册给lua,该函数接受两个参数,并且有一个返回值。当lua调用multi的时候,会将参数压入stack,所以我们可以通过lua_tonumber(pState, 1)和lua_tonumber(pState, 2)来获取,其中1为第一个参数10,2为第二个参数20。当运行完成之后,multi函数通过lua_pushnumber将结果压入lua堆栈,并通过return 1告知lua有一个返回值,为200。

可以看到,在lua中使用c也是一件很简单的事情。

lua mainly or c mainly

通过上面的例子可以看出,lua与c是很方便的交互的,但是在实际的游戏项目中,我们首先必须确定的一个问题就是,代码逻辑是以lua为主还是以c为主。

  • lua为主的游戏就是逻辑主要由lua负责,核心的对性能要求较高的逻辑则由c负责,游戏中的数据大多由lua负责。
  • c为主的游戏则是逻辑主要由c负责,lua只是负责简单的配置。

这两种方式都有优劣,对于实际游戏项目来说,个人认为,应该采用lua为主,c作为高性能核心的方式。之所以这样选择,是因为游戏的逻辑变动很大,我们需要快速的进行代码迭代,这个对于lua来说非常方便。而对于核心引擎,因为变化不大,同时对性能要求较高,所以采用c是一个很好的选择。

reg helper

在实际的游戏项目中,我们会遇到这样一个问题,假设c提供的函数为 int func(int a, int b),如果这个函数要提供给lua使用,我们需要写一个对应的注册函数,如下:

int func_wrapper(lua_State* pState)
{
    int a = int(lua_tonumber(pState, 1));
    int b = int(lua_tonumber(pState, 2));
    int ret = func(a, b);
    lua_pushnumber(pState, ret);
    return 1;
}

lua_register(pState, func_wrapper, "func");

对于任意的c函数,我们需要写一个对应的wrapper用来注册给lua。如果项目中只有几个c函数,那么无所谓,但是如果需要注册给lua的c函数很多,那么对于每一个c函数写一个wrapper,是一件很不现实的事情。并且如果c函数的参数或者返回值有变化,我们同时需要修改对应的wrapper函数。基于上述原因,我们需要一套自动机制,能够将任意的c函数注册给lua使用。实际来说,我们需要提供一个函数,对于任意的c函数func,我们只需要调用register(func, “func”),那么就能直接注册给lua使用。

traits

首先,我们必须面对的问题就是,不同的c函数,参数和返回值是不一样的,譬如对于int类型的参数,我们需要通过lua_tonumber获取数据,而对于const char* 类型的参数,我们需要通过lua_tostring来获取,同理对于返回值也一样。所以我们需要一套机制,根据c函数不同的参数和返回值类型来调用lua对应的stack操纵函数。我们可以通过c++ traits来实现。

我们提供如下一套函数:

template<typename T>
struct TypeHelper{};
bool getValue(TypeHelper<bool>, lua_State* pState, int index)
{
    return lua_toboolean(pState, index) == 1;
}
char getValue(TypeHelper<char>, lua_State* pState, int index)
{
    return static_cast<char>(lua_tonumber(pState, index));
}
int getValue(TypeHelper<int>, lua_State* pState, int index)
{
    return static_cast<int>(lua_tonumber(pState, index));
}
void pushValue(lua_State* pState, bool value)
{
    lua_pushboolean(pState, int(value));
}

void pushValue(lua_State* pState, char value)
{
    lua_pushnumber(pState, value);
}

void pushValue(lua_State* pState, int value)
{
    lua_pushnumber(pState, value);
}

通过traits技术,对于不同的参数类型,我们可以调用对应的getValue函数,来从lua获取实际的数据,而对于返回值,通过c++自动的参数匹配,就能调用对应的pushValue函数。

CCallHelper

上面解决了类型匹配的问题,下面就需要提供wrapper函数,用以封装c函数。这里我们提供call helper类来实现。

template<typename Ret>
class CCallHelper
{
public:
    static int call(Ret (*func)(), lua_State* pState)
    {
        Ret ret = (*func)();
        pushValue(pState, ret);
        return 1;
    }

    template<typename P1>
    static int call(Ret (*func)(P1), lua_State* pState)
    {
        P1 p1 = getValue(TypeHelper<P1>(), pState, 1);
        Ret ret = (*func)(p1);
        pushValue(pState, ret);
        return 1;
    }
};

CCallHelper提供了静态的call函数,第一个参数就是实际c函数,通过函数模板可以进行任意c函数的匹配,这里只提供了匹配无参数和一个参数类型的c函数模板,我们可以扩展到支持任意参数个数,但也别太多了。因为对于任意c函数来说,可能有一个返回值,也可能没有返回值,那么我们如何匹配没有返回值的c函数呢?这里就是为什么我们需要CCallHelper的原因。在c++中,是不支持函数级别的模板特化的,但是类却可以,所以我们通过特化CCallHelper来匹配无返回值的c函数。如下:

template<>
class CCallHelper<void>
{
public:
    static int call(void (*func)(), lua_State* pState)
    {
        (*func)();
        return 0;
    } 

    template<typename P1>
    static int call(void (*func)(P1), lua_State* pState)
    {
        P1 p1 = getValue(TypeHelper<P1>(), pState, 1);
        (*func)(p1);
        return 0;
    }
};

CCallDispatcher

通过CCallHelper::call(func, pState),我们就可以与lua进行交互,那么又如何调用到相应的CCallHelper呢?这里我们通过CCallDispatcher来进行,如下:

template<typename Func>
class CCallDispatcher
{
public:
    template<typename Ret>
    static int dispatch(Ret (*func)(), lua_State* pState)
    {
        return CCallHelper<Ret>::call(func, pState);
    }

    template<typename Ret, typename P1>
    static int dispatch(Ret (*func)(P1), lua_State* pState)
    {
        return CCallHelper<Ret>::call(func, pState);
    }
};

通过CCallDispatcher,我们就可以将不同的c函数dispatch到不同的CCallHelper上面。

CCallRegister and regFunction

解决了c函数派发调用的问题,最后我们就需要处理如何将任意的c函数注册给lua,代码如下:

template<typename Func>
class CCallRegister
{
public:
    static int call(lua_State* pState)
    {
        Func* func = static_cast<Func*>(lua_touserdata(pState, lua_upvalueindex(1));
        return CCallDispatcher<Func>::dispatch(*func, pState);
    }
};

template<typename Func>
void regFunction(lua_State* pState, Func func, const char* funcName)
{
    int funcSize = sizeof(Func);
    void* data = lua_newuserdata(pState, funcSize);
    memcpy(data, &func, funcSize);

    lua_pushcclosure(pState, CCallRegister<Func>::call, 1);
    lua_setglobal(pState, funcName);
}

首先,我们提供CCallRegister类,里面提供了一个static的call函数,该函数满足lua注册格式,所以实际我们是将该函数注册给lua,在call函数里面,我们通过lua_touserdata(pState, lua_upvalueindex(1))来获取实际的func,然后传递给CCallDispatcher进行派发。而将call注册则是通过regFunction,该函数将实际的c函数func存储在一个userdata中,然后将该userdata绑定到对应的CCallRgister call上面,作为一个upvalue,这样当在lua里面调用call函数的时候,通过lua_upvalueindex获取对应的upvalue,则可以取到实际的c函数。

一些设计上面的考虑

上述reghelper的实现,我已经放到github luahelper上面,并且在max os,gcc 4.2,lua5.2环境下面测试通过。

这里谈一些设计上面的问题,首先,我说的任意c函数,参数和返回值只能是基本数值类型,如bool,char,short,int,long,float,double以及字符串类型char*等,这里我并没有提供复杂类型譬如class,struct的支持。之所以这样考虑,是因为我想保证lua与
c交互的简单,云风曾经说过lua不是c++,我本人当年也曾经用了2年的时间做了同样的事情。但是实现了这套东西,功能是狠强大了,以至于可以把lua当成c++来用了,但是这真的是使用lua正确的方式吗?我现在觉得,引入复杂的结合层,反而在某些时候会带来更大的复杂性,导致语言侧重点的混淆。所以有时候,我反而觉得,对于这种语言的交互,可能使用其他方式,譬如json,反而来的更容易。

写在后面的话

对于lua和游戏开发的一些东西,其实一直想写,但是以前因为很多方面的原因而中途放弃。现在重新开始,有几个方面的原因,一个在于仍然对于游戏开发的热爱,做了4年的游戏开发,虽然现在从事云存储方面的研究,但是对游戏热情依旧。另一个方面在于lua5.2的发布,觉得是应该对以前游戏人生做一个总结了。

如果需要更强大的lua与c的交互,我觉得swig可能更合适。

Lua Debugger开发

introduction

工欲善其事,必先利其器。lua作为一门动态语言,虽然我已经习惯了使用print来进行代码调试,但是还是有很多童鞋觉得一款好用的调试器能更好的进行lua代码编写。所以在接手游戏的lua结合层之后,自然就需要提供一个debuger工具了。

我们只需要的是一个能快速进行lua代码调试的工具,所以不需要gdb那种额外复杂的功能,只需要提供几种简单的功能就行了,如下:

  • c/continue 继续执行
  • bt/backtrace 列出当前堆栈
  • f/frame n 跳转到frame n
  • l/list b e 列出源代码,b为起始行,e为结束行
  • p/print v 打印v的值
  • n/next 执行,跳过下一行,包括跳过子函数
  • s/step 执行,直到碰到不同的一行
  • return 执行,直到该函数结束

虽然调试器实现的功能很简单,但是对于大多数应用来说,已经完全足够使用。

lua debug library

lua提供了一个debug library,我们就通过这个库来实现一个调试器。
首先,我们需要注册一个lua debug hook,并且绑定LUA_MASKLINE,LUA_MASKRET,LUA_MASKCALL事件,这样当lua代码执行的时候如果碰到相应的事件,则会调用我们注册的debug hook。

当debug hook调用的时候,程序就进入debug模式,这时候就可以输入对应的命令进行执行。

db

只要注册了debug hook,那么每次lua代码执行的时候碰到对应的事件就会调用注册hook,如果每次调用都进入debug模式,那是很影响程序运行的,所以我们需要一种机制,只在需要的时候进入debug模式。

在gdb里面,我们通过设置断点来进入可调式模式,虽然在lua里面也可以这么做,但这里我们采用了一种更简单的方法。我们给lua注册一个db函数,当lua执行到db函数的时候,程序才会进入debug模式。因为lua是动态语言,如果我们需要在另一个地方也进行调试,只需要再次加入db函数,重启程序即可。

所以这里我们debug hook函数内部实现是一个状态机,当没有进入db的时候,虽然lua也会进行调用该hook,但该hook内部不作任何处理。只有当执行db函数进入debug模式,hook内部才会有相应处理。

我们在debug hook里面提供了多种状态,包括none hook,step hook,next hook和return hook。

  • none hook,没有进入debug模式,该hook不做任何处理
  • step hook,进入debug的step模式,当lua代码执行到新的一行代码时候做处理
  • next hook,进入debug的next模式,当lua代码跳过下一行时候做处理
  • return hook,进入debug的return模式,当lua执行到当前函数退出时候做处理

continue

continue会让程序继续执行。该命令会让hook切入none hook状态,直到下次lua执行db函数进入debug模式。

step

step会让hook切入step hook状态,该hook会监听LUA_MASKLINE事件,当该事件发生时候,step hook进行处理,打印当前代码,并再次进入debug模式,供下次命令输入。

next

next会让hook切入next hook状态,该hook也会监听LUA_MASKLINE事件,但是next跟step最大的区别在于next是跳过一行,也就是说如果执行的lua代码下一行是一个函数调用,step会进入函数内部,而next则会执行该函数,并跳过该函数这一行直到下一行。

所以next需要进行判断LUA_MASKLINE是否进入了一个新的函数,这里我们通过函数堆栈深度来进行,当lua代码执行到一个新的函数的时候,它的函数堆栈深度会加1,所以我们只需要记录当前的堆栈深度a,next执行到下一次LUA_MASKLINE时候,获取堆栈深度b,如果a小于b,那么表明进入了一个新的函数,所以我们不需要处理,直到再次获取的堆栈深度等于a。

return

return会让hook切入return hook状态,该hook会监听LUA_MASKRET事件,当该事件发生,return hook进行处理。这里我们仍然需要进入是否进入新函数调用的判断,因为我们只想监听的是当前函数的LUA_MASKRET事件,所以我们仍需要像next那样进行堆栈深度的判断。

backtrace/frame/list

backtrace,frame,list这几个命令这里列在一起,是因为他们都跟lua_getstack,lua_getinfo这两个函数有关系。我们通过lua_getstack初始化指定栈帧的lua_Debug结构,然后在通过调用lua_getinfo获取相关栈帧信息。

print value

print value命令可能算是最复杂的一个命令,因为有多个逻辑处理。当我们通过frame定位到某一层栈帧之后,就可以通过print打印相关的对象数据,供调试使用。

当print value的时候,首先我们查找value是否在当前函数里面local变量里面,如果没有则查找该函数的upvalue,如果仍然没有,则查找global,如果都没找到,则输出nil。

code

代码在luahelper的debughelper,只是一个简单的实现,还有一些问题需要考虑。

因为lua的debug hook注册的时候只能提供一个hook,所以为了简单起见,我对DebugHelper使用了单例模式,但是最好是一个lua实例对应一个debuger。要做到这样,自己想到了两种可能方法:

  • 使用LUAI_EXTRASPACE,并通过luai_userstateopen,luai_userstateclose将debuger绑定到lua实例上面,并通过luai_userstatethread进行debuger在coroutine的迁移。这种方法需要重新编译lua代码,适合集成lua源码的项目。
  • debuger内部使用一个map来进行对应,但是在lua里面需要替换coroutine的创建,因为创建的coroutine也需要对应到同一个debuger上面。

两种方法都懒得弄了,以后有机会去尝试一下。

end

这里只是简单的实现了一个lua debuger,但是功能我觉得足以可以在实际项目中应用了。只是越来觉得,对于动态语言,print和log才是我最喜欢的代码调试方式,因为简单而且强迫你去思考整个程序的运行流程。不过把debuger放在这里,也算是对自己以前游戏开发的一个总结吧。