Lua封装&C++实践(二)—— C++调用Lua函数的封装

在上篇博客中,记录了Lua与C/C++的基本交互,但是如果按照那样来使用的话,实在太麻烦了,所以我们开始进行封装。本篇博客主要记录C++调用Lua函数的封装。

封装目标

C++调用Lua,复杂的地方主要在于需要去理解Lua的堆栈,函数、参数都需要依次加入堆栈,结果也需要从堆栈里面取,Lua支持返回多个值,取值就需要按照在堆栈中的顺序多次去取。实际上呢,我们需要的就是调用一个lua函数,返回函数执行的结果,用C++11支持的代码来表示调用形式就是:

//通用调用公式,ret 包含返回结果
auto ret = lua::call<ret1,ret2,...>("funcName",param1,param2,param3,...);

最终的实际调用示例如下所示:

function sayHello(name, age, ismale, after)
    if ismale then
        sex = 'Mr'
    else
        sex = 'Ms'
    end
    print("call func sayHello")
    return string.format("Hello, %s %s, after %d years , your are %d years old!", sex, name, after, after+age), after+age, name, ismale
end

function sayHelloToWorld()

    return "Hello Wolrd!"
end

function sayHelloInLua()
    print("Hello World, I'm Lua!")
end

C++调用示例

void test()
    wLua::State * state = wLua::State::create();
    state->dofile("../res/test3.lua");
    //多个输入,多个返回值
    auto ret = state->call<char *,int,char*,bool>("sayHello","LiMing", 21, true, 10);
    //无输入,单个返回值
    auto ret2 = state->call<char *>("sayHelloToWorld");
    //无输入,无返回值
    auto ret3 = state->call("sayHelloInLua");
    //打印返回的结果
    TupleTraversal<decltype(ret)>::traversal(ret);
    TupleTraversal<decltype(ret2)>::traversal(ret2);
    TupleTraversal<decltype(ret3)>::traversal(ret3);
    
    delete state;
}

实现策略

按照上面的封装目标,首先,返回值需要包含返回结果,而返回的结果可能是一个,也可能是两个或者更多,也可能没有返回值,而C++由不支持函数多返回。如果是pythoner,很容易就能想到tuple了。C++11同样支持tuple,这样,我们就可以直接使用tuple来包装返回结果,而不必用其他如vector、map之类的方法了。

另外,从上面的通用调用公式上看,我们很明显是需要用到模板的,来指定函数返回的数据类型(同一个函数,返回不同的类型、不同返回个数什么的,目前还不清楚要怎么做)。

而且,我们调用的Lua方法,输入参数和返回值的类型和个数我们都是不知道的,只有调用者才指定,也就是输入和输出都要用到变长模板。

具体实现

调用的时候,其实并不想关注状态机,我只是想掉用一个方法,得到一个结果而已。所以在封装的时候把状态机也给屏蔽掉,有需要的时候再开放出来。

最后暴露出来的API为:

namespace wLua{
	class State{
	public:
		static State * create(LuaLib type = eLL_all);

        ~State();

        int dostring(std::string lua);

        int dofile(std::string path);

        template <typename ... Args,typename ... Params>
        std::tuple<int,Args...> call(std::string name,Params ... p);
	}
}

其中,我们最重要的就是去实现call方法,代码如下。在call方法中,我们需要做的事情如下:

  1. 获取调用函数,放到栈顶。
  2. 依次将参数压栈。
  3. 让lua执行函数
  4. 从堆栈中获取返回值,返回给C++.

关键点主要有三个:

  1. 变长参数模板递减处理
  2. 不定长度不定类型的tuple的类型获取与赋值
  3. 模板特化
template <typename ... Args,typename ... Params>
std::tuple<int,Args...> State::call(std::string name,Params ... p){
    int ret = lua_getglobal(l, name.c_str());
    push(p...);
    int retSize = sizeof...(Args);
    lua_call(l, sizeof...(Params), retSize);
    std::tuple<int,Args...> luaRet;
    TupleTraversal<std::tuple<int,Args...>>::traversal(luaRet, this, ret);
    return luaRet;
}

最终实现可以直接看项目代码。这里主要记录在这个过程中遇到的问题。主要还是因为是C++新手,使用C++不久,对于相关知识掌握不够熟练,熟手和高手可以直接略过了。

模板中类型判断和类型转换

如下代码是push的实现,还有部分关于字符串的特化的代码没有贴出来。开始的时候的想法一直是在方法中用typeid判断类型,然后根据不同的类型使用lua的api进行压栈。但是这时候遇到了问题,原本一直在用static_castreinterpret_cast的方式来转换,编译不过去。才想到了typeid的判断是运行时判断,而类型转换和模板推导是在编译期要确定的事情,这样当然就过不去了(调用和实现分开编译应该能过去吧?)。它在编译的时候,就会去检查所有的T能不能转换成需要目标类型,而字符串不能转用上面的方式去转int,所以就报错了。typeid判断那是运行期的事情,编译器可不会因为对它的if else就不进去了。
所以后面就想到了另外的办法来处理这个问题,主要两个办法,其实都是类似的:

  1. 通过地址,先转void * ,再转目标类型的指针,使用的时候通过指针取出数据。
  2. 根据目标类型和typeid判断的类型的数据长度,进行数据拷贝。

第一个方法,内存占用小的,转内存占用到的会出问题。比如T是float,目标类型是lua_Number也就是double,会把float后面的内存中的内容和float一起,变成一个错误的数据。所以最后使用了第二个办法。

但是第二个办法,遇到了字符串就有些问题了,加入传入的是char*,需要拷贝的数据长度无法确定。strlen啥的不能用,会编译不过去,原因和前面一样。所以这个时候就需要做模板特化了,把std::string、char * 都直接做模板特化。userdata现在暂时没去管,后面再加。

template <typename T>
void State::push(T& t){
    const std::type_info &tid = typeid(t);
    std::cout  <<tid.name() << std::endl;
    //typeid是运行时判断,而类型转换是编译时检查,所以这里不能直接转,否则会编译报错
    if(tid.__is_pointer_p()){
        std::cout<<"t is pointer"<<std::endl;
    }else{
        if(tid == typeid(nullptr)){
            lua_pushnil(l);
            std::cout << "push nil" << std::endl;
        }else if(tid == typeid(int)
            || tid == typeid(long)
            || tid == typeid(long long)
            || tid == typeid(unsigned int)
            || tid == typeid(unsigned long)
            || tid == typeid(unsigned long long)
            || tid == typeid(short)
            || tid == typeid(unsigned short)){
            lua_Integer ans = 0;
            memcpy(&ans, (void *)&t, sizeof(t));
            lua_pushinteger(l, ans);
            std::cout << "push integer" << t << "," << ans << std::endl;
        }
    }
}

template <typename T,typename ... Params>
void State::push(T& t,Params ... p){
    push(t);
    push(p...);
}

模板类特化和Tuple的遍历

虽然std库提供了方法来获取Tuple中元素的个数,但是我们并不能在for循环中来用std::get<i>(tuple)来获取对应位置的值,因为模板参数不能使用运行时的变量。我们传入std::get的模板参数,必须是编译期常量。这个时候,我们就需要以子之矛攻子之盾,用模板解决模板问题。
下面代码就是Tuple遍历赋值的实现。
为了浏览方便,把call方法的实现同样贴在下面了。

可以看到call的返回值第一个是int,表示的是lua函数获取的结果,获取失败的情况暂未处理。这里让第一个值为int实际上是限制call的返回值一定存在。最开始的时候没有这个int,这时候调用lua无返回参数的函数,就会出现编译问题,因为Tuple不支持void类型,而直接返回其他类型,call方法就算做特化也难以实现,这是可能需要构建另外一个模板函数出来了。而在首位加上一个int,保证Tuple存在元素,就可以避免这个问题了。

在遍历的实现中,使用来了类的偏特化,然后将State作为参数传入来进行遍历中的处理,而不是直接在State中用模板函数来完成,是因为C++11中,模板函数不支持偏特化,只有模板类才支持。

遍历时,从后往前进行Tuple元素的赋值,在最后一个元素时,也就是特化的那个,把lua函数获取的结果赋值过去,函数获取失败,后续调用应该终止,这个暂未处理,后续再去处理。

template <typename ... Args,typename ... Params>
std::tuple<int,Args...> State::call(std::string name,Params ... p){
    int ret = lua_getglobal(l, name.c_str());
    push(p...);
    int retSize = sizeof...(Args);
    lua_call(l, sizeof...(Params), retSize);
    std::tuple<int,Args...> luaRet;
    TupleTraversal<std::tuple<int,Args...>>::traversal(luaRet, this, ret);
    return luaRet;
}

template <typename Tuple,size_t N = std::tuple_size<Tuple>::value>
class TupleTraversal{
public:
    static void traversal(Tuple& tuple,State * state,int ret){
        using type = typename std::tuple_element<N - 1, Tuple>::type;
        std::get<N-1>(tuple) = state->pop<type>();
        TupleTraversal<Tuple, N - 1>::traversal(tuple, state, ret);
    }
};

template <typename Tuple>
class TupleTraversal<Tuple,1>{
public:
    static void traversal(Tuple& tuple, State * state,int ret){
        using type = typename std::tuple_element<0, Tuple>::type;
        std::get<0>(tuple) = ret;
    }
};

经过上述工作后,C++调用Lua函数就相对比较简单了。至于获取lua中的变量、table等后续再去完善了。

其他

笔记相关的代码在Github上,代码会不断变动,有需要的可以直接看对应的提交。此博客仅作为个人学习笔记及有兴趣的朋友参考使用,虚心接受建议与指正,不接受吐槽和批评,引用设计思想或代码希望注明出处,欢迎Fork和Star。wLuaBind代码地址


欢迎转载,转载请保留文章出处。湖广午王的博客[http://blog.csdn.net/junzia/article/details/95029880]


©️2020 CSDN 皮肤主题: 大白 设计师: CSDN官方博客 返回首页
实付0元
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值