代码改变世界

又一个lua与C++粘合层框架

2013-09-19 11:49  yu_yu  阅读(759)  评论(0编辑  收藏  举报

背景:

这是之前那篇烂文章的一个扩展吧!在游戏领域,特别多的使用到lua,作为C++的补充,当然会用到lua与C++的交互。lua提供了与C++交互的API,但是这些API各种坑爹、各种坑,各种繁琐,有的API操作了lua栈,有的却没有。为了解决lua原生API的问题,就出现了一些框架、库来改善,比如lua++,luabind…,窃以为,luabind是史上最强大的lua与C++粘合层,无出其右者。但是,他依赖于boost,靠,这就是我不爽他的地方,所以,咱撸起袖子,自造了一个粘合层框架。

 

目标:

    1. 独立,无需第三方库依赖
    2. 小巧,仅提供大多数场景的功能需求
    3. 易用,接口简单明确
    4. 方便,提供完备的错误信息

目录结构:

image

state.hpp—对lua_state封装,支持对内存定制

reference.hpp—对lua的function、string、table引用,提高性能会用到

module.hpp--支持类似C++中namespace功能,以table方式实现

lua_reg.hpp--头文件包含

iterator.hpp--对不定参数的迭代

execute.hpp--执行lua文件,对lua_pcall封装,支持错误处理

error.hpp--错误处理,提供fatal_error与parameter_error,支持对堆栈内容的解析

converter.hpp--C++数据与lua数据的转换,默认支持C++原生类型、std::string\std::pair\std::tuple\std::map\std::vector

config.hpp--这个没什么好说的

class.hpp--对C++类的支持

call.hpp--lua_pcall封装,C++调用lua函数,支持错误处理

实现机制:

1. lua参数与C++参数转换

   1: template < typename T, typename EnableT = void >
   2: struct convertion_t;

当需要对lua与C++参数进行转换时,请考虑片特化此类,如void*对应lightuserdata

   1: template < >
   2: struct convertion_t<void *>
   3: {
   4:     static void * from(state_t &state, int index)
   5:     {
   6:         LUAREG_ERROR(lua_islightuserdata(state, index) != 0, LUA_TLIGHTUSERDATA, index);
   7:  
   8:         return ::lua_touserdata(state, index);
   9:     }
  10:  
  11:     static std::uint32_t to(state_t &state, void *val)
  12:     {
  13:         if( val != nullptr )
  14:             ::lua_pushlightuserdata(state, val);
  15:         else
  16:             ::lua_pushnil(state);
  17:  
  18:         return 1;
  19:     }
  20: };

2. C++函数与lua函数对应关系

当需要把C++函数注册到lua,如

   1: int test2(int n, double d, const std::string &msg)
   2: {
   3:     return 10;
   4: }
   1: luareg::module(state, "cpp")
   2:         << lua::def("test2", &test2);

在lua::def函数里,首先会推导test2的函数签名

   1: template < typename R, typename ...Args >
   2: inline details::free_function_t<R, Args...> def(const char *name, R(*func)(Args...))
   3: {
   4:     return details::free_function_t<R, Args...>(name, func);
   5: }
根据模版参数,得到返回值类型,参数类型,构建一个free_function_t对象
   1: template < typename R, typename ...Args >
   2: struct free_function_t
   3: {
   4:     const char *name_;
   5:  
   6:     typedef R(*function_t)(Args...);
   7:     function_t function_;
   8:  
   9:     free_function_t(const char *name, function_t func)
  10:         : name_(name)
  11:         , function_(func)
  12:     {}
  13: };
free_function_t对象保存注册名及当前函数指针,通过operator<<操作符把这个free_function_t匿名对象给module的临时匿名对象
   1: inline module_t module(state_t &state, const char *name = nullptr)
   2: {
   3:     if( name )
   4:         assert(std::strlen(name) != 0);
   5:     return module_t(state, name);
   6: }
在module里,提供了operator<<操作符重载
   1: template < typename R, typename ...Args >
   2: module_t &operator<<(const details::free_function_t<R, Args...> &func)
   3: {
   4:     auto lambda = [](lua_State *l)->int
   5:     {
   6:         state_t state(l);
   7:         typedef typename details::free_function_t<R, Args...>::function_t function_t;
   8:         auto func = static_cast<function_t>(::lua_touserdata(state, lua_upvalueindex(1)));
   9:         
  10:         return details::call(state, func);
  11:     };
  12:  
  13:     ::lua_pushlightuserdata(state_, func.function_);
  14:     ::lua_pushcclosure(state_, lambda, 1);
  15:     ::lua_setfield(state_, -2, func.name_);
  16:  
  17:     return *this;
  18: }

通过把free_function_t对象的function_\name_注册到lua,这里使用了lua_pushccloure这个API,利用upvalue保存了这个注册函数的指针。这里的lambda是Lua_CFunction的原型,一旦lua调用了这个函数,就会回到这个lambda函数体中,再把函数指针取出来进行调用即可。

再来看看这个details::call

   1: template < typename R, typename ...Args >
   2: std::int32_t call(state_t &state, R(*handler)(Args...),
   3:                   typename std::enable_if<!std::is_same<R, void>::value>::type * = nullptr)
   4: {
   5:     return convertion_t<R>::to(state, call_impl(state, make_obj(handler), 0));
   6: }
   1: template < typename R, typename ...Args >
   2: std::int32_t call(state_t &state, R(*handler)(Args...),
   3:                   typename std::enable_if<std::is_same<R, void>::value>::type * = nullptr)
   4: {
   5:     call_impl(state, make_obj(handler), 0);
   6:  
   7:     return 0;
   8: }

返回值代表返回多少个数据到lua,通过convertion来完成。这里的enable_if来决断调用的C++函数返回值是否为void,如果为void则返回0个参数到lua。

解析就写到这儿吧,至于call_impl和make_obj请大家自己看源码吧,如果有什么不明白的,请加群探讨165666547

使用示例:

1. 对lua内存定制,只需要满足allocate、deallocate接口

   1: std::allocator<char> std_allocator;
   2: luareg::state_t state(std_allocator);

2. 注册自由函数

   1: luareg::module(state, "cpp")
   2:             << lua::def("test0", &test0)
   3:             << lua::def("test1", &test1)
   4:             << lua::def("test2", &test2)
   5:             << lua::def("test3", &test3)
   6:             << lua::def("test4", &test4)
   7:             << lua::def("test5", &test5)
当然,也可以注册类的成员函数,但是并不是由lua提供的userdata作为对象指针,而是由C++保存的指针
   1: lua::def("test6", &t, &test_t::test6);

3. 注册类
   1: luareg::module(state, "cpp")
   2:             [
   3:                 luareg::class_t<foo_t>(state, "foo_t")
   4:                 << luareg::constructor<int>()
   5:                 << luareg::destructor()
   6:                 << luareg::def("add", &foo_t::add)
   7:                 << luareg::def("get", &foo_t::get)
   8:                 << luareg::def("get_pointer", &foo_t::get_pointer)
   9:                 << luareg::def("get_base", &foo_t::get_base)
  10:             ]
需要注意的是constructor与destructor都不是必须的,如果没有,则采用默认。是不是很像luabind的语法呢?
4. 执行lua文件
   1: lua::execute(state, "test.lua");
5. 执行lua的一个函数
   1: try
   2:     {
   3:         lua::execute(state, "test2.lua");
   4:         std::pair<int, std::string> n = lua::call(state, "test_call", 1, "haha", 10.2, false);
   5:  
   6:         auto val = std::make_pair("test abc", 10.2);
   7:         lua::call(state, "test_call2", 1, "haha", val);
   8:     }
   9:     catch(const luareg::fatal_error_t &e)
  10:     {
  11:         std::cout << e.what() << std::endl;
  12:         e.dump(std::cout);
  13:     }
执行lua的test_call函数,返回两个值,因为lua可以返回多值,所以在C++中可以采用tuple或者pair来接收。其中,错误均已throw异常来处理,当然,debug的时候会有assert及堆栈信息和参数信息。
局限性:
    1. 对注册函数均已upvalue的方式来保存,限制了C++导出到lua函数个数(upvalue最大个数为255),不过,我认为这已足够,如果要导出很多接口道lua,那已经是不正常的了
    2. 与luabind比起,某些功能不支持(函数重载、导出变量等)
后记:
本框架大量使用C++11特性,使实现非常优雅的解决许多问题,比如lambda、variadic template、auto、decltype等等,所以,需要理解C++11,如果你有可能,请加入我们的C++11讨论群165666547