يعد 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 Shell، يقرأ rr("x.hrl")
تعريفات السجل، ويشير v(-1)
إلى قيمة خطوة واحدة سابقة في السجل.
نوع البروتوبوف | نوع إرلانج |
---|---|
مزدوج، تعويم | تعويم () | ما لا نهاية | '-اللانهاية' | نان عند التشفير، يتم قبول الأعداد الصحيحة أيضًا |
int32، int64 uint32، uint64 سينت32، سينت64 ثابت 32، ثابت 64 sfixed32، sfixed64 | عدد صحيح() |
منطقي | صحيح | خطأ شنيع عند التشفير، يتم قبول الأعداد الصحيحة 1 و0 أيضًا |
التعداد | الذرة () فك تشفير التعدادات غير المعروفة إلى عدد صحيح () |
رسالة | سجل (وبالتالي tuple()) أو Map() إذا تم تحديد خيار الخرائط (-maps). |
خيط | سلسلة يونيكود، وبالتالي قائمة الأعداد الصحيحة أو ثنائي () إذا تم تحديد خيار strings_as_binaries (-strbin). عند الترميز، يتم قبول iolists أيضًا |
بايت | ثنائي() عند الترميز، يتم قبول iolists أيضًا |
واحد من | {اسم الحقل المختار، القيمة} أو 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 }
ظهر هذا البناء لأول مرة في الإصدار 2.6.0 من Google protobuf.
message m3 {
oneof u {
int32 a = 1 ;
string b = 2 ;
}
}
يكون الحقل "واحد" اختياريًا دائمًا بشكل تلقائي.
# 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 " }
#{}
لا ينبغي الخلط بينه وبين خرائط إرلانج. ظهر هذا البناء لأول مرة في الإصدار 3.0.0 من Google protobuf (لكل من تركيب 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_
بحيث يمكن للمرء فحص ما إذا كان الحقل موجودًا بالفعل أم لا.
ومع ذلك، في Erlang، الطريقة الطبيعية لتعيين الحقول وقراءتها هي مجرد استخدام بناء الجملة للسجلات (أو الخرائط)، وهذا لا يترك طريقة جيدة في نفس الوقت لنقل ما إذا كان الحقل موجودًا أم لا وقراءة الإعدادات الافتراضية.
لذا فإن النهج في 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
ربما لا يكون البديل الأخير مفيدًا جدًا، ولكنه لا يزال ممكنًا، ويتم تنفيذه للتأكد من اكتماله.
مرجع جوجل
بالنسبة إلى proto3، ليس هناك required
ولا default=
للحقول. بدلاً من ذلك، ما لم يتم وضع علامة optional
عليها، تكون جميع الحقول العددية والسلاسل والبايتات اختيارية ضمنيًا. عند فك التشفير، إذا كان مثل هذا الحقل مفقودًا في الملف الثنائي لفك التشفير، فسيتم دائمًا فك التشفير إلى القيمة الافتراضية الخاصة بالنوع. عند التشفير، يتم تضمين هذه الحقول فقط في الملف الثنائي المشفر الناتج إذا كانت لها قيمة مختلفة عن القيمة الافتراضية الخاصة بالنوع. على الرغم من أن جميع الحقول اختيارية ضمنيًا، إلا أنه يمكن للمرء أيضًا أن يقول أنه على المستوى المفاهيمي، فإن كل هذه الحقول لها قيمة دائمًا. عند فك التشفير، ليس من الممكن تحديد ما إذا كانت القيمة موجودة عند التشفير --- بقيمة خاصة بالنوع --- أم لا.
يتم تمثيل الحقول التي تم وضع علامة عليها optional
بشكل أساسي بنفس الطريقة كما في بناء جملة proto2؛ في السجل، يكون للحقل قيمة undefined
إذا لم يتم تعيينها، وفي الخرائط لا يكون الحقل موجودًا إذا لم يتم تعيينه.
التوصية التي رأيتها إذا كنت بحاجة إلى اكتشاف البيانات "المفقودة" هي تحديد الحقول المنطقية has_
وتعيينها بشكل مناسب. قد يكون البديل الآخر هو استخدام رسائل المجمع المعروفة.
الحقول التي هي رسائل فرعية وأحد الحقول، لا تحتوي على أي نوع افتراضي محدد. يتم تشفير حقل الرسالة الفرعية الذي لم يتم تعيينه بشكل مختلف عن حقل الرسالة الفرعية الذي تم تعيينه للرسالة الفرعية، ويتم فك تشفيره بشكل مختلف. ويسري هذا حتى عندما لا تحتوي الرسالة الفرعية على حقول. إنه يعمل بشكل مشابه قليلاً لأحد الحقول. إما أنه لم يتم تعيين أي من الحقول البديلة، أو تم تعيين أحدها. يختلف التنسيق المشفر، ومن الممكن معرفة الفرق عند فك التشفير.
يوزع ملفات تعريف المخزن المؤقت للبروتوكول ويمكنه إنشاء:
ميزات ملفات تعريف المخزن المؤقت للبروتوكول: يدعم GPB:
packed
default
للحقولallow_alias
(يتم التعامل معه كما لو تم تعيينه على أنه صحيح دائمًا)oneof
(تم تقديمه في بروتوبوف 2.6.0)map<_,_>
(تم تقديمها في الإصدار 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 أيضًا إنشاء وصف ذاتي لملف proto. الوصف الذاتي هو وصف للملف الأولي، المشفر إلى ملف ثنائي باستخدام 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
يوجد في حديد التسليح دعم لـ gpb منذ الإصدار 2.6.0. راجع قسم برنامج التحويل البرمجي الأولي لملف rebar.sample.config على https://github.com/rebar/rebar/blob/master/rebar.config.sample
بالنسبة للإصدارات الأقدم من حديد التسليح --- قبل الإصدار 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
أعلاه
..
(في الصفحة الرئيسية على جيثب)..--g
(في الفروع أو بين الإصدارات) رقم الإصدار الموجود على الفرع الرئيسي لـ gpb على Github يهدف إلى أن يكون دائمًا أعدادًا صحيحة مع نقاط فقط، حتى يكون متوافقًا مع reltool. بمعنى آخر، كل دفعة إلى الفرع الرئيسي لـ Github تعتبر إصدارًا، ويتم تغيير رقم الإصدار. لضمان ذلك، يوجد خطاف git pre-push
وبرنامجين نصيين، install-git-hooks
و tag-next-minor-vsn
، في الدليل الفرعي للمساعدين. لن يعكس ملف ChangeLog بالضرورة جميع تعديلات الإصدار البسيطة، بل التحديثات المهمة فقط.
الأماكن التي يجب تحديثها عند إنشاء إصدار جديد:
تتوقع عملية بناء gpb وجود شجرة عمل git (غير ضحلة)، مع العلامات، للحصول على ترقيم الإصدار الصحيح، كما هو موضح في قسم ترقيم الإصدار، ولكن من الممكن أيضًا البناء خارج git. للقيام بذلك، لديك خياران:
gpb.vsn
، مع الإصدار الموجود في السطر الأولhelpers/mk-versioned-archive
، ثم قم بفك ضغط الأرشيف وإنشاءه بداخله. إذا قمت بإنشاء أرشيف تم إصداره في شجرة عمل git، فسيتم تعيين الإصدار تلقائيًا، وإلا فستحتاج إلى تحديده يدويًا. قم بتشغيل mk-versioned-archive --help
للحصول على معلومات حول الخيارات التي يجب استخدامها.
عند التنزيل من Github، تم إنشاء أرشيفات gpb-
إذا كنت تستخدم أرشيفات Github التلقائية لرمز المصدر zip أو tar.gz، فستحتاج إما إلى إنشاء ملف 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) لتجنب إنشاء مواصفات النوع.