gpb 是 Erlang 的 Google 协议缓冲区定义文件的编译器。
快捷方式:API 文档 ~ hex.pm 上的 gpb
假设我们有一个 protobuf 文件x.proto
message Person {
required string name = 1 ;
required int32 id = 2 ;
optional string email = 3 ;
}
我们可以通过多种不同的方式为此定义生成代码。这里我们使用命令行工具。有关与钢筋集成的信息,请参阅下文。
# .../gpb/bin/protoc-erl -I. x.proto
现在我们有了x.erl
和x.hrl
。首先我们编译它,然后我们可以在 Erlang shell 中尝试一下:
# erlc -I.../gpb/include x.erl
# erl
Erlang/OTP 19 [erts-8.0.3] [source] [64-bit] [smp:12:12] [async-threads:10] [kernel-poll:false]
Eshell V8.0.3 (abort with ^G)
1> rr("x.hrl").
['Person']
2> x:encode_msg(#'Person'{ name = " abc def " , id = 345 , email = " [email protected] " }).
<< 10 , 7 , 97 , 98 , 99 , 32 , 100 , 101 , 102 , 16 , 217 , 2 , 26 , 13 , 97 , 64 , 101 ,
120 , 97 , 109 , 112 , 108 , 101 , 46 , 99 , 111 , 109 >>
3 > Bin = v ( - 1 ).
<< 10 , 7 , 97 , 98 , 99 , 32 , 100 , 101 , 102 , 16 , 217 , 2 , 26 , 13 , 97 , 64 , 101 ,
120 , 97 , 109 , 112 , 108 , 101 , 46 , 99 , 111 , 109 >>
4 > x : decode_msg ( Bin , 'Person' ).
# 'Person' { name = " abc def " , id = 345 , email = " [email protected] " }
在 Erlang shell 中, rr("x.hrl")
读取记录定义, v(-1)
引用历史记录中前一步的值。
原型缓冲区类型 | Erlang类型 |
---|---|
双,浮动 | 浮动()|无穷大| '-无穷大' |南 编码时,也接受整数 |
int32、int64 uint32、uint64 sint32、sint64 固定32、固定64 sfixed32、sfixed64 | 整数() |
布尔值 | 真实 |错误的 编码时,整数 1 和 0 也被接受 |
枚举 | 原子() 未知枚举解码为整数() |
信息 | 记录(因此 tuple()) 或 map() 如果指定了映射 (-maps) 选项 |
细绳 | unicode 字符串,即整数列表 或 binary() 如果指定了 strings_as_binaries (-strbin) 选项 编码时,iolists 也被接受 |
字节 | 二进制() 编码时,iolists 也被接受 |
其中一个 | {选择的字段名称,值} 或ChosenFieldName => Value(如果指定了 {maps_oneof,flat} (-maps_oneof flat) 选项) |
地图<_,_> | 2 元组的无序列表, [{Key,Value}] 或一个map(),如果指定了maps (-maps)选项 |
重复的字段表示为列表。
可选字段表示为值或undefined
(如果未设置)。但是,对于映射,如果选项maps_unset_optional
设置为omitted
,则未设置的可选值将从映射中省略,而不是在编码消息时设置为undefined
。解码消息时,即使将maps_unset_optional
设置为omitted
,也会在解码的映射中设置默认值。
message m1 {
repeated uint32 i = 1 ;
required bool b = 2 ;
required eee e = 3 ;
required submsg sub = 4 ;
}
message submsg {
required string s = 1 ;
required bytes b = 2 ;
}
enum eee {
INACTIVE = 0 ;
ACTIVE = 1 ;
}
# m1 { i = [ 17 , 4711 ],
b = true ,
e = 'ACTIVE' ,
sub = # submsg { s = " abc " ,
b = << 0 , 1 , 2 , 3 , 255 >>}}
% % If compiled to with the option maps:
#{ i => [ 17 , 4711 ],
b => true ,
e => 'ACTIVE' ,
sub => #{ s => " abc " ,
b => << 0 , 1 , 2 , 3 , 255 >>}}
message m2 {
optional uint32 i1 = 1 ;
optional uint32 i2 = 2 ;
}
# m2 { i1 = 17 } % i2 is implicitly set to undefined
% % With the maps option
#{ i1 => 17 }
% % With the maps option and the maps_unset_optional set to present_undefined:
#{ i1 => 17 ,
i2 => undefined }
该结构首次出现在 Google protobuf 2.6.0 版本中。
message m3 {
oneof u {
int32 a = 1 ;
string b = 2 ;
}
}
oneof 字段自动始终是可选的。
# m3 { u = { a , 17 }}
# m3 { u = { b , " hello " }}
# m3 {} % u is implicitly set to undefined
% % With the maps option
#{ u => { a , 17 }}
#{ u => { b , " hello " }}
#{} % If maps_unset_optional = omitted (default)
#{ u => undefined } % With maps_unset_optional set to present_undefined
% % With the {maps_oneof,flat} option (requires maps_unset_optional = omitted)
#{ a => 17 }
#{ b => " hello " }
#{}
不要与 Erlang 映射混淆。该结构首次出现在 Google protobuf 版本 3.0.0 中(适用于proto2
和proto3
语法)
message m4 {
map < uint32 , string > f = 1 ;
}
对于记录,解码时项目的顺序是不确定的。
# m4 { f = []}
# m4 { f = [{ 1 , " a " }, { 2 , " b " }, { 13 , " hello " }]}
% % With the maps option
#{ f => #{}}
#{ f => #{ 1 => " a " , 2 => " b " , 13 => " hello " }}
default
选项这描述了如何解码二进制解码中不存在的可选字段。
Google protobuf 的文档说这些解码为默认值(如果指定),否则解码为字段特定于类型的默认值。 Google 的 protobuf 编译器生成的代码还包含has_
方法,因此可以检查字段是否实际存在。
然而,在 Erlang 中,设置和读取字段的自然方法是仅使用记录(或映射)的语法,这没有很好的方法来同时传达字段是否存在并读取字段默认值。
所以gpb
中的方法是你必须选择:要么或者。通常,可以查看可选字段是否存在,例如通过检查值是否undefined
。但是编译器可以选择解码为默认值,在这种情况下,您将无法查看字段是否存在。选项是defaults_for_omitted_optionals
和type_defaults_for_omitted_optionals
,分别用于解码为default=
值或特定于类型的默认值。
它的工作原理是这样的:
message o1 {
optional uint32 a = 1 [ default = 33 ];
optional uint32 b = 2 ; // the type-specific default is 0
}
给定二进制数据<<>>
,即字段a
和b
都不存在,则调用decode_msg(Input, o1)
会导致:
# o1 { a = undefined , b = undefined } % None of the options
# o1 { a = 33 , b = undefined } % with option defaults_for_omitted_optionals
# o1 { a = 33 , b = 0 } % with both defaults_for_omitted_optionals
% and type_defaults_for_omitted_optionals
# o1 { a = 0 , b = 0 } % with only type_defaults_for_omitted_optionals
最后一个替代方案可能不是很有用,但仍然可行,并且为了完整性而实施。
谷歌的参考
对于 proto3,字段既没有required
也没有default=
。相反,除非标记为optional
,否则所有标量字段、字符串和字节都是隐式可选的。在解码时,如果要解码的二进制文件中缺少这样的字段,它们总是解码为特定于类型的默认值。在编码时,只有当这些字段的值不同于特定于类型的默认值时,它们才会包含在生成的编码二进制文件中。尽管所有字段都是隐式可选的,但也可以说在概念层面上,所有此类字段始终都有一个值。在解码时,无法确定在编码时是否存在值(具有特定于类型的值)。
标记为optional
字段本质上与 proto2 语法中的表示方式相同;在记录中,如果未设置,则该字段的值为undefined
;在地图中,如果未设置,则该字段不存在。
如果您需要检测“丢失”数据,我看到的建议是定义has_
布尔字段并适当设置它们。另一种选择是使用众所周知的包装消息。
作为子消息和其中一个字段的字段没有任何特定于类型的默认值。未设置的子消息字段与设置为子消息的子消息字段的编码不同,并且解码也不同。即使子消息没有字段,这也成立。对于 oneof 字段,它的工作原理有点类似。要么没有设置任何备用 oneof 字段,要么设置其中之一。编码格式不同,解码时可以看出差异。
解析protocol buffer定义文件并可以生成:
Protocol buffer定义文件的特点:gpb支持:
packed
选项和default
选项allow_alias
枚举选项(视为始终设置为 true)oneof
(protobuf 2.6.0引入)map<_,_>
(protobuf 3.0.0引入)gpb 读取但忽略:
packed
或default
以外的选项gpb 不支持:
gpb的特点:
bytes
字段的内容,以便让运行时系统释放更大的消息二进制文件。package
属性,将包的名称添加到每个包含的消息类型(如果已定义)中,这对于避免跨包的消息类型的名称冲突很有用。请参阅use_packages
选项或-pkgs
命令行选项。get_msg_defs
函数的 gpb.hrl 中的#field{}
记录,但可以避免通过使用defs_as_proplists
或-pldefs
选项来实现此依赖性。内省
gpb 生成一些用于检查消息、枚举和服务的函数:
get_msg_defs()
(如果设置了introspect_get_proto_defs
,则为 get_proto_defs get_proto_defs()
)、 get_msg_names()
、 get_enum_names()
find_msg_def(MsgName)
和fetch_msg_def(MsgName)
find_enum_def(MsgName)
和fetch_enum_def(MsgName)
enum_symbol_by_value(EnumName, Value)
,enum_symbol_by_value_(Value)
、 enum_value_by_symbol(EnumName, Enum)
和enum_value_by_symbol_(Enum)
get_service_names()
、 get_service_def(ServiceName)
、 get_rpc_names(ServiceName)
find_rpc_def(ServiceName, RpcName)
, fetch_rpc_def(ServiceName, RpcName)
还有一些用于在完全限定名称和内部名称之间进行转换的函数。这些会考虑任何重命名选项。例如,它们对于 grpc 反射可能很有用。
fqbin_to_service_name(<<"Package.ServiceName">>)
和service_name_to_fqbin('ServiceName')
fqbins_to_service_and_rpc_name(<<"Package.ServiceName">>, <<"RpcName">>)
和service_and_rpc_name_to_fqbins('ServiceName', 'RpcName')
fqbin_to_msg_name(<<"Package.MsgName">>)
和msg_name_to_fqbin('MsgName')
fqbin_to_enum_name(<<"Package.EnumName">>)
和enum_name_to_fqbin('EnumName')
还有一些函数用于查询类型属于哪个原型。每种类型都属于某个"name"
它是一个字符串,通常是文件名,没有扩展名,例如,如果原型文件是"name.proto"
"name"
”。
get_all_proto_names() -> ["name1", ...]
get_msg_containment("name") -> ['MsgName1', ...]
get_pkg_containment("name") -> 'Package'
get_service_containment("name") -> ['Service1', ...]
get_rpc_containment("name") -> [{'Service1', 'RpcName1}, ...]
get_proto_by_msg_name_as_fqbin(<<"Package.MsgName">>) -> "name"
get_proto_by_enum_name_as_fqbin(<<"Package.EnumName">>) -> "name"
get_protos_by_pkg_name_as_fqbin(<<"Package">>) -> ["name1", ...]
还有一些版本信息函数:
gpb:version_as_string()
、 gpb:version_as_list()
和gpb:version_source()
GeneratedCode:version_as_string()
、 GeneratedCode:version_as_list()
和GeneratedCode:version_source()
?gpb_version
(在 gpb_version.hrl 中)?'GeneratedCode_gpb_version'
(在GenerateCode.hrl中)gpb 还可以生成 proto 文件的自我描述。自描述是对proto文件的描述,使用Google协议缓冲区库附带的descriptor.proto编码为二进制文件。请注意,这种编码的自我描述不会与 Google protocol buffers 编译器为同一原型生成的逐字节相同,但应该大致等效。
错误编码的 protobuf 消息和字段通常会导致解码器崩溃。此类错误编码的示例有:
地图
Gpb 可以生成地图的编码器/解码器。
选项maps_unset_optional
可用于指定不存在的可选字段的行为:它们是否从映射中省略,或者它们是否存在,但具有undefined
值,就像记录一样。
报告 .proto 文件中的错误
Gpb 不太擅长错误报告,尤其是引用错误,例如引用未定义的消息。您可能需要首先使用protoc
验证 .proto 文件是否有效,然后再将其提供给 gpb。
有关如何将 gpb 与 rebar3 结合使用的信息,请参阅 https://rebar3.org/docs/configuration/plugins/#protocol-buffers
Rebar 从 2.6.0 版本开始支持 gpb。请参阅 rebar.sample.config 文件的原型编译器部分,网址为 https://github.com/rebar/rebar/blob/master/rebar.config.sample
对于旧版本的 rebar(2.6.0 之前的版本),下面的文本概述了如何继续:
例如,将 .proto 文件放在proto/
子目录中。除了 src/ 之外的任何子目录都可以,因为 rebar 会尝试对它在 src/ 子目录中找到的任何 .proto 使用另一个 protobuf 编译器。以下是rebar.config
文件的一些行:
%% -*- erlang -*-
{pre_hooks,
[{compile, "mkdir -p include"}, %% ensure the include dir exists
{compile,
"/path/to/gpb/bin/protoc-erl -I`pwd`/proto"
"-o-erl src -o-hrl include `pwd`/proto/*.proto"
}]}.
{post_hooks,
[{clean,
"bash -c 'for f in proto/*.proto; "
"do "
" rm -f src/$(basename $f .proto).erl; "
" rm -f include/$(basename $f .proto).hrl; "
"done'"}
]}.
{erl_opts, [{i, "/path/to/gpb/include"}]}.
gpb 版本号是从与 NM 匹配的 git 最新 git 标签中获取的,其中 N 和 M 是整数。该版本被插入到 gpb.app 文件以及 include/gpb_version.hrl 中。版本是命令的结果
git describe --always --tags --match '[0-9]*.[0-9]*'
因此,要创建新版本的 gpb,获取该版本的单一来源是 git 标签。 (如果您将 gpb 导入到除 git 之外的另一个版本控制系统中,或者使用除 rebar 之外的其他构建工具,您可能必须相应地调整 rebar.config 和 src/gpb.app.src。另请参阅下面有关在git 工作树以获取有关从 git 导出 gpb 的信息。)
上面的git describe
命令的版本号看起来像
..
(在 Github 上的 master 上)..--g
(在分支上或版本之间) Github 上的 gpb master 分支上的版本号始终是带点的整数,以便与 reltool 兼容。换句话说,每次推送到 Github 的 master 分支都被视为一个版本,并且版本号会增加。为了确保这一点,在 helpers 子目录中有一个pre-push
git hook 和两个脚本install-git-hooks
和tag-next-minor-vsn
。 ChangeLog 文件不一定反映所有次要版本的变更,仅反映重要的更新。
制作新版本时需要更新的地方:
gpb 构建过程需要一个带有标签的(非浅层)git 工作树,以获得正确的版本编号,如版本编号部分中所述,但也可以在 git 之外构建。为此,您有两种选择:
gpb.vsn
手动设置版本,版本位于第一行helpers/mk-versioned-archive
脚本创建版本化存档,然后解压存档并在其中构建。如果您在 git 工作树中创建版本化存档,版本将自动设置,否则您将需要手动指定它。运行mk-versioned-archive --help
以获取有关使用哪些选项的信息。
从 Github 下载时, gpb-
如果您使用 Github 的自动源代码zip 或 tar.gz 存档,则需要如上所述创建gpb.vsn
文件,或者使用mk-versioned-archive
脚本和--override-version=
重新创建版本化存档--override-version=
选项(如果目录名称包含正确的版本,则可能是 或--override-version-from-cwd-path
选项。)
欢迎贡献,最好是 Pull 请求、git 补丁或 git fetch 请求。以下是一些指导方针:
rebar clean; rebar eunit && rebar doc
有关详细信息,请参阅变更日志。
maps_unset_optional
选项的默认值已从present_undefined
更改为omitted
,这仅涉及使用maps (-maps) 选项生成的代码。已明确设置此选项的项目不受影响。依赖默认值present_undefined
的项目需要显式设置该选项才能升级到 4.0.0。
对于类型规范,默认值已更改为尽可能生成它们。选项{type_specs,false}
(-no_type) 可用于避免生成类型规范。