Der GPB ist ein Compiler für Google-Protokollpufferdefinitionsdateien für Erlang.
Verknüpfungen: API-Dokumentation ~ gpb auf hex.pm
Nehmen wir an, wir haben eine Protobuf-Datei, x.proto
message Person {
required string name = 1 ;
required int32 id = 2 ;
optional string email = 3 ;
}
Wir können Code für diese Definition auf verschiedene Arten generieren. Hier verwenden wir das Befehlszeilentool. Informationen zur Integration mit Bewehrungsstäben finden Sie weiter unten.
# .../gpb/bin/protoc-erl -I. x.proto
Jetzt haben wir x.erl
und x.hrl
. Zuerst kompilieren wir es und dann können wir es in der Erlang-Shell ausprobieren:
# 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] " }
In der Erlang-Shell liest rr("x.hrl")
Datensatzdefinitionen und v(-1)
verweist auf einen Wert einen Schritt früher im Verlauf.
Protobuf-Typ | Erlang-Typ |
---|---|
doppelt, schweben | float() | Unendlichkeit | '-unendlich' | Nan Bei der Kodierung werden auch ganze Zahlen akzeptiert |
int32, int64 uint32, uint64 sint32, sint64 fest32, fest64 sfixed32, sfixed64 | ganze Zahl() |
bool | wahr | FALSCH Bei der Kodierung werden auch die Ganzzahlen 1 und 0 akzeptiert |
Aufzählung | Atom() Unbekannte Enumerationen werden in Integer() dekodiert |
Nachricht | Datensatz (also tuple()) oder map(), wenn die Option „maps“ (-maps) angegeben ist |
Zeichenfolge | Unicode-String, also Liste von Ganzzahlen oder binär(), wenn die Option strings_as_binaries (-strbin) angegeben ist Bei der Kodierung werden auch Iolisten akzeptiert |
Bytes | binär() Bei der Kodierung werden auch Iolisten akzeptiert |
einer davon | {ChosenFieldName, Wert} oder ChosenFieldName => Wert , wenn die Option {maps_oneof,flat} (-maps_oneof flat) angegeben ist |
Karte<_,_> | Eine ungeordnete Liste von 2-Tupeln, [{Key,Value}] oder eine map(), wenn die Option „maps“ (-maps) angegeben ist |
Wiederholte Felder werden als Listen dargestellt.
Optionale Felder werden entweder als Wert oder undefined
dargestellt, wenn sie nicht festgelegt sind. Wenn jedoch für Karten die Option maps_unset_optional
auf omitted
gesetzt ist, werden nicht gesetzte optionale Werte in der Karte weggelassen, anstatt beim Kodieren von Nachrichten auf undefined
gesetzt zu werden. Beim Dekodieren von Nachrichten wird der Standardwert in der dekodierten Karte festgelegt, auch wenn maps_unset_optional
auf omitted
gesetzt ist.
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 }
Dieses Konstrukt erschien erstmals in Google Protobuf Version 2.6.0.
message m3 {
oneof u {
int32 a = 1 ;
string b = 2 ;
}
}
Ein oneof-Feld ist automatisch immer optional.
# 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 " }
#{}
Nicht zu verwechseln mit Erlang-Karten. Dieses Konstrukt erschien erstmals in Google Protobuf Version 3.0.0 (sowohl für die proto2
als auch die proto3
Syntax).
message m4 {
map < uint32 , string > f = 1 ;
}
Bei Datensätzen ist die Reihenfolge der Elemente beim Dekodieren nicht definiert.
# m4 { f = []}
# m4 { f = [{ 1 , " a " }, { 2 , " b " }, { 13 , " hello " }]}
% % With the maps option
#{ f => #{}}
#{ f => #{ 1 => " a " , 2 => " b " , 13 => " hello " }}
default
Hier wird beschrieben, wie die Dekodierung für optionale Felder funktioniert, die in der zu dekodierenden Binärdatei nicht vorhanden sind.
In der Dokumentation für Google protobuf heißt es, dass diese auf den Standardwert dekodiert werden, sofern dieser angegeben ist, oder auf den typspezifischen Standardwert des Felds. Der vom Protobuf-Compiler von Google generierte Code enthält auch has_
-Methoden, sodass man prüfen kann, ob ein Feld tatsächlich vorhanden war oder nicht.
Allerdings besteht in Erlang die natürliche Art und Weise, Felder festzulegen und zu lesen, darin, einfach die Syntax für Datensätze (oder Karten) zu verwenden, und dies lässt keine gute Möglichkeit, gleichzeitig zu vermitteln, ob ein Feld vorhanden war oder nicht, und es zu lesen Standardwerte.
Der Ansatz bei gpb
besteht also darin, dass Sie sich entscheiden müssen: entweder oder. Normalerweise kann man erkennen, ob ein optionales Feld vorhanden ist oder nicht, indem man beispielsweise prüft, ob der Wert undefined
ist. Es gibt jedoch Optionen für den Compiler, stattdessen auf Standardwerte zu dekodieren. In diesem Fall verlieren Sie die Möglichkeit zu erkennen, ob ein Feld vorhanden ist oder nicht. Die Optionen sind defaults_for_omitted_optionals
und type_defaults_for_omitted_optionals
für die Dekodierung in default=
-Werte bzw. in typspezifische Standardwerte.
Es funktioniert so:
message o1 {
optional uint32 a = 1 [ default = 33 ];
optional uint32 b = 2 ; // the type-specific default is 0
}
Wenn binäre Daten <<>>
gegeben sind, d. h. weder Feld a
noch b
vorhanden sind, führt der Aufruf decode_msg(Input, o1)
zu Folgendem:
# 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
Die letzte Alternative ist vielleicht nicht sehr nützlich, aber dennoch möglich und der Vollständigkeit halber implementiert.
Googles Referenz
Für Proto3 gibt es weder required
noch default=
für Felder. Stattdessen sind alle Skalarfelder, Zeichenfolgen und Bytes implizit optional, sofern sie nicht mit optional
markiert sind. Wenn bei der Dekodierung ein solches Feld in der zu dekodierenden Binärdatei fehlt, wird immer auf den typspezifischen Standardwert dekodiert. Bei der Codierung werden solche Felder nur dann in die resultierende codierte Binärdatei einbezogen, wenn sie einen anderen Wert als den typspezifischen Standardwert haben. Auch wenn alle Felder implizit optional sind, könnte man auf konzeptioneller Ebene auch sagen, dass alle diese Felder immer einen Wert haben. Bei der Dekodierung kann nicht festgestellt werden, ob bei der Kodierung ein Wert – mit einem typspezifischen Wert – vorhanden war oder nicht.
Als optional
markierte Felder werden im Wesentlichen auf die gleiche Weise wie in der Proto2-Syntax dargestellt; In einem Datensatz hat das Feld den Wert undefined
wenn es nicht gesetzt ist, und in Karten ist das Feld nicht vorhanden, wenn es nicht gesetzt ist.
Eine Empfehlung, die ich gesehen habe, wenn Sie „fehlende“ Daten erkennen müssen, besteht darin, has_
boolesche Felder zu definieren und sie entsprechend festzulegen. Eine andere Alternative könnte die Verwendung der bekannten Wrapper-Nachrichten sein.
Felder, die Unternachrichten und eines von Feldern sind, haben keinen typspezifischen Standardwert. Ein Unternachrichtenfeld, das nicht festgelegt wurde, wird anders codiert als ein Unternachrichtenfeld, das für die Unternachricht festgelegt wurde, und es wird anders decodiert. Dies gilt auch dann, wenn die Unternachricht keine Felder enthält. Für eines der Felder funktioniert es etwas ähnlich. Entweder ist keines der alternativen oneof-Felder festgelegt, oder eines davon ist festgelegt. Das kodierte Format ist unterschiedlich und bei der Dekodierung kann man einen Unterschied erkennen.
Analysiert Protokollpuffer-Definitionsdateien und kann Folgendes generieren:
Merkmale der Protokollpuffer-Definitionsdateien: GPB unterstützt:
packed
und default
für Felderallow_alias
(wird so behandelt, als ob sie immer auf „true“ gesetzt wäre)oneof
(eingeführt in Protobuf 2.6.0)map<_,_>
(eingeführt in Protobuf 3.0.0)gpb liest, ignoriert aber:
packed
oder default
GPB unterstützt nicht:
Eigenschaften von GPB:
bytes
kopieren, um dem Laufzeitsystem die Freigabe der größeren Nachrichtenbinärdatei zu ermöglichen.package
nutzen, indem es den Namen des Pakets jedem enthaltenen Nachrichtentyp voranstellt (falls definiert), was nützlich ist, um Namenskonflikte von Nachrichtentypen über Pakete hinweg zu vermeiden. Siehe die Option use_packages
oder die Befehlszeilenoption -pkgs
.#field{}
-Datensatz in gpb.hrl für die Funktion get_msg_defs
, die jedoch vermieden werden kann Sie können diese Abhängigkeit auch mithilfe der Option defs_as_proplists
oder -pldefs
beheben.Selbstbeobachtung
gpb generiert einige Funktionen zur Untersuchung von Nachrichten, Enumerationen und Diensten:
get_msg_defs()
(oder get_proto_defs()
, wenn introspect_get_proto_defs
gesetzt ist), get_msg_names()
, get_enum_names()
find_msg_def(MsgName)
und fetch_msg_def(MsgName)
find_enum_def(MsgName)
und fetch_enum_def(MsgName)
enum_symbol_by_value(EnumName, Value)
,enum_symbol_by_value_(Value)
, enum_value_by_symbol(EnumName, Enum)
und 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)
Es gibt auch einige Funktionen zur Übersetzung zwischen vollqualifizierten Namen und internen Namen. Dabei werden etwaige Umbenennungsoptionen berücksichtigt. Sie können beispielsweise bei der GRPC-Reflexion nützlich sein.
fqbin_to_service_name(<<"Package.ServiceName">>)
und service_name_to_fqbin('ServiceName')
fqbins_to_service_and_rpc_name(<<"Package.ServiceName">>, <<"RpcName">>)
und service_and_rpc_name_to_fqbins('ServiceName', 'RpcName')
fqbin_to_msg_name(<<"Package.MsgName">>)
und msg_name_to_fqbin('MsgName')
fqbin_to_enum_name(<<"Package.EnumName">>)
und enum_name_to_fqbin('EnumName')
Es gibt auch einige Funktionen zum Abfragen, zu welchem Proto ein Typ gehört. Jeder Typ gehört zu einem "name"
, der eine Zeichenfolge ist, normalerweise der Dateiname ohne Erweiterung, zum Beispiel "name"
, wenn die Proto-Datei "name.proto"
war.
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", ...]
Es gibt auch einige Versionsinformationsfunktionen:
gpb:version_as_string()
, gpb:version_as_list()
und gpb:version_source()
GeneratedCode:version_as_string()
, GeneratedCode:version_as_list()
undGeneratedCode:version_source()
?gpb_version
(in gpb_version.hrl)?'GeneratedCode_gpb_version'
(in GeneratedCode.hrl)Der GPB kann auch eine Selbstbeschreibung der Protodatei generieren. Die Selbstbeschreibung ist eine Beschreibung der Proto-Datei, die mit descriptor.proto, das in der Google-Protokollpufferbibliothek enthalten ist, in eine Binärdatei codiert wird. Beachten Sie, dass eine solche codierte Selbstbeschreibung nicht Byte für Byte mit dem identisch ist, was der Google-Protokollpuffer-Compiler für dasselbe Proto generiert, sondern ungefähr gleichwertig sein sollte.
Fehlerhaft codierte Protobuf-Nachrichten und -Felder führen im Allgemeinen zum Absturz des Decoders. Beispiele für solche fehlerhaften Kodierungen sind:
Karten
GPB kann Encoder/Decoder für Karten generieren.
Mit der Option maps_unset_optional
kann das Verhalten für nicht vorhandene optionale Felder festgelegt werden: ob sie in Karten weggelassen werden oder ob sie vorhanden sind, aber wie bei Datensätzen den Wert undefined
haben.
Meldung von Fehlern in .proto-Dateien
Gpb eignet sich nicht besonders gut für die Fehlerberichterstattung, insbesondere für die Referenzierung von Fehlern, beispielsweise Referenzen auf nicht definierte Nachrichten. Möglicherweise möchten Sie zunächst mit protoc
überprüfen, ob die .proto-Dateien gültig sind, bevor Sie sie an GPB weiterleiten.
Informationen zur Verwendung von GPB mit rebar3 finden Sie unter https://rebar3.org/docs/configuration/plugins/#protocol-buffers
In Rebar gibt es seit Version 2.6.0 Unterstützung für GPB. Sehen Sie sich den Proto-Compiler-Abschnitt der Datei rebar.sample.config unter https://github.com/rebar/rebar/blob/master/rebar.config.sample an
Für ältere Versionen von Rebar – vor 2.6.0 – beschreibt der folgende Text die Vorgehensweise:
Platzieren Sie die .proto-Dateien beispielsweise in einem proto/
-Unterverzeichnis. Jedes Unterverzeichnis außer src/ ist in Ordnung, da Rebar versucht, einen anderen Protobuf-Compiler für jedes .proto zu verwenden, das es im Unterverzeichnis src/ findet. Hier sind einige Zeilen für die Datei 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"}]}.
Die GPB-Versionsnummer wird aus dem neuesten Git-Tag von Git abgerufen, das mit NM übereinstimmt, wobei N und M Ganzzahlen sind. Diese Version wird sowohl in die Datei gpb.app als auch in die Datei include/gpb_version.hrl eingefügt. Die Version ist das Ergebnis des Befehls
git describe --always --tags --match '[0-9]*.[0-9]*'
Um also eine neue Version von GPB zu erstellen, ist die einzige Quelle, von der diese Version abgerufen wird, das Git-Tag. (Wenn Sie GPB in ein anderes Versionskontrollsystem als Git importieren oder ein anderes Build-Tool als Rebar verwenden, müssen Sie möglicherweise rebar.config und src/gpb.app.src entsprechend anpassen. Siehe auch den Abschnitt unten über das Erstellen außerhalb von a (Git-Arbeitsbaum für Informationen zum Exportieren von GPB aus Git.)
Die Versionsnummer aus dem obigen Befehl git describe
sieht folgendermaßen aus:
..
(auf Master auf Github)..--g
(auf Zweigen oder zwischen Releases) Die Versionsnummer im Master-Zweig des GPB auf Github soll immer nur ganze Zahlen mit Punkten sein, um mit reltool kompatibel zu sein. Mit anderen Worten: Jeder Push an den Master-Zweig von Github gilt als Veröffentlichung und die Versionsnummer wird erhöht. Um dies sicherzustellen, gibt es im Unterverzeichnis helpers einen pre-push
-Git-Hook und zwei Skripte, install-git-hooks
und tag-next-minor-vsn
. Die ChangeLog-Datei spiegelt nicht unbedingt alle kleineren Versionsänderungen wider, sondern nur wichtige Aktualisierungen.
Orte zum Aktualisieren bei der Erstellung einer neuen Version:
Der GPB-Build-Prozess erwartet einen (nicht flachen) Git-Arbeitsbaum mit Tags, um die richtige Versionsnummerierung zu erhalten, wie im Abschnitt Versionsnummerierung beschrieben, aber es ist auch möglich, außerhalb von Git zu erstellen. Dazu haben Sie zwei Möglichkeiten:
gpb.vsn
erstellen, in der sich die Version in der ersten Zeile befindethelpers/mk-versioned-archive
ein versioniertes Archiv, entpacken Sie dann das Archiv und erstellen Sie darin. Wenn Sie das versionierte Archiv in einem Git-Arbeitsbaum erstellen, wird die Version automatisch festgelegt, andernfalls müssen Sie sie manuell angeben. Führen Sie mk-versioned-archive --help
aus, um Informationen zu den zu verwendenden Optionen zu erhalten.
Beim Herunterladen von Github wurden die gpb-
Wenn Sie Githubs automatische Quellcode- Zip- oder tar.gz-Archive verwenden, müssen Sie entweder eine gpb.vsn
Datei wie oben beschrieben erstellen oder ein versioniertes Archiv mit dem Skript mk-versioned-archive
und dem --override-version=
neu erstellen. --override-version=
Option (oder möglicherweise die oder die Option --override-version-from-cwd-path
wenn der Verzeichnisname eine richtige Version enthält.)
Beiträge sind willkommen, vorzugsweise als Pull-Requests oder Git-Patches oder Git-Fetch-Requests. Hier einige Richtlinien:
rebar clean; rebar eunit && rebar doc
Einzelheiten finden Sie im ChangeLog.
Der Standardwert für die Option maps_unset_optional
wurde von present_undefined
in omitted
geändert. Dies betrifft nur Code, der mit den Optionen „maps“ (-maps) generiert wurde. Projekte, die diese Option bereits explizit festlegen, sind nicht betroffen. Projekte, die sich auf die Standardeinstellung present_undefined
verlassen haben, müssen die Option explizit festlegen, um ein Upgrade auf 4.0.0 durchzuführen.
Für Typspezifikationen wurde die Standardeinstellung geändert, um diese nach Möglichkeit zu generieren. Die Option {type_specs,false}
(-no_type) kann verwendet werden, um die Generierung von Typspezifikationen zu vermeiden.