實(shí)現(xiàn)LUA綁定器
author : Kevin Lynx
Preface
當(dāng)LUA腳本調(diào)用我們注冊(cè)的C函數(shù)時(shí),我們需要逐個(gè)地從LUA棧里取出調(diào)用參數(shù),當(dāng)函數(shù)返回時(shí),又需要一個(gè)一個(gè)地往LUA
棧壓入返回值,并且我們注冊(cè)的函數(shù)只能是int()(lua_State*)類型。這很不方便,對(duì)于上層程序員來說更不方便。
因此我們要做的是,實(shí)現(xiàn)一個(gè)綁定器,可以把任意prototype的函數(shù)綁定到LUA腳本當(dāng)中,并且封裝取參數(shù)和壓返回值時(shí)
的諸多細(xì)節(jié)。
確實(shí),世界上已經(jīng)有很多庫(kù)做了這件事情。但是,我們這里的需求很簡(jiǎn)單,我們只需要綁定函數(shù),而不需要綁定C++類之
類的東西,自己實(shí)現(xiàn)的才是輕量級(jí)的。
What we usually do
先看下我們平時(shí)是怎么做這些事的。首先注冊(cè)函數(shù)很簡(jiǎn)單:
lua_pushcfunction( L, to_string );
lua_setglobal( L, "tostr" );

然后是func的具體處理:

/**//** 假設(shè)to_string在腳本中的原型是: string ()( number ) */
static int to_string( lua_State *L )

{
static char buf[512];

/**//* 從LUA棧中取參數(shù) */
int n = (int)lua_tonumber( L, -1 );
sprintf( buf, "%d", n );

/**//* 壓入返回值 */
lua_pushstring( L, buf );
return 1;
}

這是個(gè)簡(jiǎn)單的例子,目的是展示我之前說的局限性,以及,恩,丑陋性。
How
要讓事情變得優(yōu)美,我們就得隱藏丑陋。
首先,我們看看如何改進(jìn)to_string的處理,使其看起來干凈。最直接也是最通用的做法是,我們自己做一個(gè)粘合層,
充當(dāng)LUA與應(yīng)用層之間的粘合劑。也就是說,LUA直接回調(diào)的不再直接是應(yīng)用層的函數(shù),而是我們實(shí)現(xiàn)的這一層中的函數(shù),
我們的函數(shù)整理調(diào)用參數(shù),然后回調(diào)到上層函數(shù),上層返回后,我們收集上層的返回值,然后整理給LUA,最后返回。
這就是思路,具體實(shí)現(xiàn)時(shí)更為有趣。
Implementing...
直覺告訴我,我需要使用C++模板來實(shí)現(xiàn)。模板和宏都是個(gè)好東西,因?yàn)樗鼈兪欠盒缘模鼈兘o程序員帶來自動(dòng)性。
另一個(gè)直覺告訴我,盡量不要讓上層保存任何東西。通過模板的實(shí)例化,編譯器已經(jīng)為我們添加了很多東西,我也不
想讓上層理會(huì)我太多。
因?yàn)椋覀冎辽傩枰4嫔蠈拥暮瘮?shù)指針(我們暫時(shí)只考慮C式的函數(shù)),我們至少還需要一個(gè)粘合層函數(shù)用以被LUA
直接回調(diào),所以,我得到了以下類模板:
template <typename Prototype>
class lua_binder

{
public:
typedef Prototype func_type;
public:
static int adapter( lua_State *L )

{
return 0;
}

public:
static func_type _func;
};
template <typename Prototype> typename lua_binder<Prototype>::func_type lua_binder<Prototype>::_func = 0;

這樣,泛化了Prototype后,lua_binder可以保存任意原型的函數(shù)指針。例如:
typedef lua_binder<const char*(int)> binder_type;

借助于模板技術(shù),即使上層只是這樣一個(gè)簡(jiǎn)單的看似不會(huì)產(chǎn)生任何代碼的typedef,實(shí)際上也會(huì)產(chǎn)生出一個(gè)static的
函數(shù)指針變量:_func。
這個(gè)時(shí)候,我們也該考慮下注冊(cè)函數(shù)部分了。注意,事實(shí)上我們總共需要干兩件事:封裝粘合層函數(shù)、封裝注冊(cè)函數(shù)
部分。同樣,我們得到一個(gè)最直觀的注冊(cè)函數(shù)模板:
template <typename binder_type>
void lua_bind( lua_State *L, typename binder_type::func_type func, const char *name )

{
binder_type::_func = func;
lua_pushcfunction( L, binder_type::adapter );
lua_setgloabl( L, name );
}
為什么模板參數(shù)是binder_type而不是Prototype?(最直接的想法可能會(huì)想到Prototype)因?yàn)槲覀冃枰@取func_type
以及最重要的:設(shè)置_func的值!綜合起來,lua_bind函數(shù)主要作用就是接受用戶層函數(shù)指針,并相應(yīng)的將粘合層函數(shù)注冊(cè)
到LUA中。注意,lua_pushcfunction注冊(cè)的是binder_type::adapter函數(shù)。
那么,理論上,我們現(xiàn)在可以這樣注冊(cè)一個(gè)函數(shù):
typedef lua_binder<const char*(_cdecl*)(int)> binder_type;
lua_bind<binder_type>( L, to_string, "tostr" );

(這個(gè)時(shí)候to_string為:const char* to_string( int ) )
處理函數(shù)參數(shù)的個(gè)數(shù)
事情遠(yuǎn)沒有我們想象的那么簡(jiǎn)單。adapter函數(shù)中毫無實(shí)現(xiàn),重要的是,該如何去實(shí)現(xiàn)?我們面對(duì)的首個(gè)問題是:上層
函數(shù)參數(shù)個(gè)數(shù)不一樣,那么我們的adapter該調(diào)用多少次lua_to*去從LUA棧中獲取參數(shù)?
解決該問題的辦法是,恩,很笨,但是這可以工作:為不同參數(shù)個(gè)數(shù)的函數(shù)都實(shí)現(xiàn)一個(gè)對(duì)應(yīng)的adapter。沒有參數(shù)的函數(shù)對(duì)
應(yīng)一個(gè)adapter,一個(gè)參數(shù)的函數(shù)對(duì)應(yīng)另一個(gè)adapter,依次類推。
(穿插一下:ttl(tiny template library)庫(kù)中使用了一個(gè)很強(qiáng)大的宏技術(shù),可以自動(dòng)生成這些代碼,但是具體原理
我不懂。所以只能使用這個(gè)笨辦法了。)
這樣,我們就需要區(qū)分不同參數(shù)個(gè)數(shù)的函數(shù)原型。很顯然,我們需要改進(jìn)lua_binder。行之有效的技術(shù)是:模板偏特化。
改進(jìn)后的lua_binder類似于:
template <typename Prototype>
class lua_binder;

template <typename R, typename P1>
class lua_binder<R ( P1 )>

{
public:
typedef R result_type;
typedef P1 p1_type;
typedef result_type (*func_type)( P1 );
public:
static int adapter( lua_State *L )

{
return 0;
}
public:
static func_type _func;
};
template <typename R, typename P1>
typename lua_binder<R( P1 )>::func_type lua_binder<R( P1 )>::_func = 0;

//


lua_binder主體已經(jīng)是一個(gè)單純的聲明而已,它的諸多特化版本將分別對(duì)應(yīng)0個(gè)參數(shù),1個(gè)參數(shù),2個(gè)參數(shù)等。例如以上
列舉的就是一個(gè)參數(shù)的偏特化版本。
Now, we can try ??
那么,我們現(xiàn)在是否可以寫下諸多的lua_to*函數(shù)去獲取參數(shù)了?你覺得可以嗎?假設(shè)現(xiàn)在要獲取棧頂?shù)谝粋€(gè)參數(shù),你
該調(diào)用lua_tonumber還是lua_tostring?
問題就在于,我們并不知道該調(diào)用哪個(gè)函數(shù)。
解決辦法是:根據(jù)不同的參數(shù)類型,調(diào)用對(duì)應(yīng)的lua_to*函數(shù)。
不同的類型擁有不同的行為,這一點(diǎn)讓你想起什么?那就是模板世界里的type traits,類型萃取。我想,完成本文的
綁定器,更多的是對(duì)你模板編程能力的考驗(yàn)。
lua_to*系列函數(shù)是有限的,因此我們也只需要實(shí)現(xiàn)幾個(gè)類型的行為即可。我們這個(gè)時(shí)候的目的就是,根據(jù)不同的類型,
調(diào)用對(duì)應(yīng)的lua_to*函數(shù)。例如,對(duì)于number(int, long, double, float, char等等),我們就調(diào)用lua_tonumber。
于是得到:
template <typename _Tp>
struct param_traits

{
static _Tp get_param( lua_State *L, int index )

{
return static_cast<_Tp>( lua_tonumber( L, index ) );
}
};

template <>
struct param_traits<const char*>

{
static const char *get_param( lua_State *L, int index )

{
return lua_tostring( L, index );
}
};
//
others


param_traits主體處理所有的number(因?yàn)閚umber類型太多,也許concept可以解決這個(gè)問題),其他特化版本處理其他
類型。這樣,在adapter里,就可以根據(jù)參數(shù)類型獲取到相應(yīng)的參數(shù)了,例如:
P1 p1 = param_traits<P1>::get_param( L, -1);
到這個(gè)時(shí)候,我們的adapter函數(shù)變?yōu)椋海ㄒ砸粋€(gè)參數(shù)的函數(shù)舉例)
static int adapter( lua_State *L )

{
p1_type p1 = param_traits<p1_type>::get_param( L, -1 );
_func( p1 );

return 0;
}


And how about the result ??
是的,我們還需要處理函數(shù)返回值。我們暫時(shí)假設(shè)所有的函數(shù)都只有一個(gè)返回值。這里面對(duì)的問題同取參數(shù)一樣,我
們需要根據(jù)不同的返回值類型,調(diào)用對(duì)應(yīng)的lua_push*函數(shù)壓入返回值。
同樣的type traits技術(shù),你應(yīng)該自己寫得出來,例如:
template <typename _Tp>
struct return_traits

{
static void set_result( lua_State *L, lua_Number r )

{
lua_pushnumber( L, r );
}
};
template <>
struct return_traits<const char*>

{
static void set_result( lua_State *L, const char *r )

{
lua_pushstring( L, r );
}
};

到這個(gè)時(shí)候,我們的adapter函數(shù)基本成型了:
static int adapter( lua_State *L )

{
p1_type p1 = param_traits<p1_type>::get_param( L, -1 );
result_type r = _func( p1 );
return_traits<result_type>::set_result( L, r );
return 0;
}

The last 'return' ???
最礙眼的,是adapter最后一行的return。LUA手冊(cè)上告訴我們,lua_CFunction必須返回函數(shù)返回值的個(gè)數(shù)。我們已經(jīng)
假設(shè)我們只支持一個(gè)返回值,那么,很好,直接返回1吧。
關(guān)鍵在于,C/C++的世界里還有個(gè)關(guān)鍵字:void。是的,它表示沒有返回值。在用戶層函數(shù)返回值為void類型時(shí)(原諒
這矛盾的說法),我們這里需要返回0。
你意識(shí)到了什么?是的,我們需要根據(jù)返回值類型是否是void來設(shè)置這個(gè)return的值:1或者0。又是個(gè)type traits的
小技術(shù)。我想你現(xiàn)在很熟悉了:
template <typename _Tp>
struct return_number_traits

{
enum

{
count = 1
};
};
template <>
struct return_number_traits<void>

{
enum

{
count = 0
};
};

于是,我們的adapter變?yōu)椋?br>
static int adapter( lua_State *L )

{
p1_type p1 = param_traits<p1_type>::get_param( L, -1 );
result_type r = _func( p1 );
return_traits<result_type>::set_result( L, r );
return return_number_traits<result_type>::count;
}




Is everything OK?
我很高興我能流暢地寫到這里,同樣我希望我不僅向你展示了某個(gè)應(yīng)用的實(shí)現(xiàn),而是展示了模板編程的思想。
但是,問題在于,當(dāng)你要注冊(cè)一個(gè)返回值為void的函數(shù)時(shí):
typedef lua_binder<void(int)> binder_type;
lua_bind<binder_type>( L, my_fn, "fn" );

你可能會(huì)被編譯器告知:非法使用void類型。
是的,好好省視下你的代碼,當(dāng)你的binder_type::result_type為void時(shí),在adapter函數(shù)中,你基本上也就寫下了
void r = something 的代碼。這是個(gè)語(yǔ)法錯(cuò)誤。
同樣的問題還有:當(dāng)返回值是void時(shí),我們也沒有必要調(diào)用return_traits的set_result函數(shù)。
我想你覺察出來,又一個(gè)type traits技術(shù)。我們將根據(jù)result_type決定不同的處理方式。于是,我寫了一個(gè)caller:
template <typename _Tp>
struct caller

{
static void call( lua_State *L, p1_type &p1 )

{
result_type r = _func( p1 );
return_traits<result_type>::set_result( L, r );
}
};

template <>
struct caller<void>

{
static void call( lua_State *L, p1_type &p1 )

{
_func( p1 );
}
};


caller將根據(jù)不同的返回值類型決定如何去回調(diào)_func。比較遺憾的是,我們需要為每一個(gè)lua_binder編寫這么一個(gè)
caller,因?yàn)閏aller要調(diào)用_func,并且_func的參數(shù)個(gè)數(shù)不同。
那么現(xiàn)在,我們的adapter函數(shù)變?yōu)椋?br>
static int adapter( lua_State *L )

{
P1 p1 = lua::param_traits<P1>::get_param( L, -1);
caller<result_type>::call( L, p1 );
return return_number_traits<result_type>::count;
}

END
最后一段的標(biāo)題不帶問號(hào),所以這就結(jié)束了。下載看看我的代碼吧,為了給不同參數(shù)個(gè)數(shù)的函數(shù)寫binder,始終需要
粘貼復(fù)制的手工勞動(dòng)。
值得注意的是,在最終的代碼里,我使用了以前實(shí)現(xiàn)的functor,將函數(shù)類型泛化。這樣,lua_binder就可以綁定類
成員函數(shù),當(dāng)然,還有operator()。
開源代碼某個(gè)時(shí)候還需要勇氣,因?yàn)殚_源意味著你的代碼會(huì)被人們考驗(yàn)。不過我對(duì)我代碼的風(fēng)格比較自信。:D
下載完整的lua_binder