Protobuf چیست؟
فرض کنید میخواهیم بین دو سیستم -مثلا 2 میکروکنترلر متفاوت یا میکروکنترلر و کامپیوتر– دیتای یک struct را به اشتراک بگذاریم. میدانیم برای اینکار نهایتاً باید یک رشته بایت بین این دو سیستم تبادل شود. اما سوال اینجاست که برای تبدیل struct به یک رشته بایت قابل انتقال چه راههایی داریم؟
ابتدا فرض میکنیم که نام struct ما در فرستنده test_struct_t است و متغیری که در برنامه از آن ایجاد کرده و میخواهیم انتقال دهیم test_struct_var نام دارد.
یکی از راهها این است که در گیرنده هم test_struct_t را مثل فرستنده تعریف کنیم. سپس از آدرس test_struct_var به اندازهی سایز آن -sizeof(test_struct_t)- شروع به فرستادن بایت به بایت این متغیر struct کنیم. در گیرنده هم پس از ذخیره اطلاعات دریافتی در حافظه، آدرس شروع این اطلاعات را برابر با اشارهگری به struct تعریف شده -* my_struct_t- کنیم. راه حل ساده و قشنگی به نظر میرسد و همه چیز خوب است، جز اینکه ممکن است مشاهده کنید مقادیر struct فرستنده و گیرنده یکی نیستند!
این اتفاق میتواند ناشی از تفاوت در endianness، padding و یا سایز متفاوت در data typeهای پایه مثل int و double در سیستم مبدا و مقصد باشد.
به جز این مشکل نه چندان کوچک، این را هم باید در نظر داشت که شاید زبان برنامهنویسی متفاوتی در مبدا و مقصد داشته باشیم و نتوان structای مشابه مبدا در مقصد ایجاد کرد.
اما راه جایگزین چیست؟
با این حساب دیدیم که، اگرچه راهحل قبلی ساده و بسیاری از اوقات کار راه انداز است، اما همیشه به کار ما نمیاید و ممکن است گاهی به مشکل بخوریم. خوشبختانه data serialization به عنوان جایگزینی اصولیتر مدتها پیش ابداع شده. در دنیای کامپیوتر به پروسهی تبدیل یک data structure یا object به یک رشته بایت به جهت ذخیره یا ارسال serialization میگویند. برای اینکار هم راههای مختلفی هست. در اینجا پیشنهاد من استفاده از google protobuf هست که علاوه بر پوشش نیازهای بالا به ما امکانات بیشتری مثل فرستادن داده به صورت optional هم میدهد.
متاسفانه google protobuf به صورت پیش فرض از زبان C پشتیبانی نمیکند. اما خبر خوب این هست که ما میتوانیم از کتابخانه Nanopb استفاده کنیم که بر اساس google protobuf و برای زبان C توسعه داده شده.
چگونه از کتابخانه Nanopb استفاده کنیم؟
توضیحاتی که در ادامه قراره بدم یک مثال جامع نیست و اگر میخواهید کاری متفاوت از مثال پیش رو انجام دهید، لازمه که به داکیومنت Nanopb مراجعه کنید.
ابتدا باید فایلی متنی با فرمت proto. تعریف کنیم. در این فایل مشخص میکنیم که میخواهیم message ما به چه شکلی باشد. در ادامه خواهید دید که با کامپایل این فایل به کمک پروژه Nanopb به فایلهای لازم برای پروژه زبان C خود خواهیم رسید. در صورت لزوم میتوانیم همین فایل را برای زبانهای دیگر مثل سی شارپ یا پایتون هم کامپایل کنیم تا خروجی مربوط به این نوع زبانها را دریافت کنیم.
syntax = “proto2”; message test_struct_t { optional string greeting_message = 1; required uint32 id = 2; repeated float temperature = 3; }
test_struct.greeting_message max_length :20
bool has_greeting_message;
test_struct.temperature max_count:2 fixed_count:true
python .\nanopb\generator\nanopb_generator.py .\test.proto
Writing to test.pb.h and test.pb.c
علاوه بر این دو فایل ایجاد شده، شما لازم دارید کتابخانه Nanopb را هم به پروژه خودتان اضافه کنید. برای این کار در همان فولدر کلون شده به دنبال فایلهای .c و .h با نامهای pb_common، pb_decode، pb_encode و همچنین فایل pb.h باشید. بعد از کپی این فایلها در پروژه باید در فایلی که مدنظر دارید تا تابع موردنیازتان را فراخوانی کنید، هدرفایلهای pb_encode یا pb_decode را بسته به اینکه قصد encode یا decode را دارید، اضافه کنید.
#include “pb_encode.h” #include “pb_decode.h”
test_struct_t test_struct_var; pb_byte_t buf[test_struct_t_size + 1U] pb_ostream_t encode_stream = pb_ostream_from_buffer(&buf[1], test_struct_t_size ); bool encode_result = pb_encode(&encode_stream, test_struct_t_fields, &test_struct_var); eeprom_buffer[0] = encode_stream.bytes_written;
uint8_t bytes_written = rx_buffer[0]; if (bytes_written <= test_struct_t_size ) { pb_istream_t stream = pb_istream_from_buffer(rx_buffer, bytes_written); pb_decode(&stream, test_struct_t_fields, &test_struct_var); }