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-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) 可用來避免產生型別規格。