前言:
嵌入式开发过程中,我们会使用一些脚本工具辅助我们的工作,例如shel或者python、lua等,今天给大家分享一下,我在工作中用到的lua脚本交互使用。
作者:良知犹存
转载授权以及围观:欢迎关注微信公众号:羽林君
或者添加作者个人微信:become_me
情节介绍:
工作中, 因为我们的传感器需要出厂标定,所以我们需要有一个配置文件进行保存我们的传感器参数,这个文件支持读取和修改,实现这个功能有很多种方式,常规就是使用一个普通文件进行读写。
但是我考虑到,我们数据的复杂性,以及文件注释的描述,我选择了xml文件进行数据的保存,但是xml文件操作的库我又不想去自己写也不想去外部添加使用,本来就是一个小功能,没必要再去新增额外链接,使用别的xml操作库,所以我就盯上了我们激光slam建图算法里面用到的lua脚本,这个lua脚本的包本身以及在内核里面添加并在其他进程使用了,我只需要在我这边编译选项加 -llua动态链过去就可以多个进程一起使用了。
除了方便,也考虑到lua是一个轻量级的脚本,支持交互调用,比如说我们可以通过代码内部执行调用lua脚本函数,也可以在lua执行代码注册进去的函数。这个比shell和python有很多优势,shell只能在它脚本生成的终端去执行以及python也是类似,无法进行双方的函数交互调用。而lua可以交互调用,所以很方便。
lua介绍
Lua ,是巴西里约热内卢天主教大学里的一个研究小组于 1993 年开发的。是一种轻量小巧的脚本语言,用标准C语言编写并以源代码形式开放, 其设计目的是为了嵌入应用程序中,从而为应用程序提供灵活的扩展和定制功能。
其设计目的是为了嵌入应用程序中,从而为应用程序提供灵活的扩展和定制功能。
Lua 特性
轻量级: 它用标准C语言编写并以源代码形式开放,编译后仅仅一百余K,可以很方便的嵌入别的程序里。
可扩展: Lua提供了非常易于使用的扩展接口和机制:由宿主语言(通常是C或C++)提供这些功能,Lua可以使用它们,就像是本来就内置的功能一样。
支持面向过程(procedure-oriented)编程和函数式编程(functional programming);
自动内存管理;只提供了一种通用类型的表(table),用它可以实现数组,哈希表,集合,对象;
语言内置模式匹配;闭包(closure);函数也可以看做一个值;提供多线程(协同进程,并非操作系统所支持的线程)支持;
通过闭包和table可以很方便地支持面向对象编程所需要的一些关键机制,比如数据抽象,虚函数,继承和重载等。
Lua 应用场景
游戏开发、独立应用脚本、Web 应用脚本、扩展和数据库插件如:MySQL Proxy 和 MySQL WorkBench、安全系统,如入侵检测系统。
lua交互原理基础知识
lua和c++是通过一个虚拟栈来交互的。
c++调用lua实际上是:由c++先把数据放入栈中,由lua去栈中取数据,然后返回数据对应的值到栈顶,再由栈顶返回c++。
lua调c++也一样:先编写自己的c模块,然后注册函数到lua解释器中,然后由lua去调用这个模块的函数。
因为在我们设备上本来就有lua库,所以我开发时候直接就在CMakeLists.txt文件里面增加了 -llua,但是最开始在自己pc验证的时候,本机是没有相应的lua包的,还是下载了官网lua的源码进行编译之后,放到我的电脑指定目录进行操作验证的。
lua源码下载
去官网 http://www.lua.org/download.html 下载
make
make install
编译好的文件放到了以下三个目录
/usr/local/bin 解释器目录
/usr/local/include 头文件目录
/usr/local/lib 动态链接库目录
在后面我们进行本机测试代码时候,就可以加上绝对目录,进行头文件搜索和动态库链接了。
下面是我的一个demo测试的Makefile文件内容,其中就用了头文件目录和动态链接库目录。
OBJS = test_cpp_lua.o CFLAGS = -Wall -g -std=c++11 CC = gcc CPP = g++ INCLUDES +=-I /usr/local/include LIBS += -L /usr/local/lib -llua -ldl #LIBS = -ldl -llua target:${OBJS} # g++ -o target test_cpp_lua.o -llua -ldl @echo "-- start " ${CC} ${CFLAGS} ${OBJS} -o $@ ${INCLUDES} ${LIBS} $(CPP) ${CFLAGS} ${OBJS} -o $@ ${INCLUDES} ${LIBS} clean: -rm -f *.o core *.core target .cpp.o: #%.o:%.cpp ${CPP} ${CFLAGS} ${INCLUDES} -c $<
注意:我在编译时候还用了-ldl ,是因为程序中使用dlopen、dlsym、dlclose、dlerror 显示加载动态库,需要设置链接选项 -ldl
加载动态链接库,首先为共享库分配物理内存,然后在进程对应的页表项中建立虚拟页和物理页面之间的映射。
Lua是一种嵌入式脚本语言,即Lua不是可以单独运行的程序,在实际应用中,主要存在两种应用形式。第一种形式是,C/C++作为主程序,调用Lua代码,此时可以将Lua看做“可扩展的语言”,我们将这种应用称为“应用程序代码”。第二种形式是Lua具有控制权,而C/C++代码则作为Lua的“库代码”。在这两种形式中,都是通过Lua提供的C API完成两种语言之间的通信的。
接下来我给大家分别介绍两者调用的用法,以及补充到我自己实际使用xml文件的读写的操作demo。本文没有过多描述lua脚本语言的使用操作,仅仅做一些实际调用过程中的应用分享。
C/C++代码调用 lua变量和函数
首先我们最常用的就是进行脚本的调用,来个最常见的调用机制,在代码里面执行调用脚本里面函数或者获得脚本文件里面的一些设置信息。
这lua脚本里面的代码部分:
debug_enbale = "enable" angle_table = { roll_offset = 0.05 , pitch_offset = 0.0, yaw_offset = 0.0, } for i,v in ipairs(angle_table) do print(i,v) end
这个里面定义了一个字符串变量 debug_enbale ,和一个 lua的table angle_table,最后还有一个进行table遍历的for循环流程控制代码。这样在执行lua脚本时候就可以打印对应table里面变量信息
在lua中,lua堆栈就是一个struct,堆栈索引的方式可是是正数也可以是负数,区别是:正数索引1永远表示栈底,负数索引-1永远表示栈顶。所以我们在使用过程中会看到push_x 和 to_x这样的函数,就是进行堆栈的操作。
这部分是常规的使用,我在里面分别获取了number数据和string数据,放到我的执行代码的运行变量中去。
#include "lua.hpp" #include <iostream> int main(int argc,char ** argv) { lua_State *pLua = luaL_newstate(); if(!pLua) { LOG(Info, "Failed to open Lua!"); return false; } luaL_openlibs(pLua); int bRet = luaL_loadfile(pLua, lua_path.c_str()); if (bRet) { LOG(Info, "load .lua file failed" ); return false; } // 执行lua文件 bRet = lua_pcall(pLua, 0, 0, 0); if (bRet) { LOG(Info, "call .lua file failed" ); return false; } lua_getglobal(pLua, "debug_enbale"); std::string str = lua_tostring(pLua, -1);//获得lua脚本debug_enbale位置的数据 LOG(Info, "debug_enbale=" << str); auto get_float_data_from_lua = [&](const char * table_name,const char * value_name) -> float{ lua_getglobal(pLua, table_name); lua_getfield(pLua, -1, value_name); return lua_tonumber(pLua, -1); }; roll_offset = get_float_data_from_lua("angle_table","roll_offset"); pitch_offset = get_float_data_from_lua("angle_table","pitch_offset"); yaw_offset = get_float_data_from_lua("angle_table","yaw_offset"); LOG(Info, "angle_table:" << roll_offset <<" " << pitch_offset <<" " << yaw_offset ); lua_close(pLua); }
重要函数描述
1.因为工程是cpp,所以添加lua.hpp,如果是C工程,可以直接包含lua.h。
2.lua_State *pLua = luaL_newstate(); Lua库中没有定义任何全局变量,而是将所有的状态都保存在动态结构lua_State中,后面所有的C API都需要该指针作为第一个参数。
3.luaL_openlibs函数是用于打开Lua中的所有标准库,如io库、string库等。
4.luaL_loadfile实际调用了lua_load函数来加载lua文件。
5.lua_pcall函数会将程序块从栈中弹出,并在保护模式下运行该程序块。执行成功返回0,否则将错误信息压入栈中。
6.lua_getglobal调用这个宏的时候,都会将Lua代码中与之相应的全局变量值压入栈中
7.lua_tostring函数中的-1,表示栈顶的索引值,栈底的索引值为1,以此类推。该函数将返回栈顶的字符串信息
7.lua_getfield把堆栈中指定索引-1为栈顶 angle_table中的value_name的具体值push到堆栈。
8.lua_tonumber 把栈顶中数据以数值形式返
9.lua_close用于释放状态指针所引用的资源。
其中,使用有数据区分的函数lua_tonumber和lua_tostring两种函数,lua_tonumber返回包括整形和浮点型。
这样我就获得了lua脚本里面我写好的数据,用来配合我代码执行。
lua变量 调用C/C++代码函数
引用文章《Step By Step(Lua调用C函数)》
Lua可以调用C函数的能力将极大的提高Lua的可扩展性和可用性。对于有些和操作系统相关的功能,或者是对效率要求较高的模块,我们完全可以通过C函数来实现,之后再通过Lua调用指定的C函数。对于那些可被Lua调用的C函数而言,其接口必须遵循Lua要求的形式,即typedef int (lua_CFunction)(lua_State L)。
简单说明一下,该函数类型仅仅包含一个表示Lua环境的指针作为其唯一的参数,实现者可以通过该指针进一步获取Lua代码中实际传入的参数。返回值是整型,表示该C函数将返回给Lua代码的返回值数量,如果没有返回值,则return 0即可。需要说明的是,C函数无法直接将真正的返回值返回给Lua代码,而是通过虚拟栈来传递Lua代码和C函数之间的调用参数和返回值的。这里我们将介绍两种Lua调用C函数的规则。
Lua调用C函数有两种方式
1、程序主体在C中运行,C函数注册到Lua中。C调用Lua,Lua调用C注册的函数,C得到函数的执行结果。
2、程序主体在Lua中运行,C函数作为库函数供Lua使用。
第一种方式看起来很罗嗦,也很奇怪。既然程序主体运行在C中,而且最终使用的也是C中定义的函数,那么为何要将函数注册给Lua,然后再通过Lua调用函数呢?
所以相比于第一种方式,第二种方式使用的更加普遍。
关于这部分代码我也只是在我电脑上跑了几个范例,大家也可以去网上自己去查找相关例子,我自己在设备上并没有使用到这个部分,后续我有使用可以再写这部分应用给大家分享。
lua进行xml文件的操作
这个部分是因为之前的功能做了一些修正,原因是我们需要的传感器参数放置的文件可以被修改,如果使用lua脚本里面写入参数,那么参数相当与定死了,我们后续是无法使用lua脚本修改里面本身的数据的,所以后来我就使用xml文件放置我的传感器参数,使用lua脚本进行读写。
xml是一个常用来写一些我们不定数据配置文件的格式,lua也有luaxml,工具包,但是我为了不新增额外库实现,所以使用了lua里面I/O库(lua用于读取和处理文件的库)读写的方法进行读写xml文件。
大家也可以用xml一个lua其他工具进行使用,会更加方便,下面分享一个官方给的链接:
http://lua-users.org/wiki/LuaXml
里面为四种不同方法的xml读写工具:
工具包;
仅限 Lua 的 XML 解析器;
包含 C 代码和绑定的 XML 解析器;
用于处理基于 XML 的协议(例如 XML-RPC 和 SOAP)的模块。
lua脚本中读写xml函数
function get_value_from_xml(path,element_name) xml_file=path element=element_name head="<"..element..">" tail="</"..element..">" file = io.open(xml_file, "r"); --打开xml文件 data = file:read("*all"); --读取文件的全部内容到data变量中 file:close(); --关闭xml文件 --获取起始tag与关闭tag之间的内容到value中 _,_,value=string.find(data, head.."(.-)"..tail) --输出value的值到标准输出 -- print(value) return value end function set_value_to_xml(path,element_name,set_value) xml_file=path element=element_name new_value=set_value head="<"..element..">" --根据元素名生成起始tag,即<element_name> tail="</"..element..">" --根据元素名生成关闭tag,即</element_name> file = io.open(xml_file, "r"); --打开xml文件 data = file:read("*all"); --读取文件的全部内容到data变量中 file:close(); --关闭xml文件 --将element之前的内容,element的值,element之后的内容,分别保存在pre,old_value,follow中 _,_,pre,old_value,follow=string.find(data, "(.*)("..head..".-"..tail..")(.*)") file = io.open(xml_file, "w"); --打开xml文件 file:write(pre..head..new_value..tail..follow); --拼装出新的文件内容,并写入 file:close(); --关闭xml文件 end
上面主要是利用了 string.find 它的三个参数和三个返回值。string.find 功能是从字符串中找到特定的内容。
第一个参数是目标字符串(所有的内容,比如data),第二个参数是想要找的字符串(比如 item 之间的内容),第三个参数是从第几个字符开始找起(我让它成为一个不断变化的值)。
第一个返回值是找到的字符串( item 之间的内容)的首字符位置(一个数字),第二个返回值是找到的字符串( item 之间的内容)的尾字符位置(一个数字),第三个是找到的字符串( item 之间的内容)。
所以代码就是靠每次循环的第三个返回值来获取内容。
cpp代码:
#include "lua.hpp" #include <iostream> int main(int argc,char ** argv) { lua_State *pLua = luaL_newstate(); if(!pLua) { LOG(Info, "Failed to open Lua!"); return false; } luaL_openlibs(pLua); int bRet = luaL_loadfile(pLua, lua_path.c_str()); if (bRet) { LOG(Info, "load .lua file failed" ); return false; } // 执行lua文件 bRet = lua_pcall(pLua, 0, 0, 0); if (bRet) { LOG(Info, "call .lua file failed" ); return false; } auto lua_func_call_wirte = [&](const char * func_name, const char * key_name,float &value){ lua_getglobal(pLua, func_name); lua_pushstring(pLua ,surface_xml_used_path.c_str()); lua_pushstring(pLua ,key_name); lua_pushnumber(pLua ,value); bRet = lua_pcall(pLua, 3, 1, 0);//三个参数 一个返回值 if (bRet) { const char* pErrorMsg = lua_tostring(pLua, -1); LOG(Info, "lua_pcall - ErrorMsg:" << pErrorMsg ); // lua_close(pLua); return false; } if (lua_isnumber(pLua, -1)) { value = lua_tonumber(pLua, -1); LOG(Info, "surface_config " << value); return true; } return false; }; float roll_offset = 0.5,pitch_offset = 0.2,yaw_offset=0.6; lua_func_call_wirte("set_value_to_xml"," roll_offset", roll_offset); lua_func_call_wirte("set_value_to_xml"," pitch_offset", pitch_offset); lua_func_call_wirte("set_value_to_xml"," yaw_offset", yaw_offset); auto lua_func_call_number = [&](const char * func_name, const char * key_name,float &value){ lua_getglobal(pLua, func_name); lua_pushstring(pLua ,surface_xml_used_path.c_str()); lua_pushstring(pLua ,key_name); bRet = lua_pcall(pLua, 2, 1, 0);//两个参数 一个返回值 if (bRet) { const char* pErrorMsg = lua_tostring(pLua, -1); LOG(Info, "lua_pcall - ErrorMsg:" << pErrorMsg ); // lua_close(pLua); return false; } if (lua_isnumber(pLua, -1)) { value = lua_tonumber(pLua, -1); LOG(Info, "config " << value); return true; } return false; }; auto lua_func_call_string = [&](const char * func_name, const char * key_name,std::string &value){ lua_getglobal(pLua, func_name); lua_pushstring(pLua ,surface_xml_used_path.c_str()); lua_pushstring(pLua ,key_name); bRet = lua_pcall(pLua, 2, 1, 0); if (bRet) { const char* pErrorMsg = lua_tostring(pLua, -1); ZY_LOG("robotctl", kInfo, "lua_pcall - ErrorMsg:" << pErrorMsg ); lua_close(pLua); return false; } if (lua_isstring(pLua, -1)) { value = lua_tostring(pLua, -1); LOG(Info, "config " << value.c_str() ); return true; } return false; }; std::string temp_from_lua; lua_func_call_string("get_value_from_xml","debug_enable",temp_from_lua); LOG(Info, "debug_enbale =" << temp_from_lua); lua_func_call_number("get_value_from_xml","down_roll_offset",roll_offset); lua_func_call_number("get_value_from_xml","down_pitch_offset",pitch_offset); lua_func_call_number("get_value_from_xml","down_yaw_offset",yaw_offset); LOG(Info, "ngle_table:" << roll_offset <<" " << pitch_offset <<" " << yaw_offset ); lua_close(pLua); }
xml文件内容:
<debug_enable>enable</debug_enable> <roll_offset>0.0</roll_offset> <pitch_offset>0.0</pitch_offset> <yaw_offset>0.0</yaw_offset>
看到这块大家可能会问我,为什么不直接使用linux下直接做一个文件进行读写配置数据呢,我的想法是,纯文件不好进行注释,因为我的配置参数有些很长,我想把它尽可能让别人看懂,所以我写了一些注释进去,xml很符合我的要求,此外lua脚本也一些脚本使用过程中,很好的可以辅助我本身的代码执行,所以我就考虑把一些整体差不多执行的功能操作可以集成到一起,统一接口去执行。所以最后选择了lua+xml,技术有很多种实现思路,但是我们需要衡量一下哪些部分的技术可以让平台可以重复利用的多一些。
结语
这就是我分享我在工作中使用lua脚本的操作,如果大家有更好的想法和需求,也欢迎大家加我好友交流分享哈。
作者:良知犹存,白天努力工作,晚上原创公号号主。公众号内容除了技术还有些人生感悟,一个认真输出内容的职场老司机,也是一个技术之外丰富生活的人,摄影、音乐 and 篮球。关注我,与我一起同行。