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可能更合適。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章