loop_in_codes

低调做技术__欢迎移步我的独立博客 codemaro.com 微博 kevinlynx

实现自己的LUA绑定器-一个模板编程挑战

实现LUA绑定器

author : Kevin Lynx

Preface


    当LUA脚本调用我们注册的C函数时,我们需要逐个地从LUA栈里取出调用参数,当函数返回时,又需要一个一个地往LUA
栈压入返回值,并且我们注册的函数只能是int()(lua_State*)类型。这很不方便,对于上层程序员来说更不方便。
    因此我们要做的是,实现一个绑定器,可以把任意prototype的函数绑定到LUA脚本当中,并且封装取参数和压返回值时
的诸多细节。
    确实,世界上已经有很多库做了这件事情。但是,我们这里的需求很简单,我们只需要绑定函数,而不需要绑定C++类之
类的东西,自己实现的才是轻量级的。

What we usually do

    先看下我们平时是怎么做这些事的。首先注册函数很简单:

    lua_pushcfunction( L, to_string );
    lua_setglobal( L,
"tostr"
);


    然后是func的具体处理:

    /** 假设to_string在脚本中的原型是: string ()( number ) */
   
static int to_string( lua_State *L )
   
{
       
static char buf[512
];
       
/* 从LUA栈中取参数 */

       
int n = (int)lua_tonumber( L, -1 );
        sprintf( buf,
"%d"
, n );
       
/* 压入返回值 */

        lua_pushstring( L, buf );
       
return 1;
    }


    这是个简单的例子,目的是展示我之前说的局限性,以及,恩,丑陋性。


How

    要让事情变得优美,我们就得隐藏丑陋。
    首先,我们看看如何改进to_string的处理,使其看起来干净。最直接也是最通用的做法是,我们自己做一个粘合层,
充当LUA与应用层之间的粘合剂。也就是说,LUA直接回调的不再直接是应用层的函数,而是我们实现的这一层中的函数,
我们的函数整理调用参数,然后回调到上层函数,上层返回后,我们收集上层的返回值,然后整理给LUA,最后返回。
    这就是思路,具体实现时更为有趣。

Implementing...

    直觉告诉我,我需要使用C++模板来实现。模板和宏都是个好东西,因为它们是泛性的,它们给程序员带来自动性。
    另一个直觉告诉我,尽量不要让上层保存任何东西。通过模板的实例化,编译器已经为我们添加了很多东西,我也不
想让上层理会我太多。
    因为,我们至少需要保存上层的函数指针(我们暂时只考虑C式的函数),我们至少还需要一个粘合层函数用以被LUA
直接回调,所以,我得到了以下类模板:

    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可以保存任意原型的函数指针。例如:

 

typedef lua_binder<const char*(int)> binder_type;

  
    借助于模板技术,即使上层只是这样一个简单的看似不会产生任何代码的typedef,实际上也会产生出一个static的
函数指针变量:_func。

    这个时候,我们也该考虑下注册函数部分了。注意,事实上我们总共需要干两件事:封装粘合层函数、封装注册函数
部分。同样,我们得到一个最直观的注册函数模板:

 

    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 );
    }


    为什么模板参数是binder_type而不是Prototype?(最直接的想法可能会想到Prototype)因为我们需要获取func_type
以及最重要的:设置_func的值!综合起来,lua_bind函数主要作用就是接受用户层函数指针,并相应的将粘合层函数注册
到LUA中。注意,lua_pushcfunction注册的是binder_type::adapter函数。

    那么,理论上,我们现在可以这样注册一个函数:

    typedef lua_binder<const char*(_cdecl*)(int)> binder_type;
    lua_bind
<binder_type>( L, to_string, "tostr"
);


    (这个时候to_string为:const char* to_string( int ) )


处理函数参数的个数

    事情远没有我们想象的那么简单。adapter函数中毫无实现,重要的是,该如何去实现?我们面对的首个问题是:上层
函数参数个数不一样,那么我们的adapter该调用多少次lua_to*去从LUA栈中获取参数?
    解决该问题的办法是,恩,很笨,但是这可以工作:为不同参数个数的函数都实现一个对应的adapter。没有参数的函数对
应一个adapter,一个参数的函数对应另一个adapter,依次类推。
    (穿插一下:ttl(tiny template library)库中使用了一个很强大的宏技术,可以自动生成这些代码,但是具体原理
我不懂。所以只能使用这个笨办法了。)
    这样,我们就需要区分不同参数个数的函数原型。很显然,我们需要改进lua_binder。行之有效的技术是:模板偏特化。
改进后的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主体已经是一个单纯的声明而已,它的诸多特化版本将分别对应0个参数,1个参数,2个参数等。例如以上
列举的就是一个参数的偏特化版本。

Now, we can try ??

    那么,我们现在是否可以写下诸多的lua_to*函数去获取参数了?你觉得可以吗?假设现在要获取栈顶第一个参数,你
该调用lua_tonumber还是lua_tostring?
    问题就在于,我们并不知道该调用哪个函数。
    解决办法是:根据不同的参数类型,调用对应的lua_to*函数。
    不同的类型拥有不同的行为,这一点让你想起什么?那就是模板世界里的type traits,类型萃取。我想,完成本文的
绑定器,更多的是对你模板编程能力的考验。
    lua_to*系列函数是有限的,因此我们也只需要实现几个类型的行为即可。我们这个时候的目的就是,根据不同的类型,
调用对应的lua_to*函数。例如,对于number(int, long, double, float, char等等),我们就调用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(因为number类型太多,也许concept可以解决这个问题),其他特化版本处理其他
类型。这样,在adapter里,就可以根据参数类型获取到相应的参数了,例如:

    P1 p1 = param_traits<P1>::get_param( L, -1);
    到这个时候,我们的adapter函数变为:(以一个参数的函数举例)

    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 ??

    是的,我们还需要处理函数返回值。我们暂时假设所有的函数都只有一个返回值。这里面对的问题同取参数一样,我
们需要根据不同的返回值类型,调用对应的lua_push*函数压入返回值。
    同样的type traits技术,你应该自己写得出来,例如:

    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 );
        }

    }
;


    到这个时候,我们的adapter函数基本成型了:

    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手册上告诉我们,lua_CFunction必须返回函数返回值的个数。我们已经
假设我们只支持一个返回值,那么,很好,直接返回1吧。
    关键在于,C/C++的世界里还有个关键字:void。是的,它表示没有返回值。在用户层函数返回值为void类型时(原谅
这矛盾的说法),我们这里需要返回0。
    你意识到了什么?是的,我们需要根据返回值类型是否是void来设置这个return的值:1或者0。又是个type traits的
小技术。我想你现在很熟悉了:

    template <typename _Tp>
   
struct return_number_traits
   
{
       
enum

       
{
            count
= 1

        }
;
    }
;
    template
<>

   
struct return_number_traits<void>
   
{
       
enum

       
{
            count
= 0

        }
;
    }
;


    于是,我们的adapter变为:

    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?

    我很高兴我能流畅地写到这里,同样我希望我不仅向你展示了某个应用的实现,而是展示了模板编程的思想。
    但是,问题在于,当你要注册一个返回值为void的函数时:

    typedef lua_binder<void(int)> binder_type;
    lua_bind
<binder_type>( L, my_fn, "fn"
);


    你可能会被编译器告知:非法使用void类型。
    是的,好好省视下你的代码,当你的binder_type::result_type为void时,在adapter函数中,你基本上也就写下了
void r = something 的代码。这是个语法错误。

    同样的问题还有:当返回值是void时,我们也没有必要调用return_traits的set_result函数。
    我想你觉察出来,又一个type traits技术。我们将根据result_type决定不同的处理方式。于是,我写了一个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将根据不同的返回值类型决定如何去回调_func。比较遗憾的是,我们需要为每一个lua_binder编写这么一个
caller,因为caller要调用_func,并且_func的参数个数不同。
    那么现在,我们的adapter函数变为:

    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

    最后一段的标题不带问号,所以这就结束了。下载看看我的代码吧,为了给不同参数个数的函数写binder,始终需要
粘贴复制的手工劳动。
    值得注意的是,在最终的代码里,我使用了
以前实现的functor,将函数类型泛化。这样,lua_binder就可以绑定类
成员函数,当然,还有operator()。

    开源代码某个时候还需要勇气,因为开源意味着你的代码会被人们考验。不过我对我代码的风格比较自信。:D

    下载完整的lua_binder

posted on 2008-08-13 09:33 Kevin Lynx 阅读(6929) 评论(15)  编辑 收藏 引用 所属分类: c/c++lua

评论

# re: 实现自己的LUA绑定器-一个模板编程挑战 2008-08-13 09:47 dophi

kevin哥哥真厉害啊~  回复  更多评论   

# re: 实现自己的LUA绑定器-一个模板编程挑战 2008-08-13 22:45 陈梓瀚(vczh)

根据经验,开源之后,很少人会真的给你反馈什么。特别是在这里。  回复  更多评论   

# re: 实现自己的LUA绑定器-一个模板编程挑战 2008-08-14 15:29 白云哥

模板用的很精彩,学习  回复  更多评论   

# re: 实现自己的LUA绑定器-一个模板编程挑战[未登录] 2008-08-14 22:52 清风徐来

nice work  回复  更多评论   

# re: 实现自己的LUA绑定器-一个模板编程挑战[未登录] 2008-08-17 23:04 Gohan

从你的文章中了解到了类型萃取,又是一个不小的收获。  回复  更多评论   

# re: 实现自己的LUA绑定器-一个模板编程挑战 2008-09-11 19:32 czc

谢谢共享代码,有两个疑问:

1) 文中写道:

typedef lua_binder<const char*(int)> binder_type;

借助于模板技术,即使上层只是这样一个简单的看似不会产生任何代码的typedef,实际上也会产生出一个static的 函数指针变量:_func。

=================================================
如果只是用 typedef 定义一个类型而没有使用的话是不会产生任何代码的。上面的那句 typedef 编译器不会为 _func 分配存储空间,我为此还特意写了个简单程序验证了一下。或者这里是指别的意思?

2) _func 被声明为静态类型,意味着每种函数类型只能bind一个实例函数?如果要bind 多个同类型的函数,那么只有最后被bind的可以被调用,前面的被后面的覆盖了。 比如在你的测试程序里再注册一个函数 void my_fun_2(), 在 test.lua 中调用

a = fn()
a = fn_2()

结果会打印出两个 my_fn_2, 而不是一个my_fn, 一个my_fn_2


  回复  更多评论   

# re: 实现自己的LUA绑定器-一个模板编程挑战 2008-09-12 09:28 Kevin Lynx

@czc
非常高兴有人可以给我提出如此宝贵的意见!我觉得很少有人会把我写的东西认真读过,甚至相关代码。

1)我觉得你是对的,我认为只要类模板被实例化,就相当于产生了一个类,那么就会产生这个static变量。但是typedef很可能没有实例化类模板,所以我觉得你是对的,但是如何去证明这一点?

2)你说的完全正确。当lua_binder被用于实际项目时,我也发现了这个问题,所以最后我只得用一个ID去区分这些binder:
template <typename Prototype, long id>
class lua_binder;
上层代码就不得不自己提供一个唯一的ID,很丑陋,但是我没有想到优雅的解决办法,不知道你有没有什么看法?
  回复  更多评论   

# re: 实现自己的LUA绑定器-一个模板编程挑战 2008-12-25 12:12 天堂的隔壁

似乎没有考虑大量参数是数据结构的情况?
不过思路已经很有启发作用了,3ks  回复  更多评论   

# re: 实现自己的LUA绑定器-一个模板编程挑战 2009-02-27 16:19 剑神一笑

先鼓励下楼主的共享精神 :-)
不过这样的实现自己研究玩还可以,但是实际项目中基本上不会用到。
原因很简单,违反了kiss原则,在我们这些UNIX程序员的眼中,你举的第一个“丑陋”的例子才是美的,因为项目中其它成员看到,也都很容易理解代码。
C++及模板都是强大的工具,但也容易做出过度设计  回复  更多评论   

# re: 实现自己的LUA绑定器-一个模板编程挑战 2009-02-28 14:54 Kevin Lynx

@剑神一笑

你说的有道理。我也越来越喜欢UNIX的东西了。:)
  回复  更多评论   

# re: 实现自己的LUA绑定器-一个模板编程挑战 2010-04-12 00:05 anonymous

原则就是用来违反的,不违反原则就不能进步。  回复  更多评论   

# re: 实现自己的LUA绑定器-一个模板编程挑战 2010-05-06 14:10 小时候可靓了

来顶一个哦!  回复  更多评论   

# re: 实现自己的LUA绑定器-一个模板编程挑战 2012-08-31 10:10 asdf

对于bool, BOOL, BOOLEAN, 请问楼主如何解决……  回复  更多评论   

# re: 实现自己的LUA绑定器-一个模板编程挑战 2013-09-12 17:06 yzm

楼主屌爆了  回复  更多评论   

# re: 实现自己的LUA绑定器-一个模板编程挑战 2015-04-23 11:50 yk

@Kevin Lynx

为何要typedef lua_binder呢?

binder中的共性已经很好的抽象到adapter里面了,那么个性完全可以由成员变量来体现吧?

直接办法是去掉static,让functor直接成为binder的一个成员
然后bind的时候不在传static那个指针了,binder定义一个对象出来,把这个对象传给bind

这么做不知道行不行?  回复  更多评论   


只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   博问   Chat2DB   管理