LCM类型定义语言
LCM1类型定义语言
LCM类型定义语言的使用和特性。
简介
除了提供通信原语集合,LCM还包括产生平台无关的数据类型的编组和解组函数的工具。它类似于XDR,但是它的目标是更大的类型安全性,以及对C、Java和Python等多种语言的一流支持。本文档描述了数据编组功能;通信功能在其他地方有描述。注意,可以独立于LCM的通信功能使用LCM的数据编组功能。
设计目标
LCM编组功能的主要设计目标是:
- 提供一个简单的机制来定义复杂的类型,这对于C和Java的用户来说是非常舒适的
- 提供对各种客户端语言的原生支持
- 提供平台相关细节的抽象机制,如字节顺序
- 最大化编译与运行的类型安全
- 能够识别类型不兼容的消息,例如当两个应用程序有不同版本的相同数据类型时
- 产生空间效率高的编码消息
- 减小编码和解码的计算成本
当前版本的LCM只有少数的妥协来实现这些目标。在某些情况下,只能用取最小公约的方式来确保所有的平台支持LCM提供的特性。
类型定义
类型定义包含在以".lcm"为后缀的文件中。它们通常使用小写字母和下划线来命名:例如,类型"wind_speed_t"定义在文件"wind_speed_t.lcm"中。工具lcm-gen将LCM类型定义转换为特定语言的实现。
结构体
LCM结构体是由其他类型组成的复合类型。我们从一个简单的结构体开始,它叫做"temperature_t",它包含一个名为"utime"的64位整数和一个名为"degCelsius"的64位浮点数。下面的例子中也展示了两种类型的注释。
struct temperature_t
{
int64_t utime; // Timestamp, in microseconds
/* Temperature in degrees Celsius. A "float" would probably
* be good enough, unless we're measuring temperatures during
* the big bang. Note that the asterisk on the beginning of this
* line is not syntactically necessary, it's just pretty.
*/
double degCelsius;
}
这些声明必须出现在一个名为temperature_t.lcm的文件中。
LCM类型不包含指针(但支持数组,见下文):这消除了循环引用的可能性。
在进一步讨论之前,让我们先看看可用的各种基本类型。
基本类型
LCM支持以下几种基本类型:
type | Description |
---|---|
int8_t | 8-bit signed integer |
int16_t | 16-bit signed integer |
int32_t | 32-bit signed integer |
int64_t | 64-bit signed integer |
float | 32-bit IEEE floating point value |
double | 64-bit IEEE floating point value |
string | UTF-8 string |
boolean | true/false logical value |
byte | 8-bit value |
整型都是有符号的(这是必要的,以确保与缺少无符号类型的Java进行简单的交互),并以网络字节顺序编码。
类型byte
在C/C++中表示为uint8_t
。具有本机byte
表示的语言使用其各自的本机字节表示(例如,在Java中的类型byte
)。
浮点类型使用IEEE 32和64位格式进行编码。LCM实现可能不使用任何其他编码。32和64位量以网络字节顺序传输。
类型boolean
被编码为一个字节,其值为0或1。N个布尔值的数组将需要N个字节。
类型string
编码一个以NULL结尾的UTF-8字符串。字符串作为一个32位整数发送,该整数包含字符串的总长度(以字节为单位,包括终止的NULL字符),后续紧跟字符串本身的字节(同样包括NULL字符)。
数组
LCM支持由基本类型、结构体或常量声明组成的多维数组。数组的维数由LCM类型声明声明:您不能对包含变量维数组的LCM类型进行编码。相比之下,变量大小的数组是可以的。看看下面的例子:
struct point2d_list_t
{
int32_t npoints;
double points[npoints][2];
}
这个例子展示了一个由变长和固定长度组件组成的二维数组声明。在变长声明中,包含长度的变量必须在其用作数组长度之前声明。还要注意,长度变量(在上面的例子中为npoints)必须是整数类型,并且必须始终大于或等于零。
当数组被编码和解码时,每个维度的大小已知:它是一个常量(由LCM类型声明给出),或者它是一个先前编码/解码的变量。因此,数组通过递归地编码数组的每个元素来编码,其中最内层的维度一起编码。换句话说,上面的数组将按顺序编码为 points[0][0],points[0][1],points[1][0],points[1][1],points[2][0],points[2][1],等等。
常量
LCM提供了一种简单的方法来声明可以随后用于填充其他数据字段的常量。用户可以自由地以任何他们选择的方式使用这些常量:作为魔术数字、枚举或位域。
常量可以通过使用const关键字声明。
struct my_constants_t
{
const int32_t YELLOW=1, GOLDENROD=2, CANARY=3;
const double E=2.8718;
}
注意,必须为常量声明类型。支持所有整数和浮点类型。不支持字符串常量。
命名空间
LCM允许在命名空间中定义类型,使用户能够更容易地使用来自其他组织的类型,即使这些类型具有相同的名称。命名空间机制与Java的机制非常类似。在支持命名空间(如Java和Python)的语言中,LCM命名空间机制映射到本机机制。在像C这样的语言中,命名空间被包名前置到类型名称来近似。
有关命名空间的例子,请参见下文。请注意,package关键字标识该文件中定义的结构的命名空间,并且通过在两者之间加上一个句点来连接包和类型名称来形成完全限定类型。
package mycorp;
struct camera_image_t {
int64_t utime;
string camera_name;
jpeg.image_t jpeg_image;
mit.pose_t pose;
}
鼓励LCM用户将其类型放入唯一的命名空间,并完全限定所有成员字段的类型。
性能考虑
使用LCM编码和解码的运行时成本通常不是系统瓶颈。Marshalling函数比XML实现快得多,但是由于每个成员都必须单独处理(例如,为了确保正确的字节顺序),因此LCM比使用原始C结构更昂贵。LCM的第一个应用程序使用了超过40MB/s。
指纹计算
指纹确保编码和解码方法同意数据类型的格式。指纹是一个递归函数,它是一个类型包含的所有类型的函数。这会产生一个潜在的问题,当类型可能相互递归时:我们必须避免无限递归。
基本思想是,每种类型都有一个“基本”指纹,我们将为类型“A”表示为“K_A”。K_A是从lcm类型描述派生的常量(并且它存储为lcm_struct->hash)。我们希望计算实际的指纹(或哈希),A(),它是A包含的所有类型的函数。
此外,为了能够识别递归,A()函数需要一个参数,该参数是已访问类型的列表。例如,C([A,B])表示我们希望计算类型C的哈希值,给定C是类型B的成员,类型B是类型A的成员。如果[list]包含C,则通过将C([list]) = 0来避免递归。
原始类型的贡献是通过K_A处理的;它们没有递归。
上述定义中出现了一个小问题:如果类型A,B和C相互递归,我们可能会有两种类型具有相同的哈希值。这显然是不可取的。我们通过使递归的顺序相关来解决这个问题:在树中的每个节点上,我们将值(按位)向左旋转1位。在递归深度N处包含的类型的贡献被旋转N位。
值得注意的是,对于枚举(它们不能包含其他类型),这种机制是完全不必要的;对于枚举,我们只是使用lcmenum->hash中的哈希值。
伪代码:
v = compute_hash(type, parents)
if type is member of parents
return 0
v = K_type;
for each members m of type
v += compute_hash(m, [parents, type])
return rot_left(v);
当编码/解码类型T时,我们将使用compute_hash(T, [])作为哈希函数。
例子:
struct A
{
B b;
C c;
}
struct B
{
A a;
}
struct C
{
B b;
}
从图上看,我们可以通过显示每个分支的子级来计算它们的哈希值。我们使用小写字母来表示终端叶子(其中叶子与其父级之一的类相同)。
A B C
/ \ | |
B C A B
| | / \ |
a B b C A
| | / \
a b b c
A() = R{K_A + R{K_B}} + R{K_C + R{K_B}}
B() = R{K_B + R{K_A + R{K_C}}}
C() = R{K_C + R{K_B + R{K_A}}}
值得注意的是,没有旋转,B() == C()。
实现
计算指纹的算法很简单:
- 计算结构的基本哈希
- 在基本哈希计算中包括字段名。
- 如果类型是原始类型,则在基本哈希计算中包括其类型名。
- 如果类型不是原始类型,则不要在基本哈希计算中包括它(如上所述)。 它将包含在递归计算中。
- 递归地计算结构的指纹
- 这将递归进入非原始结构类型,使用它们的基本哈希。
下面是C语言的固定引用实现,用于解析侧,生成本地结构哈希(非递归)以及C绑定类型(其中计算是递归完成的):
- https://github.com/lcm-proj/lcm/blob/v1.4.0/lcmgen/lcmgen.c#L233-L267
- https://github.com/hoxovic/lcm/blob/v1.4.0/lcmgen/emit_c.c#L390-L439
当然,这也应该与其他语言的实现一致。
相关工作
LCM与XDR最为相似,XDR用于RPC并由RFC4506描述。两者都使用类C语法(甚至包括C关键字,如“结构”)。 LCM的不同之处在于它的语言更小:不支持不常用的功能,如联合。 LCM不支持指针:这可以消除在XDR中可能出现的指针追踪问题。 LCM以更自然的方式支持可变长度数组,并且LCM在编码数据中包含类型“签名”。 此类型签名允许运行时错误检测。
数据编码表示通常会被与XML进行比较。 XML和LCM用于非常不同的功能。 XML的冗长性和通用结构有助于代理使用它们理解的信息,同时安全地跳过对它们不熟悉的属性。 相比之下,LCM旨在于紧密耦合的代理,但它们可能不在同一内存空间中。 更加严格的类型定义,以及空间效率和计算效率高的编码,更适合于这些类型的应用。
开发历史
LCM的marshalling功能是为MIT的DARPA城市挑战赛车辆而创建的,开发工作开始于2006年的夏天。早期版本支持了很多现在已经被废弃的功能:减少了一些不必要的功能,这大大简化了代码库,因为大多数功能通常会影响到多种语言的后端(目前是C、Java和Python)。
LCM,Lightweight Comunication Marshalling,是一个用于通信和数据编组的库。Marshalling的中文意思是“装配”,这里指的是将数据结构转换为字节流的过程。 ↩︎