LCM库——LCM Type Language分析(一)
说明
LCM是一套消息传递和数据编组(序列化,data marshalling)的库和工具,主要面向高带宽和低延时的近实时系统。它提供了publish/subscribe消息的机制,数据序列化和反序列化的代码自动生成工具。
LCM通过LCM Type Language来定义要传递的数据,然后通过代码自动生成来产生序列化和反序列化的代码,提供了很多主流编程语言的支持。它支持两种结构体,struct和enum,其语法类似C。struct的成员变量除了支持Primitive Types,它还支持固定数组和可变长数组,常量。每个类型自带一个哈希值,由成员变量类型和名字的哈希来产生,主要被用在反序列化时的类型检查。它的自动生成工具本质上是一个C结构体的parser。对于一个lcm类定义文件,它首先分词,再解析语法,最后生成各编程语言的代码文件。
分词 tokenize.h/c
该模块主要将lcm文件中的字符串分词,仅仅是分词,不作语法判断。分词种类有:
单字符token,C语言格式,以'开头,可以有escaped char,以'结束。
单字符token,有"();\",:\'[]"。
C++字符常量,以"开头,以"结尾。
C++格式注释,单个注释为一个分词,支持/*和//。
连续的运算符,有"!~<>=&|^%*+="。
其他所有由whitespace分隔的 alpha-numeric字符串。注意.也算alpha-numeric字符。
其主要代码逻辑在tokenize_next_internal,该函数由以下几个步骤顺序执行来分词。
忽略whitespace。
判断是否为字符常量,字符常量以'开头,可以有escaped char,以'结束。若是,创建分词。
判读是否为字符串常量,字符串常量以"开头,以"结尾。若是,创建分词。这部分有bug,如果运算符是文件的最后一个字符,将会陷入死循环,因为它没有判断下一个是不是EOF,无条件回滚一个字符。
判断是否是连续的运算符。若是,创建分词。
判断是否是注释。若是,创建分词。
判断是否是alpha-numeric字符串。若是,创建分词。
如果输入字符串为abc;‘e’ “string”/comment/ 3.14=+==123,它会被分词为:
tok# line col: token
0 1 0: abc
1 1 3: ;
2 1 4: 'e'
3 1 8: "string"
4 1 16: comment
5 1 28: 3.14
6 1 32: =+==
7 1 36: 123
语法解析 lcmgen.h/c
该文件可以看做是LCM Type Language的语法分析器(Parser),其实就是一个C语言的struct的语法解析器,采用了递归下降分析。函数主入口为lcmgen_handle_file,循环调用parse_entity函数直至没有可以解析的实体(entity)。parse_entity函数是真正的解析器,它会顺序尝试解析三种实体package、struct和enum,任何一个实体被成功解析都会返回。
注:这种调法意味着同一个lcm文件中可以有多个package语句,该语句以下的定义直至下一个package语句被安插在同一个package中。
接下来看上述三种实体是如何被解析的。
package 实体
package的解析相对来说简单一些,它本身没有任何数据结构,就是lcmgen_t中的一个字符串成员变量。就是先查当前token是否是package,第二个token有效,第二个token是package的名字,再看第三个token是否为;。中间的所有注释token都被忽略了。这里面其实有一个bug,就是没有查package名是否是合法的。可以往里面名字里面添加$$$,也可以通过lcmgen。
enum 实体
enum实例的解析也不复杂,其内部主要是循环地去解析枚举类型值lcm_enum_value_t,其实就是解析是否是val [= xxx],的格式,可以不赋值,lcm会自动地给未赋值的变量一个当前最大值+1,它也会查是否有重复的值。解析时同样不检查是否是合法变量名。它的哈希值计算仅仅考虑枚举类型的名字,每个值的名字不考虑,这是为了方便往里面添加新的值,比如ErrorCode是不断增长的。
struct 实体
struct解析最为复杂。结构体中支持一般成员变量、常变量、定长数组,不定长数组,不支持内嵌struct和enum。它的哈希计算是各个成员变量的名字,primitive_type的类型名,数组的大小,注意这并不是最终在生成的代码中的哈希值,最终代码还会递归调用非primitive类型的哈希函数。
常变量解析,常变量以const打头,解析类型token,类型仅支持"int8_t", “int16_t”, “int32_t”, “int64_t”, “float”, “double”,期望下一个token是等号,接下来是常量值,最后是;。
普通变量,它的解析相对简单,由于允许引用不同package中的结构体,变量名还自带package信息,就是.前面的东西。如果没有package信息且为非primitive类型,默认该类型在当前的package内。PS:其实lcm在处理普通成员变量时将其作为长度为1的定长数组。
多维数组,多维数组的每个维度的长度可以是常量也可以是变量,如果是变量,该变量必须是之前出现过得变量名,该变量的大小需要调用者自己维护,这很容易出错。
C++代码生成 emit_cpp.c
该模块负责根据语法解析的结果生成C++代码,函数主入口为int emit_cpp(lcmgen_t *lcmgen)。
生成的头文件的位置
它会根据每个结构体所在的package,在指定目录下生成对应的头文件。比如package为myspace.types,头文件的路径为 specified_folder/myspace/types/struct_name.hpp。
头文件的include部分
它会根据是否包含了string和动态数组来包含std::vector和std::string的头文件,然后包含每个非primitive成员变量类型对应的头文件,它们的路径在其typename中。
struct声明部分
struct的声明有三部分组成,普通成员变量,常量成员变量以及一些固定的成员函数。
成员函数声明:
public的成员函数一共五个:
inline int encode(void *buf, int offset, int maxlen) const;
inline int getEncodedSize() const;
inline int decode(const void buf, int offset, int maxlen);
inline static int64_t getHash();
inline static const char getTypeName();
此外,还有一些helper function也会被生成,这些函数被用来帮助public成员函数的实现。primitive类的相关函数定义提前写在lcm/lcm_coretypes.h中,这里阐述的是用户自定义的成员函数。
生成成员函数定义
生成encode函数定义
encode函数是一个序列化函数,该函数的声明是:
/** Encode a message into binary form.
* @param buf The output buffer.
* @param offset Encoding starts at thie byte offset into @p buf.
* @param maxlen Maximum number of bytes to write. This should generally be equal to getEncodedSize().
* @return The number of bytes encoded, or <0 on error.
*/
inline int encode(void *buf, int offset, int maxlen) const;
它由emit_encode生成,首先生成一个计算int64_t的哈希值的代码,然后通过emit_encode_nohash递归生成代码去调用每个成员变量的encode函数。当成员变量是普通成员变量时,将其视为长度为1的定长数组。如果是primitive类型,直接调用lcm_coretypes中的encode函数。当它是数组时,将会通过N层循环encode每一个数组元素。
生成getEncodedSize函数定义
getEncodedSize函数是输出序列化之后的数据的大小,它由emit_encoded_size生成,就是计算每个数组的大小求和,它会调用每个成员变量的getEncodedSize并求和。
生成decode函数定义
它由emit_decode生成。先decode哈希值,如果哈希值和实时计算的哈希值不吻合,报错。顺序对每一个成员变量进行decode。实现细节和encode类似,逻辑上并不复杂,只是生成数组decode的代码稍显复杂。
生成getHash函数定义
它由emit_get_hash生成。如果内部没有非初等对象,直接用语法解析阶段生成的哈希值来生成新的哈希值。如果内部有非初等对象,递归调用每个非初等对象的哈希函数。这里就有一个问题,如果非初等变量内部包含了结构体本身,这会造成无限循环,它采用了一个巧妙的技巧,就是每次hash时都查一下它的父节点的哈希函数是否和当前结构体的哈希函数一样,如果一样就返回零。
总结
lcmgen结构清晰,代码可读性高,小巧玲珑,值得一看。LCM Type Language的优点是strong typed,这一点比protobuffer强,但是缺乏版本控制、向前向后兼容性。
代码产生案例
假设我们有如下LCM Type:
package myspace.types;
struct temperature_t
{
const int64_t const_value = 999;
string str;
int64_t utime;
int32_t size;
foonamespace.Foo foo[size][2];
float point[3];
Bar bar[2];
}
使用./lcm-gen -x temperature.lcm编译后,产生myspace/types/temperature_t.hpp:
/** THIS IS AN AUTOMATICALLY GENERATED FILE. DO NOT MODIFY
* BY HAND!!
*
* Generated by lcm-gen
**/
#ifndef __myspace_types_temperature_t_hpp__
#define __myspace_types_temperature_t_hpp__
#include <lcm/lcm_coretypes.h>
#include <string>
#include <vector>
#include "foonamespace/Foo.hpp"
#include "myspace/types/Bar.hpp"
namespace myspace
{
namespace types
{
class temperature_t
{
public:
std::string str;
int64_t utime;
int32_t size;
std::vector< std::vector< foonamespace::Foo > > foo;
float point[3];
myspace::types::Bar bar[2];
public:
// If you're using C++11 and are getting compiler errors saying
// things like ‘constexpr’ needed for in-class initialization of
// static data member then re-run lcm-gen with '--cpp-std=c++11'
// to generate code that is compliant with C++11
static const int64_t const_value = 999LL;
public:
/**
* Encode a message into binary form.
*
* @param buf The output buffer.
* @param offset Encoding starts at thie byte offset into @p buf.
* @param maxlen Maximum number of bytes to write. This should generally be
* equal to getEncodedSize().
* @return The number of bytes encoded, or <0 on error.
*/
inline int

最低0.47元/天 解锁文章
1462

被折叠的 条评论
为什么被折叠?



