使用C++11模板编程封装lua

文章相关源代码下载:

https://github.com/baixuefeng/lua-cpp-wrapper

为了在项目中对接lua,查找了一些开源库,但都一些不尽如人意的地方:比如,支持的函数参数数量有限,一旦真遇到超过10参数时,就不支持了;不支持枚举,不支持自定义类型,不方便扩展等,因此参照C++11最新的模板技术,自行封装一个改造版的lua库。解决了上面那些问题。


一、Lua背景知识:

1.  Lua如何调用C

/*

**Type for C functions registered with Lua

*/

typedef  int (*lua_CFunction) (lua_State *L);

 

lua可以调用的C函数原型如上,lua库会把参数等信息存入lua_State*里面,而后调用注册的C函数,在该函数里面实现逻辑,最后把C的返回值push到lua_State*的栈上,最后该函数的返回值int表示push了多少个数据到lua_State*的栈上。通常写C代码时,一个回调函数总是会给一个额外的void*的参数,作为userdata,方便回调时用户使用,注意这里lua的回调没有,它需要通过其它方式解决,下面讲到。

 

2.  给C回调函数设置userdata

要注册C函数到lua里面,最终会调用的一个函数是

LUA_API void lua_pushcclosure (lua_State *L, lua_CFunction fn, int n);

第一、二个参数容易理解,重点是第三个参数,表示有多少个值被关联到这个注册的函数中。做法是,先push数据到lus_State*的栈上,而后调用该函数,把数据的个数传给第三个参数。

这样做了之后,当lua脚本回调到C函数中时,可以用lua_upvalueindex取得上面关联到C回调函数中的userdata,比如,关联了3个数据,那么就可以lua_upvalueindex(1),lua_upvalueindex(2), lua_upvalueindex(3)取得这三个数据的栈索引,从lus_State*的栈上读取该数据。

 

二、实现C++调用转接到lua的基本方法:

基于上面的lua调用C的基本知识,把C++和lua关联起来,让lua脚本可以调用C++的基本实现逻辑就清楚了,实现一些lua_CFunction,从lua_State*中读取lua脚本传递的参数,而后把它们转成C++类型,传参,调用合适的C++函数,返回值push到lus_State*的栈上,最后返回push的数据个数。

基本思路是清晰的,但实做起来有问题:不同的C++函数,名字,参数,返回值各不相同,这种简单的做法等于每一个转接的C++函数都需要写一个对应的lua_CFunction,而且如果C++函数有变动,相应的转接函数也得跟着变,整个操作成本非常大。

我们希望的目标是,能够有一种方法,把C++调用与lua自动关联起来,而后所有的参数传递,返回值等都自动处理好

 

三、需要解决的问题

1.  不想写那么多lua_CFunction,只写一个怎么样?

一个lua_CFunction要对应多种调用,必须有一种机制能够映射到不同的函数,结合二,可以这样实现:

    把要注册的C++函数指针作为userdata传给公共的注册函数,回调时取出函数指针调用之。

2.  函数指针的类型怎么办?

借用C++的模板函数特性,把公共的注册函数实现为模板,注册时把函数指针的类型显式实例化注册函数,这样回调过来的时候,可以把函数指针的类型恢复起来。[1]

下面是代码示例:

    //注册函数指针

template<class_FuncType>

void push_cpp_callable_to_lua(lua_State * pLua, _FuncType pf)

{

    //_FuncType 传值, 自动把函数转成指向函数的指针

    void * ppf = lua_newuserdata(pLua, sizeof(pf));

    assert(ppf);

    //把调用指针写入upvalue

    std::memcpy(ppf, &pf, sizeof(pf));

    ::lua_pushcclosure(pLua,MainLuaCFunctionCall<_FuncType>, 1);

}

 

//这是公共的注册函数

    template<class _FuncType>

    int MainLuaCFunctionCall(lua_State * pLua)

    {

       void * ppf = lua_touserdata(pLua, lua_upvalueindex(1));

        assert(*(_FuncType*)ppf);

      //…

 

3.  解析函数的参数信息、返回值信息

这是调用函数的基本要求。这里借鉴stl中的源代码,用一系列的模板特化,把参数及返回值信息解析出来。

template<classT>

struct CallableTypeHelper;

 

template<class_RetType, class... _ArgType>

struct CallableTypeHelper<_RetType(* )(_ArgType...)>

{

    //函数指针的特化

 

template<class_RetType, class _ClassType, class... _ArgType>

struct CallableTypeHelper<_RetType(_ClassType::* )(_ArgType...)>

{

//成员函数指针的特化

}

 

template<class_RetType, class _ClassType>

struct CallableTypeHelper<_RetType _ClassType::* >

{

    //成员变量指针的特化

 

有这些模板特化,只要把公共注册函数MainLuaCFunctionCall的模板类型_FuncType传给CallableTypeHelper<_FuncType>,各种类型便都解析出来了。实际实现的代码还需要考虑一些问题,不同的函数调用类型(__stdcall、__cdedl、__fastcall、__thiscall等),const属性等,这些可以使用宏来简化代码的编写。

这些类型特性是我们关心的,返回值类型,参数类型,调用类型(函数、成员函数还是成员变量)。这里定义一些类型别名:

using class_t = …; //如果是成员函数或成员变量,它代表类类型

using result_t = …; //返回值类型

using arg_tuple_t = std::tuple<…>; //参数打包而成的tuple类型

using arg_index_t = typename MakeArgIndex<…>::type;//参数序列号

static const CallableIdType callable_id = …;//调用类型的常量值

 

这里的MakeArgIndex作用是把可变参数的模板类型转化为整数序列,实现如下:

template<size_t ... Index>

struct ArgIndex

{};

template<class T, class ... Rest>

struct MakeArgIndexHelper

{

    using type = T;

};

 

template<size_t ... Index, class T,class ... Rest>

struct MakeArgIndexHelper<ArgIndex<Index...>, T, Rest ... > :

   public MakeArgIndexHelper<ArgIndex<sizeof...(Rest), Index...>,Rest...>

{};

 

//根据可变参数生成参数序列,从0开始

template<class ... T>

struct MakeArgIndex

{

   using type = typename MakeArgIndexHelper<ArgIndex<>, T...>::type;

};

 

MakeArgIndex用可变参数模板实例化时,转到MakeArgIndexHelper,第一个参数传ArgIndex<>。MakeArgIndexHelper有一个模板特化,此时优先匹配这个特化的版本,并且可变的类型包被拆分出来,而后,每向下继承一层,便把一个整数值写入到ArgIndex的类型列表中,可变的类型包里便少一个类型。每一次继承,便会再次查找一个最合适的匹配。直到最后可变类型包为0时,只能匹配主版本的MakeArgIndexHelper,这时定义第一个参数的别名为type,它就是ArgIndex<0,1,2,…>.这种技术便是模板元编程。C++14中把这个生成整数序列的算法内置为编译器支持了。

接下来,会看到这些解析出来的类型的作用。

 

4.  区别调用

不同的函数指针类型的调用方法是不同的,如函数指针,直接传参;成员函数指针,要用类指针调用;成员变量指针,用类指针调用,不传参。

怎么办?还是模板特化,上面不是已经用模板特化解析出来了各种需要的类型吗,这时候就派上用场了:

/** 调用分发器

@Tparam[in] callId: 常量值, 表示调用类型

@Tparam[in] returnVoid: 常量值, 表示返回类型是否为void

@Tparam[in] _CallableType:CallableTypeHelper类类型

@Tparam[in] _IndexType: 参数序列号

*/

template<CallableIdType callId, boolreturnVoid, class _CallableType, class _IndexType>

struct  luaCFunctionDispatcher;

 

//模板特化, 函数指针, 返回void

template<class _CallableType, size_t ...index>

struct luaCFunctionDispatcher<CallableIdType::POINTER_TO_FUNCTION, true,_CallableType, ArgIndex<index...> >

{

};

//模板特化, 函数指针, 有返回值

template<class _CallableType, size_t ...index>

structluaCFunctionDispatcher<CallableIdType::POINTER_TO_FUNCTION, false,_CallableType, ArgIndex<index...> >

{

};

//模板特化, 成员函数指针, 返回void

template<class _CallableType, size_t ...index>

struct luaCFunctionDispatcher<CallableIdType::POINTER_TO_MEMBER_FUNCTION,true, _CallableType, ArgIndex<index...> >

{

};

//模板特化, 成员函数指针, 有返回值

template<class _CallableType, size_t ...index>

structluaCFunctionDispatcher<CallableIdType::POINTER_TO_MEMBER_FUNCTION, false,_CallableType, ArgIndex<index...> >

{

};

//模板特化, 成员变量指针

template<class _CallableType>

struct luaCFunctionDispatcher<CallableIdType::POINTER_TO_MEMBER_DATA, false,_CallableType, ArgIndex<> >

{

};

这里注意模板特化中的常量参数 size_t ... index ,传类型参数ArgIndex<…>,但是模板的常量参数却不是ArgIndex,这样做的目的是剥离ArgIndex,只保留最有用的整数序列。

 

5.  怎样传参?怎样返回值?

上面已经解析出了函数的参数类型,结合参数的整数序列,如果有一个值序列,与函数的参数一一对应,那么只需要按整数序列将可变值序列展开,传参即可。

因此需要制定一个规则,lua调用C++时,必须按C++函数的参数列表,一一对应传参。这样,回调的lua_State*中,栈索引1,2,…恰好对应C++函数的参数整数序列。假设有一个辅助函数from_lua可以根据栈索引,从lua中读取数据转成C++类型的值,那么就可以按下面的方式调用C++函数:

pf(from_lua(pLua, index + 1)...);

pf代表C++函数指针,另外C++函数的参数整数序列是从0开始,而lua的栈索引是从1开始,因此要把序列号加1.

再假设有一个辅助函数to_lua,可以把C++数据写入到lua中,那么整个的C++调用,并且把C++返回值写回lua就完全可以实现了:

to_lua(pf(from_lua(pLua, index + 1)...));

有多少个值写入到lua栈上了呢?简单,调用C++之前记录下栈顶,调用之后,返回栈顶增加的数值即可。

至此,整个的注册、回调、转发调用C++,返回值回传lua的流程已经清晰:

(1) push_cpp_callable_to_lua,将C++调用注册到lua,C++调用指针通过lua的upvalue机制传入,类型通过模板显式实例化实现,所有的C++调用最终归结到一个总的lua回调:MainLuaCFunctionCall

(2) lua回调C++时,首先进入MainLuaCFunctionCall,而后从upvalue中取出C++调用指针,并且恢复类型,借肋模板特化解析类型,而后根据不同情况区别调用C++指针;

(3) to_lua(pf(from_lua(pLua,index + 1)...)); 转化lua数据,传参,调用C++函数,返回值回写lua,完成。

6.  最后要解决的问题

from_lua,to_lua怎么实现?

Lua要转成C++,需要知道C++类型;同样,C++转lua也需要根据类型。

回顾上面解析函数类型的信息,最终得到了这些类型别名:

using class_t = …; //如果是成员函数或成员变量,它代表类类型

using result_t = …; //返回值类型

using arg_tuple_t = std::tuple<…>; //参数打包而成的tuple类型

using arg_index_t = typenameMakeArgIndex<…>::type;//参数序列号

static const CallableIdType callable_id = …;//调用类型的常量值

其中,result_t就是返回值类型,于是to_lua可以解决了;arg_tuple_t是tuple类型,结合整数序列,可以用std::tuple_element获取某一个参数的类型,于是from_lua也可解决了。不同的C++类型,lua与之转换的做法不相同,要使用一个函数解决这些类型的转换,自然而然地想到,再次使用模板特化,假设该类模板取名为lua_io_dispatcher,那么C++函数调用就可以这样实现:

lua_io_dispatcher<result_t>::to_lua(

    pLua,

    pf(lua_io_dispatcher<

        std::decay_t<std::tuple_element<index, arg_tuple_t>::type >

                    >::from_lua(pLua, index+ 1)...)

    );

 

7.  更进一步

到上面,是目前包括luaplus等开源库的普遍做法,实现上虽千差万别,但基本原理大同小异。不过实际应用的时候,还会遇到问题,就是第6步中,用模板特化实现类型转化这里,普遍的做法就是从int,unsigned int,long,unsigned long,double,float,…等基本类型通通特化一遍,看起来支持得也挺全面了,但是,如果遇到用户自定义类型怎么办?C++的函数中使用自定义类型的情况非常普遍,别的不说,单说标准库中,当这些基本类型与vector结合时,需要特化的版本数量就得再翻一倍,list呢,再翻一倍,dequeu,map,set,…,因此,单单通过模板特化是根本无法满足需要的,无论你预先特化多少个版本,都是不够用的。必须让用户能够很方便的进行自由扩展。

但模板特化这种做法,对于自由扩展来说,严重不友好。

(1) 使用模板的地方必须看到所有的特化版本,那些看不到的版本不能生效;

(2) 所有特化的版本必须和主声明在同一个命名空间中;

第(1)条所导致的后果就是用户为了实现扩展必须把各种头文件都加到该模板实现里,第(2)条所导致的后果就是用户为了实现扩展得修改这个lua封装库。两条都是不可接受的。

为了解决这个问题,可以借鉴C++标准库中IO流的做法,C++的IO流就是一种可以自由扩展自定义类型的实现,只要用户自定义的类型重载了<<和>>运算符,就可以和标准库完美融合。

这就需要在from_lua,to_lua的实现里,做二次转接,比如定义两个类,lua_ostream,lua_istream,from_lua可以如此实现:

T from_lua(lua_State * pL, int index)

{

    T temp{};

    lua_istream is(pL, index);

    is >> temp;

    return temp;

}

    to_lua可以如果实现:

    void to_lua(lua_State * pL, const T &value)

    {

        lua_ostream os(pL);

        os << value;

   }

然后,可以仿照C++标准库IO流,预置lua_ostream和lua_istream对基本类型的重载实现,这样遇到用户自定义的类型时,它只要对该类型重载<<和>>运算符,就可以自由扩展了。

不过,模板特化也还是有用的,比如,枚举类型,通常他们都可以直接转化为整数,但在对C++来说,他们又是一个自定义的类型。如果每个枚举类型再自己去重载,未免累赘。这一点可以使用C++11的std::is_enum,std::underlying_type来解决:

template<classT,

    bool isEnum = std::is_enum<std::decay_t<T>>::value>

struct lua_io_dispatcher

{

    …

};

而后增加一组模板特化:

template<classT>

struct lua_io_dispatcher<T, true>

{

    using  _UType = typename std::underlying_type_t<T>;

    static int to_lua(lua_State * pL, T value)

    {

        return lua_io_dispatcher<_UType>::to_lua(pL,static_cast<_UType>(value));

    }

 

    static T from_lua(lua_State * pL, int index,T defaultValue = T{})

    {

        lua_stack_check checker(pL);

        return static_cast<T>(lua_io_dispatcher<_UType>::from_lua(pL,index, defaultValue));

    }

};

当类型是枚举时,获取underlying_type,通常他就是int,之后转接到int类型。


至此,才算是真正大功告成。



[1] 方法借鉴自luaplus

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
如果您想封装SSH2库到Lua中,以便在Lua脚本中使用SSH2功能,您可以按照以下步骤进行操作: 1. 下载并安装libssh2库。libssh2是一个用于SSH2协议的C库,您需要先安装它以在您的C++代码中使用SSH2功能。 2. 创建一个新的C++文件,命名为lua_ssh2.cpp(或任意其他名称),用于封装SSH2库到Lua。 3. 在lua_ssh2.cpp文件中包含必要的头文件,并使用extern "C"声明函数以便在Lua中调用。例如: ```cpp #include <lua.hpp> #include <libssh2.h> extern "C" { int luaopen_ssh2(lua_State* L); } ``` 4. 实现SSH2相关的Lua函数。在luaopen_ssh2函数中实现您所需的SSH2功能,根据您的需求可以包括连接到SSH服务器、执行命令、上传/下载文件等操作。根据libssh2库的API文档,您可以使用相关函数实现这些功能。 ```cpp static int lua_ssh2_connect(lua_State* L) { // 连接到SSH服务器的逻辑 return 0; } static int lua_ssh2_exec(lua_State* L) { // 执行命令的逻辑 return 0; } // 其他SSH2相关函数的实现... static const luaL_Reg ssh2_funcs[] = { { "connect", lua_ssh2_connect }, { "exec", lua_ssh2_exec }, // 其他SSH2相关函数的映射 { NULL, NULL } }; int luaopen_ssh2(lua_State* L) { luaL_newlib(L, ssh2_funcs); return 1; } ``` 请根据您的具体需求实现所需的SSH2功能。以上代码只是一个简单的示例,您可以根据您的实际情况进行扩展和修改。 5. 编译并链接C++代码,生成动态链接库(或静态库)。根据您的平台和工具链,使用

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值