Protobuf第三方扩展开发指南

谷歌Protobuf目前最新3.6.1版本官方支持许多语言:C++、Java、Python、Go等等。除官方支持外这里还列出了许多开源的第三方扩展。

如果官方不支持自己的语言,各种开源库也不能满足需求,那么就需要自己动手编写第三方扩展来支持目标语言。

本文介绍如何为Protobuf编写第三方扩展。注意:

  • 本文不讨论谷歌RPC
  • 本文假设读者已经了解Protobuf的基本用法,如果你还不会用那么本文不适合你阅读

第1章 了解Protobuf

Protobuf分为编译器和运行时两个部分。本章介绍Protobuf是如何工作的。

1.1 Protobuf是如何运行的

通常我们编写好xxx.proto文件后需要:

  • 编译:将proto文件编译生成目标码,不同目标语言的目标码形式都不相同。
    以Python为例,protoc --python_out=/path/to/output/ xxx.proto会生成xxx_pb2.py
    protoc可以在GitHub的release页面下载,例如:protoc-3.6.1-linux-x86_64.zip。
  • 运行:将生成的目标码拷贝至目标机运行,要求目标机上有对应的运行时。
    以Python为例,Python应用程序中可以直接调用:import xxx_pb2
    运行需要依赖Protobuf的Python运行时,可以在release页面下载,例如:protobuf-python-3.6.1.tar.gz。

图1-1 显示了Protobuf是如何工作的,也有的时候编译、运行是在同一个机器上进行的。


图1-1 Protobuf工作流程

1.2 编写第三方扩展的几种方案

读到这应该也猜到了,我们需要编写编译器和运行时两个部分。运行时没什么好说的,可以参考后续的Demo。编译器分为前端和后端两个部分,见图1-2。

图1-2 编译器前端、后端、目标码

谷歌已经实现了编译器前端,在此基础上我们只需要编写编译器后端即可:

  • 方案一:在谷歌CommandLineInterface接口的基础上进一步开发,该接口封装了Protobuf编译器前端,在此基础上你可以轻松实现编译器后端;你必须使用C++语言开发后端;
  • 方案二:编写插件来实现,Protobuf 3.0开始支持插件,这是目前最推荐的方式;
  • 方案三:自己编写编译器前端和后端,如果没有特殊需求的话非常不推荐这种方式,详见下文;

第2章 三种方案快速入门

2.1 方案一:通过C++接口实现编译器后端(不推荐)

早期Protobuf 2不支持插件,因此一些较老的开源项目是使用此方案,如:

  • protobuf-c

Protobuf 3开始已经支持插件了,我们推荐使用插件实现,本节的技术点算是过时了。因此这一节的内容被我移到这篇文章了。

2.2 方案二:通过编写插件实现编译器后端(推荐)

编译器后端无非就是获取proto语法树,然后进行生成。在方案一中,语法树是通过C++的一个类来表达的,这样就导致后端代码需要依赖谷歌C++的头文件和库,兼容性较差。

而方案二是通过proto数据流来表达语法树的,后端只要依赖相应语言的Protobuf库即可。这是兼容性最好的方案。由于Protobuf官方就支持:C++、Dart、Go、Java、Python、Ruby、C#、OC、Javascript、PHP,因此无论使用上述哪种语言都可以用来开发编译器后端插件。

2.2.1 一个哲学概念

proto代码定义的是信息的模型,例如下面一段proto代码定义的就是人的信息模型:

syntax = "proto3";
package demo;

message Person {
    string name = 1;
    int32 id = 2;
    string email = 3;
}

而这段信息模型对应的一个可能的信息内容是:

张三, 112233, zhangsan@163.com

这样我们就知道信息模型是信息内容的抽象泛化,它们处于不同层次。就好像我们可以通过橡皮泥模子捏出各种颜色的橡皮泥。那么信息模型就好比橡皮泥模子,而信息内容就好比捏出来的橡皮泥。


image.png

然而proto代码自身也可以看做是信息,它也能有对应的模型,即:proto代码是proto代码的模型,这听起来真的很绕,自己怎么就成了自己的模型了呢。其实更详细的说应该是:有一种特殊的proto代码,它是其它任意proto代码的模型,这个特殊的proto代码就是descriptor.proto。这真的很哲学,从某角度来看descriptor.proto也是proto代码,它和普通proto代码是同一个层次的;但从另一个角度来看descriptor.proto能作为所有proto代码的模型,它又和普通proto代码不在同一个层次。

descriptor.proto的这种现象在计算机科学中叫做「元(meta)」,例如:

  • 在Python中我们用一个类来定义其它类,这叫元类
  • 在Lua中我们用一个表来定义其它表,这叫元表
  • 类似的词汇你可能还听过很多,例如:元编程

几乎毫无例外的,各个技术领域出现的「元」的概念都成为最难理解的知识点之一,元本身的概念超出了本文范围。

2.2.2 插件的运行流程

在执行protoc编译的时候,命令行是这样写的:protoc -python_out=./ *.proto,Protobuf原生支持Python所以认识-python_out,这表示把当前目录下所有proto都编译成python。如果我们命令行这样写:protoc -xxx_out=./ *.proto,由于不认识xxx,于是protoc会在PATH路径下寻找一个叫做protoc-gen-xxx的可执行文件。而protoc-gen-xxx就是我们要实现的插件。

插件的运行流程如图:

  • 我们只需要关注步骤3.1~3.5,其余步骤是谷歌protoc完成
  • CodeGeneratorRequest和CodeGeneratorResponse对象定义在plugin.proto里面
  • FileDescriptor代表了一个proto文件,定义在descriptor.proto里面
image.png

2.2.3 插件具体实现代码

本章概述就说了插件可以通过多种语言(Python、Java、C#等)实现,这里我们以Python为例。实现涉及到的主要类为:

  • CodeGeneratorRequest对应:from google.protobuf.compiler.plugin_pb2 import CodeGeneratorRequest
  • CodeGeneratorResponse对应:from google.protobuf.compiler.plugin_pb2 import CodeGeneratorResponse
  • FileDescriptor对应:from google.protobuf.descriptor import FileDescriptor

一个简单的Demo代码如下,将代码命名为protoc-gen-hello,赋予可执行权限,然后放置到PATH路径下:

#!/usr/bin/env python3
import sys
from google.protobuf.compiler import plugin_pb2

# 3.1 读取二进制
input_data = sys.stdin.buffer.read()

# 3.2 反序列化
req = plugin_pb2.CodeGeneratorRequest.FromString(input_data)

# 3.3 根据业务需求具体实现
# req.proto_file[0]是FileDescriptor类型的对象
# 正常业务肯定要通过req.proto_file[0]读取proto代码的信息,然后根据具体业务需求解析
# 但这段代码只是为了简单演示,就不读取了

# 3.4 构造CodeGeneratorResponse对象
# 下面这段逻辑说明,不管输入的proto如何
# 本插件都会输出aaa.txt和bbb.txt文件,文件内容都是hello
resp = plugin_pb2.CodeGeneratorResponse()
resp.file.add()
resp.file[0].name = 'aaa.txt'
resp.file[0].content = 'hello'
resp.file.add()
resp.file[1].name = 'bbb.txt'
resp.file[1].content = 'hello'

# 3.5 将CodeGeneratorResponse序列化成二进制后打印
sys.stdout.buffer.write(resp.SerializeToString())

为了运行这段代码,我们的命令是:protoc --hello_out=./ *.proto,这样protoc会去寻找一个叫做protoc-gen-hello的可执行文件。

2.3 方案三:自己编写编译器前端、后端

此法风险和难度比较大:

  • 需要开发者熟练掌握编译原理
  • 谷歌并没有对此方案提供任何技术支持
  • Protobuf有一些隐含语法,这部分并没有在官方文档中说明,让此方案的兼容性得不到保障

如果有特殊需求才考虑此法:

  • 有proto文件热加载需求,即希望应用能够直接加载proto文件

目前也有一些开源库使用此方案:

2.4 方案对比

方案一只能通过C++实现。如果不是遗留项目,方案一没有什么优势。

方案二可选开发语言多样,推荐使用。

方案三难度最大,基本只有特殊需求、学习研究会考虑此法。

第三章 Protobuf插件开发详解

在2.2节中已经介绍了基本内容,因为从Protobuf 3开始这是最常用的方法,所以这里花一个章节的篇幅详细介绍。

本章以Python语言为例编写一个简易的Protobuf插件,该插件的名字是protoc-gen-lint,用来检测proto代码是否有潜在问题。我们知道一个成熟的lint工具检测内容是非常多的,甚至包括英语单词拼写是否正确,但是本章作为教学例子,只做了非常有限的几个功能。

本章例子的检测的内容是:

  • 判断message名是否为驼峰命名法,即不能含有下划线、不能有两个连续大写字母出现
  • 等等

3.1 代码如下

#!/usr/bin/env python3
import sys
from google.protobuf.compiler import plugin_pb2

input_data = sys.stdin.buffer.read()
req = plugin_pb2.CodeGeneratorRequest.FromString(input_data)
resp = plugin_pb2.CodeGeneratorResponse()

for f in req.proto_file:
    log = ''
    for m in f.message_type:
        if '_' in m.name:
            # 判断是否存在下划线
            log += '{0}中的{1}不符合驼峰命名法\n'.format(f.name, m.name)
            break
        else:
            # 判断是否存在连续的大写字母
            for i in range(len(m.name) - 1):
                if m.name[i].isupper() and m.name[i+1].isupper():
                    log += '{0}中的{1}不符合驼峰命名法\n'.format(f.name, m.name)
                    break
    if log != '':
        # 若确实存在问题,则将报错信息输出到.lint.txt后缀的文件中
        resp.file.add()
        resp.file[-1].name = f.name.rstrip('.proto') + '.lint.txt'
        resp.file[-1].content = log

sys.stdout.buffer.write(resp.SerializeToString())

附录

附录:官方支持语言列表

附录:供参考的开源项目

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容