« Windows 下调试问题一则 | 返回首页 | 继续完善 protobuf 库 »

Proto Buffers in Lua

Google 的 Jeff Dean 同学说,设计分布式系统一定要有 Protocol Description Language

Google Proto Buffers 的意义在于,定义了一个不错的 PDL 。protobuffers 的实现反而不那么重要了。

这几天我一直在倒腾 lua 下的 proto buffers 的支持。一直在思考,怎样的接口才是最适合 lua 使用的。

大多数语言下的 proto buffers 实现,都是将编码的数据块展开成本地语言的数据结构。对于 C/C++ ,这是最高效的形式。但对于动态语言,那就未必了。虽然 google 为 python 做的 proto buffers 的官方实现也是如此,但我依然想考虑一下,是否有更高效的方式来做这件事。

lua ,最高效的数据处理交换是利用语言的栈。虽然 table 也足够快,但复杂且深层次的 table 往往容易成为最终的性能瓶颈。btw, google 的 js V8 引擎,在部分场合表现的比 lua jit 更为出色,很大程度上是因为它极大的加快了 table 的查询。这几乎会成为所有解释型动态语言的性能热点。而且不断了生成临时 table ,还会给 gc 造成较大的负担。那么,能不能找到一种方式可以回避在 lua 中构造复杂数据结构,而又能方便的使用由 proto buffers 定义出来的结构化数据呢?

我花了三四天初步实现了一个 lua proto buffers 的库,全部是以 C 代码完成的。最终提供给 lua 的接口只有四个。

  1. load 能加载一个 proto buffers 的元数据,对协议本身做一个描述。并生成一个 C 对象 (userdata) 返回到 lua state 中。这个 C 对象包含了一组 proto 描述信息,除了其中的字符串信息外,全部储存在一块连续的内存中。作为 lua 的 userdata ,不需要任何 gc 方法。其中引用的字符串全部记录在这个 userdata 的环境表中,在 C 对象里只记录字符串表里的序号。这个方式可以很大的提高 lua 运行时的字符串传递性能。

  2. pattern 可以为某个 message 生成一个模式对象 (还是 C 对象),这个模式对象可以用来解码或编码数据。pattern 的概念是整个库的核心概念。库会依据 pattern 来编码或解码数据。pattern 本身的生成比较重量,但在实际应用中,往往是一次性的,而不需要对每个数据包处理时都重新生成 pattern 。做一些小手脚,就可以在 lua 层面做进一步的简单封装。用字符串的形式来 cache 并索引 pattern 。

  3. unpack 可以对一个数据块进行解包工作。unpack 必须提供数据块以及相应的 pattern 对象。它会按 pattern 的指示把结构化数据解到栈上。这种方式回避了每次解包都生成临时的 table 。当然,如果使用者还是喜欢构造一个 table 来表达数据块中的数据结构的话,依然可以通过几行 lua 封装代码来办到。

  4. pack 可以根据一个 pattern 对数据块打包。同样从栈接收数据源。不需要构造临时的 table 用于打包。


我在做这个库之前,做了一些工作来解析 Google Proto Buffers 定义的协议描述文件。如上所说,我认为,协议定义及描述是整个 Proto Buffers 的核心,而其实现以及二进制编码规则则是次要的东西。

google 提供了开源工具,把文本协议描述文件编译成 proto buffers 自身形式的结构化数据。这做了很漂亮,也方便了对协议本身的解析。不过若是抛开二进制编码,若想让整套协议自举,而整个过程和具体编码形式无关的话,更有趣的做法是自己来实现文本协议描述的解析。

好在用 lua 来做这项工作并不太难,proto buffers 定义的语言也相当简单,容易解析。我大约写了 100 来行 lua 代码来完成基本的语义分析。当然,用了强大的 lpeg 库。话说,PEG 是个相当强大的东西,强烈推荐没学过的同学们好好学习一下。用顺手的话,为自己的项目定义 DSL 那是易如反掌。

整个解析的实际运行过程非常繁重,但最终只是生成一个很小的 C 数据对象。我用了一个有趣的方案来干这件事:单独开一个 lua State 来完成协议的解析。这个独立的 lua State 总共所消耗的内存可以事先预估,不会太大(跟 proto 文件的规模以及复杂度有关)。所以我为这个独立的 State 定制了一个只分配不释放的内存管理器。这个内存管理器工作在独立的系统内存页上,方便最后一次回收。这样,每次解析一个协议文件,只需要消耗一些临时内存,而在解析完后,能够绝对干净的清除痕迹。我统计了一下一个最终大约 70K 的 C 数据对象,处理过程消耗了大约 1.5M 内存。

灵活使用多个 lua State 应该是一个良好的习惯。尤其在较大规模的需要长期运行程序中,把各个模块分配到独立的 lua State 空间中运行,可以使任务完成后,内存得到准确的回收。


这几天做的这个东西,还很初步,所以就不细写了。等成熟后再考虑开源。

ps. 在 lua 社区,设计些小的模式语言来完成数据处理,似乎是一个很流行的做法。lua 自带的字符串匹配如此;cosmo 也是个有趣的小玩意;lpeg 也提供一个 The re Module 来模拟正则表达式的语法。等等这些启发我设计出这个 proto buffers 的 pattern 。

Comments

每次看云风大神文章都有收获,完了再一看发表时间7年前
sprotoloader.lua 正是使用Lpeg,强大的工具。
求开源,求教育!
talk is cheap
最近也在找protobuf的lua实现,求开源T_T
麻烦开源
Google Proto Buffers 的意义在于,定义了一个不错的 PDL 。 c++ 方面 有一些细节问题就是 Google Proto Buffers 是否是和xml一样自描述的,如果是自描述的,那么接收到序列化的字符串,以后反序列就能得到相应的pro定义,和相对应的lass 类 举个例子 比如 client server server 开始已已经定义的a class 传输,client 可以理解。 当server 新定义了class b 然后传输给client ,client 也可以解析这个定义,至少是可以实例化成b 这样一个对象。c++ 里面生成未定义的类 这个该如何实现呢?
虽然没看懂。不太了解动态语言的飘过,难道php
http://www.leyuan100.com/
支持
看不大懂
google codes老是被敝,没有意思
佩服
PEG的速度不好, 如果用不好,速度更差, 嘿嘿
看了一下PEG的介绍,已经有好多种语言都有支持的库了。很强大啊!
请问到底什么是模式语言?举个例子呗
赞,虽然没看懂。不太了解动态语言的飘过,难道php也可以这么做所?

Post a comment

非这个主题相关的留言请到:留言本