gpb — это компилятор файлов определений буферов протокола Google для Erlang.
Ярлыки: документация по API ~ gpb на hex.pm
Допустим, у нас есть файл 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:
# 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 rr("x.hrl")
считывает определения записей, а v(-1)
ссылается на значение, находящееся на один шаг раньше в истории.
Тип протобуфа | Тип Эрланга |
---|---|
двойной, плавающий | плавать() | бесконечность | '-бесконечность' | Нэн При кодировании принимаются и целые числа. |
интервал32, интервал64 uint32, uint64 синт32, синт64 фиксированный32, фиксированный64 исправлено32, исправлено64 | целое число() |
логическое значение | правда | ЛОЖЬ При кодировании принимаются также целые числа 1 и 0. |
перечисление | атом() неизвестные перечисления декодируются в целое число() |
сообщение | запись (таким образом, кортеж()) или map(), если указана опция карт (-maps) |
нить | строка Юникода, то есть список целых чисел илиbinary(), если указана опция strings_as_binaries (-strbin). При кодировании тоже принимаются иолисты |
байты | двоичный() При кодировании тоже принимаются иолисты |
один из | {ChosenFieldName, Значение} или ChosenFieldName => Значение , если указана опция {maps_oneof, Flat} (-maps_oneof Flat). |
карта<_,_> | Неупорядоченный список из двух кортежей, [{Key,Value}] или карту(), если указана опция карт (-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 " }
#{}
Не путать с картами Эрланга. Эта конструкция впервые появилась в 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 говорится, что они декодируют значение по умолчанию, если оно указано, или же значение по умолчанию для конкретного типа поля. Код, сгенерированный компилятором protobuf от Google, также содержит методы has_
, позволяющие проверить, присутствует ли поле на самом деле или нет.
Однако в Эрланге естественным способом установки и чтения полей является просто использование синтаксиса для записей (или карт), и это не оставляет хорошего способа одновременно сообщить, присутствует ли поле или нет, и прочитать по умолчанию.
Итак, подход в 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
Последний из альтернатив, возможно, не очень полезен, но все же возможен и реализован для полноты картины.
Ссылка Google
Для proto3 для полей нет ни required
, ни default=
. Вместо этого, если они не отмечены optional
, все скалярные поля, строки и байты являются неявно необязательными. При декодировании, если такое поле отсутствует в двоичном файле для декодирования, они всегда декодируют до значения по умолчанию для конкретного типа. При кодировании такие поля включаются в результирующий закодированный двоичный файл только в том случае, если их значение отличается от значения по умолчанию для конкретного типа. Несмотря на то, что все поля неявно являются необязательными, можно также сказать, что на концептуальном уровне все такие поля всегда имеют значение. При декодировании невозможно определить, присутствовало ли при кодировании значение — со значением, специфичным для типа, — или нет.
Поля, помеченные как optional
, по существу представлены так же, как и в синтаксисе proto2; в записи поле имеет undefined
значение, если оно не установлено, а в картах поле отсутствует, если оно не установлено.
Я видел рекомендацию, если вам нужно обнаружить «отсутствующие» данные: определить логические поля has_
и установить их соответствующим образом. Другой альтернативой может быть использование известных сообщений-оболочек.
Поля, которые являются подсообщениями и одним из полей, не имеют каких-либо специфичных для типа полей по умолчанию. Поле подсообщения, которое не было установлено, кодируется иначе, чем поле подсообщения, установленное для подсообщения, и декодируется по-другому. Это справедливо, даже если подсообщение не имеет полей. Для одного из полей это работает примерно так же. Либо ни одно из альтернативных полей oneof не установлено, либо установлено одно из них. Закодированный формат отличается, и при декодировании можно заметить разницу.
Анализирует файлы определения буфера протокола и может генерировать:
Особенности файлов определения буфера протокола: gpb поддерживает:
packed
параметры и параметры default
для полейallow_alias
(рассматривается так, как если бы он всегда был установлен как true)oneof
(представлен в protobuf 2.6.0)map<_,_>
(представлено в protobuf 3.0.0)gpb читает, но игнорирует:
packed
или default
gpb не поддерживает:
Характеристики ГПБ:
bytes
полей, чтобы позволить системе времени выполнения освободить более крупный двоичный файл сообщения.package
, добавляя имя пакета к каждому содержащемуся типу сообщения (если оно определено), что полезно во избежание конфликтов имен типов сообщений в разных пакетах. См. параметр use_packages
или параметр командной строки -pkgs
.#field{}
в gpb.hrl для функции get_msg_defs
, но этого можно избежать. эту зависимость, используя также опцию defs_as_proplists
или -pldefs
.самоанализ
gpb генерирует несколько функций для проверки сообщений, перечислений и сервисов:
get_msg_defs()
(или get_proto_defs()
если установлен introspect_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"
если прото-файл был "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", ...]
Есть также некоторые функции информации о версии:
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'
(в GeneratedCode.hrl)gpb также может генерировать самоописание прото-файла. Самоописание — это описание файла прототипа, закодированное в двоичный файл с использованием descriptor.proto, который поставляется с библиотекой буферов протокола Google. Обратите внимание, что такое закодированное самоописание не будет побайтно идентично тому, что компилятор буферов протокола Google сгенерирует для того же прототипа, но должно быть примерно эквивалентно.
Ошибочно закодированные сообщения и поля protobuf обычно приводят к сбою декодера. Примерами таких ошибочных кодировок являются:
Карты
Gpb может генерировать кодеры/декодеры для карт.
Опция maps_unset_optional
может использоваться для указания поведения отсутствующих необязательных полей: будут ли они опущены на картах или они присутствуют, но имеют undefined
значение, как для записей.
Отчеты об ошибках в файлах .proto
Gpb не очень хорош в составлении отчетов об ошибках, особенно в отношении ссылок на ошибки, например ссылки на сообщения, которые не определены. Возможно, вы захотите сначала проверить с помощью protoc
, что файлы .proto действительны, прежде чем передавать их в gpb.
Информацию о том, как использовать gpb с rebar3, см. на странице https://rebar3.org/docs/configuration/plugins/#protocol-buffers.
В rebar есть поддержка gpb начиная с версии 2.6.0. См. раздел прото-компилятора файла rebar.sample.config по адресу https://github.com/rebar/rebar/blob/master/rebar.config.sample.
Для более старых версий rebar — до 2.6.0 — в тексте ниже описано, как действовать:
Поместите файлы .proto, например, в подкаталог proto/
. Любой подкаталог, кроме src/, подойдет, поскольку rebar попытается использовать другой компилятор protobuf для любого .proto, который он найдет в подкаталоге src/. Вот несколько строк файла 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 извлекается из последнего тега git, соответствующего NM, где 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 для получения информации об экспорте gpb из git.)
Номер версии из приведенной выше команды git describe
будет выглядеть так:
..
(в мастере на Github)..--g
(в ветках или между выпусками) Номер версии в главной ветке gpb на Github всегда должен быть целым числом с точками, чтобы быть совместимым с reltool. Другими словами, каждое нажатие на главную ветку Github считается релизом, и номер версии увеличивается. Чтобы обеспечить это, в подкаталоге helpers есть хук pre-push
и два скрипта: install-git-hooks
и tag-next-minor-vsn
. Файл журнала изменений не обязательно будет отражать все незначительные изменения версий, а только важные обновления.
Места для обновления при создании новой версии:
Процесс сборки gpb ожидает, что (не поверхностное) рабочее дерево git с тегами будет иметь правильную нумерацию версий, как описано в разделе «Нумерация версий», но сборку можно выполнить и вне git. Для этого у вас есть два варианта:
gpb.vsn
с версией в первой строке.helpers/mk-versioned-archive
, затем распакуйте архив и выполните сборку внутри него. Если вы создаете версионный архив в рабочем дереве git, версия будет установлена автоматически, в противном случае вам придется указать ее вручную. Запустите mk-versioned-archive --help
, чтобы получить информацию о том, какие параметры использовать.
При загрузке с Github архивы gpb-
Если вы используете автоматические архивы исходного кода zip или tar.gz Github, вам нужно будет либо создать файл gpb.vsn
, как описано выше, либо заново создать версионный архив с помощью сценария mk-versioned-archive
и --override-version=
опция --override-version=
(или, возможно, опция или --override-version-from-cwd-path
если имя каталога содержит правильную версию.)
Вклады приветствуются, желательно в виде запросов на включение, исправлений git или запросов на выборку git. Вот несколько руководящих принципов:
rebar clean; rebar eunit && rebar doc
Подробности смотрите в журнале изменений.
Значение по умолчанию для опции maps_unset_optional
изменено на omitted
с present_undefined
. Это касается только кода, созданного с помощью опций карт (-maps). Проекты, в которых этот параметр уже задан явно, не будут затронуты. Проекты, которые полагались на значение по умолчанию, равное present_undefined
должны будут явно установить этот параметр, чтобы выполнить обновление до версии 4.0.0.
Для спецификаций типов изменено значение по умолчанию, позволяющее генерировать их, когда это возможно. Опцию {type_specs,false}
(-no_type) можно использовать, чтобы избежать создания спецификаций типов.