O gpb é um compilador para arquivos de definições de buffer de protocolo do Google para Erlang.
Atalhos: documentação da API ~ gpb em hex.pm
Digamos que temos um arquivo protobuf, x.proto
message Person {
required string name = 1 ;
required int32 id = 2 ;
optional string email = 3 ;
}
Podemos gerar código para esta definição de diversas maneiras diferentes. Aqui usamos a ferramenta de linha de comando. Para informações sobre integração com vergalhões, veja mais abaixo.
# .../gpb/bin/protoc-erl -I. x.proto
Agora temos x.erl
e x.hrl
. Primeiro compilamos e depois podemos testá-lo no 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] " }
No shell Erlang, o rr("x.hrl")
lê as definições de registro e o v(-1)
faz referência a um valor uma etapa anterior no histórico.
Tipo de protobuf | Tipo Erlang |
---|---|
duplo, flutuante | flutuar() | infinito | '-infinito' | nan Ao codificar, números inteiros também são aceitos |
int32, int64 uint32, uint64 sint32, sint64 fixo32, fixo64 sfixed32, sfixed64 | inteiro() |
bool | verdade | falso Ao codificar, os inteiros 1 e 0 também são aceitos |
enumeração | átomo() enums desconhecidos decodificados para inteiro() |
mensagem | registro (portanto tupla()) ou map() se a opção mapas (-maps) for especificada |
corda | string unicode, portanto lista de inteiros ou binary() se a opção strings_as_binaries (-strbin) for especificada Ao codificar, os iolists também são aceitos |
bytes | binário() Ao codificar, os iolists também são aceitos |
um de | {ChosenFieldName, Valor} ou ChosenFieldName => Valor se a opção {maps_oneof,flat} (-maps_oneof flat) for especificada |
mapa<_,_> | Uma lista não ordenada de 2 tuplas, [{Key,Value}] ou um mapa(), se a opção mapas (-maps) for especificada |
Os campos repetidos são representados como listas.
Os campos opcionais são representados como valor ou undefined
se não forem definidos. No entanto, para mapas, se a opção maps_unset_optional
for definida como omitted
, os valores opcionais não definidos serão omitidos do mapa, em vez de serem definidos como undefined
ao codificar mensagens. Ao decodificar mensagens, mesmo com maps_unset_optional
definido como omitted
, o valor padrão será definido no 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 construção apareceu pela primeira vez no Google protobuf versão 2.6.0.
message m3 {
oneof u {
int32 a = 1 ;
string b = 2 ;
}
}
Um campo oneof é automaticamente sempre 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 " }
#{}
Não deve ser confundido com mapas de Erlang. Esta construção apareceu pela primeira vez no Google protobuf versão 3.0.0 (para a sintaxe proto2
e proto3
)
message m4 {
map < uint32 , string > f = 1 ;
}
Para registros, a ordem dos itens é indefinida durante a decodificação.
# m4 { f = []}
# m4 { f = [{ 1 , " a " }, { 2 , " b " }, { 13 , " hello " }]}
% % With the maps option
#{ f => #{}}
#{ f => #{ 1 => " a " , 2 => " b " , 13 => " hello " }}
default
Isto descreve como a decodificação funciona para campos opcionais que não estão presentes no binário para decodificação.
A documentação do protobuf do Google diz que eles são decodificados para o valor padrão, se especificado, ou então para o padrão específico do tipo do campo. O código gerado pelo compilador protobuf do Google também contém métodos has_
para que se possa examinar se um campo estava realmente presente ou não.
No entanto, em Erlang, a maneira natural de definir e ler campos é apenas usar a sintaxe para registros (ou mapas), e isso não deixa uma boa maneira de, ao mesmo tempo, transmitir se um campo estava presente ou não e ler o padrões.
Portanto, a abordagem no gpb
é que você deve escolher: um ou outro. Normalmente, é possível ver se um campo opcional está presente ou não, por exemplo, verificando se o valor é undefined
. Mas existem opções para o compilador decodificar para os padrões; nesse caso, você perde a capacidade de ver se um campo está presente ou não. As opções são defaults_for_omitted_optionals
e type_defaults_for_omitted_optionals
, para decodificação para valores default=
ou para padrões específicos de tipo, respectivamente.
Funciona assim:
message o1 {
optional uint32 a = 1 [ default = 33 ];
optional uint32 b = 2 ; // the type-specific default is 0
}
Dados os dados binários <<>>
, ou seja, nem o campo a
nem b
estão presentes, então a chamada decode_msg(Input, o1)
resulta em:
# 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
A última das alternativas talvez não seja muito útil, mas ainda é possível e implementada de forma completa.
Referência do Google
Para proto3, não há required
nem default=
para campos. Em vez disso, a menos que marcado com optional
, todos os campos escalares, strings e bytes são implicitamente opcionais. Na decodificação, se tal campo estiver faltando no binário a ser decodificado, eles sempre serão decodificados para o valor padrão específico do tipo. Na codificação, esses campos só serão incluídos no binário codificado resultante se tiverem um valor diferente do valor padrão específico do tipo. Embora todos os campos sejam implicitamente opcionais, também se poderia dizer que, a nível conceptual, todos esses campos têm sempre um valor. Na decodificação, não é possível determinar se na codificação um valor estava presente — com um valor específico do tipo — ou não.
Os campos marcados como optional
são essencialmente representados da mesma forma que na sintaxe proto2; em um registro o campo tem o valor undefined
se não estiver definido, e nos mapas o campo não estará presente se não estiver definido.
Uma recomendação que vi se você precisar detectar dados "ausentes" é definir campos booleanos has_
e configurá-los adequadamente. Outra alternativa poderia ser usar as conhecidas mensagens wrapper.
Os campos que são submensagens e um dos campos não possuem nenhum padrão específico de tipo. Um campo de submensagem que não foi definido codifica de maneira diferente de um campo de submensagem definido para a submensagem e é decodificado de maneira diferente. Isso vale mesmo quando a submensagem não possui campos. Funciona de forma um pouco semelhante para um dos campos. Nenhum dos campos alternativos está definido ou um deles está. O formato codificado é diferente e na decodificação é possível perceber a diferença.
Analisa arquivos de definição de buffer de protocolo e pode gerar:
Recursos dos arquivos de definição de buffer de protocolo: gpb suporta:
packed
e default
para camposallow_alias
(tratada como se estivesse sempre definida como verdadeira)oneof
(introduzido no protobuf 2.6.0)map<_,_>
(introduzido no protobuf 3.0.0)gpb lê mas ignora:
packed
ou default
gpb não suporta:
Características do GPB:
bytes
, para permitir que o sistema de tempo de execução libere o binário de mensagem maior.package
acrescentando o nome do pacote a cada tipo de mensagem contida (se definido), o que é útil para evitar conflitos de nomes de tipos de mensagens entre pacotes. Consulte a opção use_packages
ou a opção de linha de comando -pkgs
.#field{}
em gpb.hrl para a função get_msg_defs
, mas é possível evitar esta dependência usando também a opção defs_as_proplists
ou -pldefs
.Introspecção
gpb gera algumas funções para examinar mensagens, enumerações e serviços:
get_msg_defs()
(ou get_proto_defs()
se introspect_get_proto_defs
estiver definido), get_msg_names()
, get_enum_names()
find_msg_def(MsgName)
e fetch_msg_def(MsgName)
find_enum_def(MsgName)
e fetch_enum_def(MsgName)
enum_symbol_by_value(EnumName, Value)
,enum_symbol_by_value_(Value)
, enum_value_by_symbol(EnumName, Enum)
e 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)
Existem também algumas funções para tradução entre nomes totalmente qualificados e nomes internos. Eles levam em consideração quaisquer opções de renomeação. Eles podem ser úteis, por exemplo, com reflexão grpc.
fqbin_to_service_name(<<"Package.ServiceName">>)
e service_name_to_fqbin('ServiceName')
fqbins_to_service_and_rpc_name(<<"Package.ServiceName">>, <<"RpcName">>)
e service_and_rpc_name_to_fqbins('ServiceName', 'RpcName')
fqbin_to_msg_name(<<"Package.MsgName">>)
e msg_name_to_fqbin('MsgName')
fqbin_to_enum_name(<<"Package.EnumName">>)
e enum_name_to_fqbin('EnumName')
Existem também algumas funções para consultar a qual proto um tipo pertence. Cada tipo pertence a algum "name"
que é uma string, geralmente o nome do arquivo, sem extensão, por exemplo "name"
se o arquivo proto for "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", ...]
Existem também algumas funções de informações de versão:
gpb:version_as_string()
, gpb:version_as_list()
e gpb:version_source()
GeneratedCode:version_as_string()
, GeneratedCode:version_as_list()
eGeneratedCode:version_source()
?gpb_version
(em gpb_version.hrl)?'GeneratedCode_gpb_version'
(em GeneratedCode.hrl)O gpb também pode gerar uma autodescrição do arquivo proto. A autodescrição é uma descrição do arquivo proto, codificado em um binário usando o descriptor.proto que vem com a biblioteca de buffers de protocolo do Google. Observe que essas autodescrições codificadas não serão idênticas byte a byte ao que o compilador de buffers de protocolo do Google gerará para o mesmo proto, mas devem ser aproximadamente equivalentes.
Mensagens e campos protobuf codificados erroneamente geralmente farão com que o decodificador trave. Exemplos de tais codificações errôneas são:
Mapas
Gpb pode gerar codificadores/decodificadores para mapas.
A opção maps_unset_optional
pode ser usada para especificar o comportamento de campos opcionais não presentes: se eles são omitidos dos mapas ou se estão presentes, mas têm o valor undefined
como para registros.
Relatório de erros em arquivos .proto
O Gpb não é muito bom em relatar erros, especialmente referenciando erros, como referências a mensagens que não estão definidas. Você pode querer primeiro verificar com protoc
se os arquivos .proto são válidos antes de alimentá-los ao gpb.
Para obter informações sobre como usar gpb com rebar3, consulte https://rebar3.org/docs/configuration/plugins/#protocol-buffers
No rebar há suporte para gpb desde a versão 2.6.0. Consulte a seção do compilador proto do arquivo rebar.sample.config em https://github.com/rebar/rebar/blob/master/rebar.config.sample
Para versões mais antigas do vergalhão --- anteriores à 2.6.0 --- o texto abaixo descreve como proceder:
Coloque os arquivos .proto, por exemplo, em um subdiretório proto/
. Qualquer subdiretório, diferente de src/, é adequado, pois o rebar tentará usar outro compilador protobuf para qualquer .proto que encontrar no subdiretório src/. Aqui estão algumas linhas para o arquivo 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"}]}.
O número da versão gpb é obtido da última tag git do git correspondente a NM, onde N e M são números inteiros. Esta versão é inserida no arquivo gpb.app e também em include/gpb_version.hrl. A versão é o resultado do comando
git describe --always --tags --match '[0-9]*.[0-9]*'
Assim, para criar uma nova versão do gpb, a única fonte de onde esta versão é obtida é a tag git. (Se você estiver importando gpb para outro sistema de controle de versão que não o git, ou usando outra ferramenta de construção que não o rebar, talvez seja necessário adaptar rebar.config e src/gpb.app.src adequadamente. Consulte também a seção abaixo sobre construção fora de um árvore de trabalho do git para obter informações sobre como exportar gpb do git.)
O número da versão do comando git describe
acima será parecido com
..
(no master no Github)..--g
(em ramificações ou entre versões) O número da versão no branch master do gpb no Github deve ser sempre apenas números inteiros com pontos, para ser compatível com o reltool. Em outras palavras, cada push para o branch master do Github é considerado um lançamento e o número da versão é aumentado. Para garantir isso, há um git hook pre-push
e dois scripts, install-git-hooks
e tag-next-minor-vsn
, no subdiretório helpers. O arquivo ChangeLog não refletirá necessariamente todas as alterações de versão secundárias, apenas atualizações importantes.
Locais para atualizar ao fazer uma nova versão:
O processo de construção do gpb espera uma árvore de trabalho git (não superficial), com tags, para obter a numeração de versão correta, conforme descrito na seção Numeração de versão, mas também é possível compilar fora do git. Para fazer isso, você tem duas opções:
gpb.vsn
, com a versão na primeira linhahelpers/mk-versioned-archive
, depois descompacte o arquivo e construa dentro dele. Se você criar o arquivo versionado em uma árvore de trabalho git, a versão será definida automaticamente, caso contrário, você precisará especificá-la manualmente. Execute mk-versioned-archive --help
para obter informações sobre quais opções usar.
Ao fazer download do Github, os arquivos gpb-
Se você usar os arquivos zip ou tar.gz de código-fonte automático do Github, você precisará criar um arquivo gpb.vsn
conforme descrito acima ou recriar um arquivo versionado usando o script mk-versioned-archive
e o --override-version=
opção --override-version=
(ou possivelmente a opção ou --override-version-from-cwd-path
se o nome do diretório contiver uma versão adequada.)
Contribuições são bem-vindas, de preferência como solicitações pull ou patches git ou solicitações de busca git. Aqui estão algumas diretrizes:
rebar clean; rebar eunit && rebar doc
Consulte o ChangeLog para obter detalhes.
O valor padrão para a opção maps_unset_optional
foi alterado para omitted
, de present_undefined
Isso se aplica apenas ao código gerado com as opções de mapas (-maps). Projetos que já definiram explicitamente esta opção não serão impactados. Projetos que dependiam do padrão present_undefined
precisarão definir a opção explicitamente para atualizar para 4.0.0.
Para especificações de tipo, o padrão foi alterado para gerá-las quando possível. A opção {type_specs,false}
(-no_type) pode ser usada para evitar a geração de especificações de tipo.