Le gpb est un compilateur de fichiers de définitions de tampons de protocole Google pour Erlang.
Raccourcis : documentation API ~ gpb sur hex.pm
Disons que nous avons un fichier protobuf, x.proto
message Person {
required string name = 1 ;
required int32 id = 2 ;
optional string email = 3 ;
}
Nous pouvons générer du code pour cette définition de différentes manières. Ici, nous utilisons l'outil de ligne de commande. Pour plus d'informations sur l'intégration avec rebar, voir plus bas.
# .../gpb/bin/protoc-erl -I. x.proto
Nous avons maintenant x.erl
et x.hrl
. Nous le compilons d’abord, puis nous pouvons l’essayer dans le shell Erlang :
# 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] " }
Dans le shell Erlang, le rr("x.hrl")
lit les définitions d'enregistrement et le v(-1)
fait référence à une valeur une étape plus tôt dans l'historique.
Type Protobuf | Type Erlang |
---|---|
doubler, flotter | flotteur() | infini | '-infini' | grand-mère Lors du codage, les entiers sont également acceptés |
int32, int64 uint32, uint64 saint32, saint64 fixe32, fixe64 sfixed32, sfixed64 | entier() |
bouffon | vrai | FAUX Lors du codage, les entiers 1 et 0 sont également acceptés |
énumération | atome() les énumérations inconnues décodent en entier() |
message | enregistrer (donc tuple()) ou map() si l'option maps (-maps) est spécifiée |
chaîne | chaîne unicode, donc liste d'entiers ou binaire() si l'option strings_as_binaries (-strbin) est spécifiée Lors du codage, les iolistes sont également acceptés |
octets | binaire() Lors du codage, les iolistes sont également acceptés |
l'un des | {ChosenFieldName, Valeur} ou ChosenFieldName => Value si l'option {maps_oneof,flat} (-maps_oneof flat) est spécifiée |
carte<_,_> | Une liste non ordonnée de 2 tuples, [{Key,Value}] ou un map(), si l'option maps (-maps) est spécifiée |
Les champs répétés sont représentés sous forme de listes.
Les champs facultatifs sont représentés soit par la valeur, soit par undefined
s'ils ne sont pas définis. Cependant, pour les cartes, si l'option maps_unset_optional
est définie sur omitted
, alors les valeurs facultatives non définies sont omises de la carte, au lieu d'être définies sur undefined
lors du codage des messages. Lors du décodage des messages, même avec maps_unset_optional
défini sur omitted
, la valeur par défaut sera définie dans la carte décodée.
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 }
Cette construction est apparue pour la première fois dans la version 2.6.0 du protobuf de Google.
message m3 {
oneof u {
int32 a = 1 ;
string b = 2 ;
}
}
Un champ oneof est automatiquement toujours facultatif.
# 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 " }
#{}
A ne pas confondre avec les cartes Erlang. Cette construction est apparue pour la première fois dans Google protobuf version 3.0.0 (pour la syntaxe proto2
et proto3
)
message m4 {
map < uint32 , string > f = 1 ;
}
Pour les enregistrements, l'ordre des éléments n'est pas défini lors du décodage.
# m4 { f = []}
# m4 { f = [{ 1 , " a " }, { 2 , " b " }, { 13 , " hello " }]}
% % With the maps option
#{ f => #{}}
#{ f => #{ 1 => " a " , 2 => " b " , 13 => " hello " }}
default
Ceci décrit le fonctionnement du décodage pour les champs facultatifs qui ne sont pas présents dans le binaire à décoder.
La documentation de Google protobuf indique que ceux-ci décodent à la valeur par défaut si elle est spécifiée, ou bien à la valeur par défaut spécifique au type du champ. Le code généré par le compilateur protobuf de Google contient également des méthodes has_
permettant de vérifier si un champ était réellement présent ou non.
Cependant, en Erlang, la manière naturelle de définir et de lire les champs consiste simplement à utiliser la syntaxe des enregistrements (ou des cartes), ce qui ne laisse aucun moyen efficace de transmettre en même temps si un champ était présent ou non et de lire le champ. valeurs par défaut.
L'approche en gpb
est donc que vous devez choisir : soit ou. Normalement, il est possible de voir si un champ facultatif est présent ou non, par exemple en vérifiant si la valeur est undefined
. Mais il existe des options pour le compilateur pour décoder les valeurs par défaut, auquel cas vous perdez la possibilité de voir si un champ est présent ou non. Les options sont defaults_for_omitted_optionals
et type_defaults_for_omitted_optionals
, pour le décodage respectivement en valeurs default=
ou en valeurs par défaut spécifiques au type.
Cela fonctionne de cette façon :
message o1 {
optional uint32 a = 1 [ default = 33 ];
optional uint32 b = 2 ; // the type-specific default is 0
}
Étant donné les données binaires <<>>
, c'est-à-dire qu'aucun champ a
ni b
n'est présent, alors l'appel decode_msg(Input, o1)
donne :
# 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
La dernière des alternatives n’est peut-être pas très utile, mais reste possible et mise en œuvre par souci d’exhaustivité.
La référence de Google
Pour proto3, il n'y a ni required
ni default=
pour les champs. Au lieu de cela, à moins d'être marqués d' optional
, tous les champs scalaires, chaînes et octets sont implicitement facultatifs. Lors du décodage, si un tel champ manque dans le binaire à décoder, ils décodent toujours à la valeur par défaut spécifique au type. Lors du codage, ces champs ne sont inclus dans le binaire codé résultant que s'ils ont une valeur différente de la valeur par défaut spécifique au type. Même si tous les champs sont implicitement facultatifs, on pourrait aussi dire que sur le plan conceptuel, tous ces champs ont toujours une valeur. Au décodage, il n'est pas possible de déterminer si au codage, une valeur était présente --- avec une valeur spécifique au type --- ou non.
Les champs marqués comme optional
sont essentiellement représentés de la même manière que dans la syntaxe proto2 ; dans un enregistrement, le champ a la valeur undefined
s'il n'est pas défini, et dans les cartes, le champ n'est pas présent s'il n'est pas défini.
Une recommandation que j'ai vue si vous avez besoin de détecter des données "manquantes" est de définir des champs booléens has_
et de les définir de manière appropriée. Une autre alternative pourrait être d’utiliser les messages wrapper bien connus.
Les champs qui sont des sous-messages et l'un des champs n'ont pas de valeur par défaut spécifique au type. Un champ de sous-message qui n'a pas été défini code différemment d'un champ de sous-message défini pour le sous-message, et il décode différemment. Cela est valable même lorsque le sous-message n'a pas de champs. Cela fonctionne un peu de la même manière pour l'un des champs. Soit aucun des champs alternatifs n'est défini, soit l'un d'entre eux l'est. Le format codé est différent et lors du décodage, il est possible de faire une différence.
Analyse les fichiers de définition de tampon de protocole et peut générer :
Caractéristiques des fichiers de définition de tampon de protocole : gpb prend en charge :
packed
et default
pour les champsallow_alias
(traitée comme si elle était toujours définie sur true)oneof
(introduit dans protobuf 2.6.0)map<_,_>
(introduit dans protobuf 3.0.0)gpb lit mais ignore :
packed
ou default
gpb ne prend pas en charge :
Caractéristiques du gpb :
bytes
, afin de permettre au système d'exécution de libérer le message binaire le plus volumineux.package
en ajoutant le nom du package à chaque type de message contenu (s'il est défini), ce qui est utile pour éviter les conflits de noms de types de messages entre les packages. Voir l'option use_packages
ou l'option de ligne de commande -pkgs
.#field{}
dans gpb.hrl pour la fonction get_msg_defs
, mais il est possible d'éviter cette dépendance en utilisant également l'option defs_as_proplists
ou -pldefs
.Introspection
gpb génère certaines fonctions pour examiner les messages, les énumérations et les services :
get_msg_defs()
(ou get_proto_defs()
si introspect_get_proto_defs
est défini), get_msg_names()
, get_enum_names()
find_msg_def(MsgName)
et fetch_msg_def(MsgName)
find_enum_def(MsgName)
et fetch_enum_def(MsgName)
enum_symbol_by_value(EnumName, Value)
,enum_symbol_by_value_(Value)
, enum_value_by_symbol(EnumName, Enum)
et 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)
Il existe également des fonctions de traduction entre les noms complets et les noms internes. Ceux-ci prennent en compte toutes les options de renommage. Ils peuvent être utiles par exemple avec la réflexion grpc.
fqbin_to_service_name(<<"Package.ServiceName">>)
et service_name_to_fqbin('ServiceName')
fqbins_to_service_and_rpc_name(<<"Package.ServiceName">>, <<"RpcName">>)
et service_and_rpc_name_to_fqbins('ServiceName', 'RpcName')
fqbin_to_msg_name(<<"Package.MsgName">>)
et msg_name_to_fqbin('MsgName')
fqbin_to_enum_name(<<"Package.EnumName">>)
et enum_name_to_fqbin('EnumName')
Il existe également des fonctions permettant de demander à quel proto appartient un type. Chaque type appartient à un "name"
qui est une chaîne, généralement le nom du fichier, sans extension, par exemple "name"
si le fichier proto était "name.proto"
.
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", ...]
Il existe également quelques fonctions d'informations sur la version :
gpb:version_as_string()
, gpb:version_as_list()
et gpb:version_source()
GeneratedCode:version_as_string()
, GeneratedCode:version_as_list()
etGeneratedCode:version_source()
?gpb_version
(dans gpb_version.hrl)?'GeneratedCode_gpb_version'
(dans GeneratedCode.hrl)Le gpb peut également générer une auto-description du fichier proto. L'auto-description est une description du fichier proto, codée en binaire à l'aide du descriptor.proto fourni avec la bibliothèque de tampons de protocole Google. Notez qu'une telle auto-description codée ne sera pas identique octet par octet à ce que le compilateur de tampons de protocole Google générera pour le même proto, mais devrait être à peu près équivalente.
Les messages et champs protobuf codés par erreur entraîneront généralement un crash du décodeur. Des exemples de tels codages erronés sont :
Cartes
Gpb peut générer des encodeurs/décodeurs pour les cartes.
L'option maps_unset_optional
peut être utilisée pour spécifier le comportement des champs facultatifs non présents : s'ils sont omis des cartes, ou s'ils sont présents, mais ont la valeur undefined
comme pour les enregistrements.
Signalement des erreurs dans les fichiers .proto
Gpb n'est pas très doué pour signaler les erreurs, en particulier pour faire référence aux erreurs, telles que les références à des messages qui ne sont pas définis. Vous souhaiterez peut-être d'abord vérifier avec protoc
que les fichiers .proto sont valides avant de les transmettre à gpb.
Pour plus d'informations sur l'utilisation de gpb avec rebar3, voir https://rebar3.org/docs/configuration/plugins/#protocol-buffers
Dans rebar, gpb est pris en charge depuis la version 2.6.0. Voir la section du proto-compilateur du fichier rebar.sample.config sur https://github.com/rebar/rebar/blob/master/rebar.config.sample
Pour les anciennes versions de rebar --- antérieures à 2.6.0 --- le texte ci-dessous explique comment procéder :
Placez les fichiers .proto par exemple dans un sous-répertoire proto/
. Tout sous-répertoire, autre que src/, convient, car rebar essaiera d'utiliser un autre compilateur protobuf pour tout .proto trouvé dans le sous-répertoire src/. Voici quelques lignes pour le fichier 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"}]}.
Le numéro de version de gpb est extrait de la dernière balise git correspondant à NM où N et M sont des entiers. Cette version est insérée dans le fichier gpb.app ainsi que dans le fichier include/gpb_version.hrl. La version est le résultat de la commande
git describe --always --tags --match '[0-9]*.[0-9]*'
Ainsi, pour créer une nouvelle version de gpb, la seule source à partir de laquelle cette version est récupérée est la balise git. (Si vous importez gpb dans un autre système de contrôle de version que git, ou si vous utilisez un autre outil de construction que rebar, vous devrez peut-être adapter rebar.config et src/gpb.app.src en conséquence. Voir aussi la section ci-dessous sur la construction en dehors d'un git work tree pour plus d'informations sur l'exportation de gpb depuis git.)
Le numéro de version de la commande git describe
ci-dessus ressemblera à
..
(sur master sur Github)..--g
(sur les branches ou entre les versions) Le numéro de version sur la branche master du gpb sur Github est destiné à toujours être uniquement des entiers avec des points, afin d'être compatible avec reltool. En d'autres termes, chaque poussée vers la branche principale de Github est considérée comme une version et le numéro de version est modifié. Pour garantir cela, il existe un hook git pre-push
et deux scripts, install-git-hooks
et tag-next-minor-vsn
, dans le sous-répertoire helpers. Le fichier ChangeLog ne reflétera pas nécessairement toutes les modifications mineures de la version, mais uniquement les mises à jour importantes.
Lieux à mettre à jour lors de la création d'une nouvelle version :
Le processus de construction de gpb attend un arbre de travail git (non superficiel), avec des balises, pour obtenir la bonne numérotation des versions, comme décrit dans la section Numérotation des versions, mais il est également possible de construire en dehors de git. Pour ce faire, vous avez deux options :
gpb.vsn
, avec la version sur la première lignehelpers/mk-versioned-archive
, puis décompressez l'archive et construisez-la. Si vous créez l'archive versionnée dans un arbre de travail git, la version sera définie automatiquement, sinon vous devrez la spécifier manuellement. Exécutez mk-versioned-archive --help
pour obtenir des informations sur les options à utiliser.
Lors du téléchargement depuis Github, les archives gpb-
Si vous utilisez les archives automatiques de code source zip ou tar.gz de Github, vous devrez soit créer un fichier gpb.vsn
comme décrit ci-dessus, soit recréer une archive versionnée à l'aide du script mk-versioned-archive
et de l' --override-version=
option --override-version=
(ou éventuellement l'option ou l'option --override-version-from-cwd-path
si le nom du répertoire contient une version appropriée.)
Les contributions sont les bienvenues, de préférence sous forme de requêtes pull, de correctifs git ou de requêtes git fetch. Voici quelques lignes directrices :
rebar clean; rebar eunit && rebar doc
Consultez le journal des modifications pour plus de détails.
La valeur par défaut de l'option maps_unset_optional
est passée à omitted
, de present_undefined
Cela concerne uniquement le code généré avec les options maps (-maps). Les projets qui définissent déjà explicitement cette option ne sont pas concernés. Les projets qui s'appuyaient sur la valeur par défaut present_undefined
devront définir l'option explicitement afin de passer à la version 4.0.0.
Pour les spécifications de type, la valeur par défaut a changé pour les générer lorsque cela est possible. L'option {type_specs,false}
(-no_type) peut être utilisée pour éviter de générer des spécifications de type.