gpb es un compilador de archivos de definiciones de búfer del protocolo de Google para Erlang.
Accesos directos: documentación API ~ gpb en hex.pm
Digamos que tenemos un archivo protobuf, x.proto
message Person {
required string name = 1 ;
required int32 id = 2 ;
optional string email = 3 ;
}
Podemos generar código para esta definición de varias maneras diferentes. Aquí usamos la herramienta de línea de comando. Para obtener información sobre la integración con barras de refuerzo, consulte más abajo.
# .../gpb/bin/protoc-erl -I. x.proto
Ahora tenemos x.erl
y x.hrl
. Primero lo compilamos y luego podemos probarlo en el shell de 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] " }
En el shell de Erlang, rr("x.hrl")
lee definiciones de registros y v(-1)
hace referencia a un valor un paso anterior en el historial.
tipo protobuf | tipo erlang |
---|---|
doble, flotar | flotador() | infinito | '-infinito' | yaya Al codificar, también se aceptan números enteros. |
int32, int64 uint32, uint64 sint32, sint64 fijo32, fijo64 sfixed32, sfixed64 | entero() |
booleano | verdadero | FALSO Al codificar, también se aceptan los números enteros 1 y 0. |
enumeración | átomo() enumeraciones desconocidas se decodifican a entero () |
mensaje | registro (por lo tanto tupla()) o map() si se especifica la opción mapas (-maps) |
cadena | cadena Unicode, por lo tanto lista de números enteros o binario() si se especifica la opción strings_as_binaries (-strbin) Al codificar, también se aceptan iolistas. |
bytes | binario() Al codificar, también se aceptan iolistas. |
uno de | {Nombre del campo elegido, valor} o ChosenFieldName => Valor si se especifica la opción {maps_oneof,flat} (-maps_oneof flat) |
mapa<_,_> | Una lista desordenada de 2 tuplas, [{Key,Value}] o un mapa(), si se especifica la opción mapas (-maps) |
Los campos repetidos se representan como listas.
Los campos opcionales se representan como valor o undefined
si no se configuran. Sin embargo, para los mapas, si la opción maps_unset_optional
está configurada como omitted
, los valores opcionales no configurados se omiten del mapa, en lugar de configurarse como undefined
al codificar mensajes. Al decodificar mensajes, incluso con maps_unset_optional
configurado en omitted
, el valor predeterminado se establecerá en el mapa decodificado.
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 }
Esta construcción apareció por primera vez en la versión 2.6.0 del protobuf de Google.
message m3 {
oneof u {
int32 a = 1 ;
string b = 2 ;
}
}
Un campo oneof es automáticamente siempre opcional.
# 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 " }
#{}
No confundir con mapas de Erlang. Esta construcción apareció por primera vez en Google protobuf versión 3.0.0 (tanto para la sintaxis proto2
como de proto3
)
message m4 {
map < uint32 , string > f = 1 ;
}
Para los registros, el orden de los elementos no está definido al decodificar.
# m4 { f = []}
# m4 { f = [{ 1 , " a " }, { 2 , " b " }, { 13 , " hello " }]}
% % With the maps option
#{ f => #{}}
#{ f => #{ 1 => " a " , 2 => " b " , 13 => " hello " }}
default
Esto describe cómo funciona la decodificación para campos opcionales que no están presentes en el binario a decodificar.
La documentación de Google protobuf dice que estos se decodifican al valor predeterminado si se especifica, o al valor predeterminado específico del tipo del campo. El código generado por el compilador protobuf de Google también contiene métodos has_
para que uno pueda examinar si un campo estaba realmente presente o no.
Sin embargo, en Erlang, la forma natural de configurar y leer campos es simplemente usar la sintaxis para registros (o mapas), y esto no deja una buena manera de transmitir al mismo tiempo si un campo estaba presente o no y leer el valores predeterminados.
Entonces, el enfoque en gpb
es que tienes que elegir: o. Normalmente, es posible ver si un campo opcional está presente o no, por ejemplo, comprobando si el valor no está undefined
. Pero hay opciones para que el compilador decodifique los valores predeterminados, en cuyo caso se pierde la capacidad de ver si un campo está presente o no. Las opciones son defaults_for_omitted_optionals
y type_defaults_for_omitted_optionals
, para decodificar valores default=
o valores predeterminados específicos del tipo, respectivamente.
Funciona de esta manera:
message o1 {
optional uint32 a = 1 [ default = 33 ];
optional uint32 b = 2 ; // the type-specific default is 0
}
Dados los datos binarios <<>>
, es decir, ni el campo a
ni b
están presentes, entonces la llamada decode_msg(Input, o1)
da como resultado:
# 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 última de las alternativas quizás no sea muy útil, pero aún así es posible y se implementa de manera completa.
Referencia de Google
Para proto3, no hay campos required
ni default=
. En cambio, a menos que estén marcados con optional
, todos los campos, cadenas y bytes escalares son implícitamente opcionales. Al decodificar, si dicho campo falta en el binario a decodificar, siempre decodifican al valor predeterminado específico del tipo. Al codificar, dichos campos solo se incluyen en el binario codificado resultante si tienen un valor diferente del valor predeterminado específico del tipo. Aunque todos los campos son implícitamente opcionales, también se podría decir que a nivel conceptual, todos esos campos siempre tienen un valor. Durante la decodificación, no es posible determinar si durante la codificación estaba presente un valor (con un valor específico del tipo) o no.
Los campos marcados como optional
se representan esencialmente de la misma manera que en la sintaxis de proto2; en un registro el campo tiene el valor undefined
si no está configurado, y en mapas el campo no está presente si no está configurado.
Una recomendación que he visto si necesita detectar datos "faltantes" es definir los campos booleanos has_
y configurarlos adecuadamente. Otra alternativa podría ser utilizar los conocidos mensajes contenedores.
Los campos que son submensajes y uno de los campos no tienen ningún tipo predeterminado específico. Un campo de submensaje que no se configuró se codifica de manera diferente a un campo de submensaje configurado para el submensaje y se decodifica de manera diferente. Esto se mantiene incluso cuando el submensaje no tiene campos. Funciona de manera un poco similar para uno de los campos. O ninguno de los campos alternativos oneof está configurado o uno de ellos sí lo está. El formato codificado es diferente y al decodificarlo es posible notar la diferencia.
Analiza archivos de definición de búfer de protocolo y puede generar:
Características de los archivos de definición del búfer de protocolo: gpb admite:
packed
y default
para los camposallow_alias
(tratada como si siempre estuviera configurada como verdadera)oneof
(introducido en protobuf 2.6.0)map<_,_>
(introducido en protobuf 3.0.0)gpb lee pero ignora:
packed
o default
gpb no soporta:
Características del gpb:
bytes
para permitir que el sistema de ejecución libere el mensaje binario más grande.package
anteponiendo el nombre del paquete a cada tipo de mensaje contenido (si está definido), lo cual es útil para evitar conflictos de nombres de tipos de mensajes entre paquetes. Consulte la opción use_packages
o la opción de línea de comando -pkgs
.#field{}
en gpb.hrl para la función get_msg_defs
, pero es posible evitarlo. esta dependencia utilizando también la opción defs_as_proplists
o -pldefs
.Introspección
gpb genera algunas funciones para examinar mensajes, enumeraciones y servicios:
get_msg_defs()
(o get_proto_defs()
si introspect_get_proto_defs
está configurado), get_msg_names()
, get_enum_names()
find_msg_def(MsgName)
y fetch_msg_def(MsgName)
find_enum_def(MsgName)
y fetch_enum_def(MsgName)
enum_symbol_by_value(EnumName, Value)
,enum_symbol_by_value_(Value)
, enum_value_by_symbol(EnumName, Enum)
y 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)
También hay algunas funciones para traducir entre nombres completos y nombres internos. Estos tienen en cuenta cualquier opción de cambio de nombre. Pueden resultar útiles, por ejemplo, con la reflexión grpc.
fqbin_to_service_name(<<"Package.ServiceName">>)
y service_name_to_fqbin('ServiceName')
fqbins_to_service_and_rpc_name(<<"Package.ServiceName">>, <<"RpcName">>)
y service_and_rpc_name_to_fqbins('ServiceName', 'RpcName')
fqbin_to_msg_name(<<"Package.MsgName">>)
y msg_name_to_fqbin('MsgName')
fqbin_to_enum_name(<<"Package.EnumName">>)
y enum_name_to_fqbin('EnumName')
También hay algunas funciones para consultar a qué proto pertenece un tipo. Cada tipo pertenece a algún "name"
, que es una cadena, generalmente el nombre del archivo, sin extensión, por ejemplo "name"
si el archivo proto era "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", ...]
También hay algunas funciones de información de versión:
gpb:version_as_string()
, gpb:version_as_list()
y gpb:version_source()
GeneratedCode:version_as_string()
, GeneratedCode:version_as_list()
yGeneratedCode:version_source()
?gpb_version
(en gpb_version.hrl)?'GeneratedCode_gpb_version'
(en GeneratedCode.hrl)El gpb también puede generar una autodescripción del archivo proto. La autodescripción es una descripción del archivo proto, codificada en un binario utilizando el descriptor.proto que viene con la biblioteca de buffers de protocolo de Google. Tenga en cuenta que dichas autodescripciones codificadas no serán idénticas byte a byte a lo que generará el compilador de buffers de protocolo de Google para el mismo proto, pero deberían ser aproximadamente equivalentes.
Los mensajes y campos de protobuf codificados erróneamente generalmente provocarán que el decodificador falle. Ejemplos de tales codificaciones erróneas son:
Mapas
Gpb puede generar codificadores/decodificadores para mapas.
La opción maps_unset_optional
se puede utilizar para especificar el comportamiento de campos opcionales no presentes: si se omiten en los mapas o si están presentes, pero tienen el valor undefined
como para los registros.
Informe de errores en archivos .proto
Gpb no es muy bueno para informar errores, especialmente errores de referencia, como referencias a mensajes que no están definidos. Es posible que desee verificar primero con protoc
que los archivos .proto sean válidos antes de enviarlos a gpb.
Para obtener información sobre cómo usar gpb con rebar3, consulte https://rebar3.org/docs/configuration/plugins/#protocol-buffers
En rebar hay soporte para gpb desde la versión 2.6.0. Consulte la sección del compilador proto del archivo rebar.sample.config en https://github.com/rebar/rebar/blob/master/rebar.config.sample
Para versiones anteriores de rebar (anteriores a 2.6.0), el texto siguiente describe cómo proceder:
Coloque los archivos .proto, por ejemplo, en un subdirectorio proto/
. Cualquier subdirectorio, que no sea src/, está bien, ya que rebar intentará usar otro compilador protobuf para cualquier .proto que encuentre en el subdirectorio src/. Aquí hay algunas líneas para el archivo 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"}]}.
El número de versión de gpb se obtiene de la última etiqueta git de git que coincide con NM, donde N y M son números enteros. Esta versión se inserta en el archivo gpb.app así como en include/gpb_version.hrl. La versión es el resultado del comando.
git describe --always --tags --match '[0-9]*.[0-9]*'
Por lo tanto, para crear una nueva versión de gpb, la única fuente de donde se obtiene esta versión es la etiqueta git. (Si está importando gpb a otro sistema de control de versiones que no sea git, o está utilizando otra herramienta de compilación que no sea rebar, es posible que deba adaptar rebar.config y src/gpb.app.src en consecuencia. Consulte también la sección siguiente sobre la compilación fuera de un árbol de trabajo de git para obtener información sobre cómo exportar gpb desde git).
El número de versión del comando git describe
anterior se verá así
..
(en master en Github)..--g
(en ramas o entre versiones) El número de versión en la rama maestra de gpb en Github debe ser siempre solo números enteros con puntos, para que sea compatible con reltool. En otras palabras, cada envío a la rama maestra de Github se considera una versión y el número de versión aumenta. Para garantizar esto, hay un gancho git pre-push
y dos scripts, install-git-hooks
y tag-next-minor-vsn
, en el subdirectorio de ayuda. El archivo ChangeLog no reflejará necesariamente todos los cambios menores en la versión, solo las actualizaciones importantes.
Lugares para actualizar al realizar una nueva versión:
El proceso de compilación de gpb espera un árbol de trabajo de git (no superficial), con etiquetas, para obtener la numeración de versiones correcta, como se describe en la sección Numeración de versiones, pero también es posible compilar fuera de git. Para hacer eso, tienes dos opciones:
gpb.vsn
, con la versión en la primera líneahelpers/mk-versioned-archive
, luego descomprima el archivo y compílelo dentro de él. Si crea el archivo versionado en un árbol de trabajo de git, la versión se configurará automáticamente; de lo contrario, deberá especificarla manualmente. Ejecute mk-versioned-archive --help
para obtener información sobre qué opciones usar.
Al descargar desde Github, los archivos gpb-
Si utiliza los archivos zip o tar.gz de código fuente automático de Github, deberá crear un archivo gpb.vsn
como se describe anteriormente o volver a crear un archivo versionado usando el script mk-versioned-archive
y --override-version=
opción --override-version=
(o posiblemente la opción o --override-version-from-cwd-path
si el nombre del directorio contiene una versión adecuada).
Las contribuciones son bienvenidas, preferiblemente como solicitudes de extracción, parches de git o solicitudes de recuperación de git. Aquí hay algunas líneas guía:
rebar clean; rebar eunit && rebar doc
Consulte el registro de cambios para obtener más detalles.
El valor predeterminado para la opción maps_unset_optional
ha cambiado a omitted
, de present_undefined
Esto se refiere solo al código generado con las opciones de mapas (-maps). Los proyectos que ya configuraron esta opción explícitamente no se ven afectados. Los proyectos que dependían del valor predeterminado present_undefined
deberán configurar la opción explícitamente para poder actualizar a 4.0.0.
Para las especificaciones de tipo, el valor predeterminado ha cambiado para generarlas cuando sea posible. La opción {type_specs,false}
(-no_type) se puede utilizar para evitar generar especificaciones de tipo.