在上篇博客中,记录了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 thensex = 'Mr'elsesex = 'Ms'endprint("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
endfunction sayHelloToWorld()return "Hello Wolrd!"
endfunction 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]


Lua封装C++实践(二)—— C++调用Lua函数的封装相关推荐

  1. Lua封装C++实践(三)——Lua注册C++构造函数

    一个std::tuple<int,float,std::string>这样的结构,如何传递给int call(int,float ,std::string)这样的函数作为参数?如何根据函数 ...

  2. Lua封装C++实践(一)——Lua和C/C++的基本交互

    Lua 是一个小巧的脚本语言,它本身就是作为嵌入脚本而设计的,在目前所有脚本引擎中,Lua的速度是最快的.而且它的解释器非常轻量,其解释器不过200k(不同版本可能略有差异). Lua项目包含许多技术 ...

  3. python封装一个函数并调用_python - 函数的封装与调用

    一.函数的定义,函数名,函数体以及函数的调用 1.函数的定义语法: def 函数名(): 函数体 2.函数名的定义与变量名命名一样 3.函数的封装与调用 #函数的封装 defyue():print(' ...

  4. Golang实践录:调用C++函数

    趁着五一放假,趁着有时间,把欠的一些技术集中研究研究,写写文章,好给自己一个交待. 本文介绍如何在 Golang 中调用 C++ 函数. 起因 因工作需求,需要将一个工具由终端行的运行方式迁移到 we ...

  5. Golang实践录:调用C++函数的优化

    趁着五一放假,趁着有时间,把欠的一些技术集中研究研究,写写文章,好给自己一个交待. 本文继续介绍如何在 Golang 中调用 C++ 函数. 起因 前面文章介绍的方式,在运行时需要指定动态库位置,或将 ...

  6. clips系列二-clips调用外部函数

    一.在clips中声明用户定义外部函数(对应advance编程3.1节) 修改clips源码中的,userfunction.c文件中的EnvUserFunctions. 在EnvUserFunctio ...

  7. 大数据之hive实践二(DDL+DML+查询+函数)

    第 4 章 DDL 数据定义 4.1 创建数据库 1)创建一个数据库,数据库在 HDFS 上的默认存储路径是/user/hive/warehouse/*.db. hive (default)> ...

  8. android lua sd卡,记Android层执行Lua脚本的一次实践

    0. 前言 最近一直在写Lua脚本,有时候出了问题,不知道是Lua层的问题,还是上游的问题,不知道从何下手.于是我学习了一点C/C++和JNI,把整个解析Lua脚本包.执行Lua脚本的流程全部都读了一 ...

  9. lua运行外部程序_二、C++调用Lua函数

    上一篇文章中我们已经把测试环境搭建完毕了,接下来就用上次的项目工程进行代码测试和分析. 这篇文章主要讲在C++中怎么调用Lua中的函数add,并且把lua中函数计算结果返回给C++,然后在打印出来计算 ...

最新文章

  1. 【JavaScript总结】JavaScript语法基础:DOM
  2. Android 适配底部返回键等虚拟键盘的完美解决方案
  3. 菜鸟、普通、老鸟程序猿如何写奇数判断?--位操作符妙用
  4. Junit单元测试遇到的initializationerror:method initializationerror not found
  5. C# 之 HttpResponse 类
  6. 机器学习算法-随机森林之决策树R 代码从头暴力实现(2)
  7. C++ 预处理器和名称空间
  8. 参数幂等性校验失败_快速入手 Spring Boot 参数校验
  9. 软件变更控制 - 控制成本溢出
  10. 配置Apache+Php+PDT(Zend Debugger)
  11. 在react开发过程中由于setState的异步特性,获取最新state遇到问题
  12. 软件开发过程中各种文档的作用
  13. 2017年电子设计大赛(B题 滚球控制系统)赛后总结
  14. 计算机网络 子网掩码
  15. LEARNING ACTIONABLE REPRESENTATIONS WITH GOAL-CONDITIONED POLICIES
  16. 正交试验设计的基本步骤
  17. vue3 +Ts后导包出现红色波浪线【vscode】
  18. torchvision.transforms.ColorJitter函数详解
  19. nodejs shell交互_NodeJs交互式命令行工具Inquirer.js-开箱指南
  20. 【Unity】Unity在运行时崩溃了怎么办?别害怕,还有救!

热门文章

  1. 【思科网络行业解决方案】
  2. 联发科的GPU起来了!安卓旗舰芯超越苹果,天玑9200一战成名
  3. fbd 文件的研究笔记
  4. 关于一个小游戏 ———猜数字
  5. 无感token刷新,我是怎么做的
  6. ALOS数据免费查询方法
  7. BSN-DDC基础网络详解(五):接入DDC网络(1)
  8. 基于 android 创建 React-Native 项目与连接夜深模拟器
  9. linux怎么vi文件后删除空行,Vim 如何删除或替换空行空格
  10. php 小程序 运动步数_【永久会员专享】运动步数宝换购小程序源码包更新【更新至V9.6.7】...