Создание обратных вызовов и структур для повторного поля в сообщении protobuf в nanopb в c
У меня есть протокольное сообщение, определенное как:
message SimpleMessage {
repeated int32 number = 1;}
теперь, после компиляции, поле имеет pb_callback_t
и я предполагаю написать эту функцию. (без файла.options)
теперь, где и что должна содержать функция? где хранятся сами данные и как я могу получить к ним доступ / назначить новые данные?
* РЕДАКТИРОВАТЬ *
согласно ответу @Groo, это код, который я пробовал:
typedef struct {
int numbers_decoded;
} DecodingState;
bool read_single_number(pb_istream_t *istream, const pb_field_t *field, void **arg)
{
// get the pointer to the custom state
DecodingState *state = (DecodingState*)(*arg);
int32_t value;
if (!pb_decode_varint32(istream, &value))
{
const char * error = PB_GET_ERROR(istream);
printf("Protobuf error: %s", error);
return false;
}
printf("Decoded successfully: %d", value);
state->numbers_decoded++;
return true;
}
int main(void) {
int32_t arr[3] = {10, 22, 342};
uint8_t buffer[128];
size_t message_length;
bool status;
SimpleMessage simple = SimpleMessage_init_zero;
printf("\nbefore : arr[0] = %d\n",arr[0]);
// set the argument and the callback fn
simple.number.arg = &arr;
simple.number.funcs.decode = read_single_number;
pb_ostream_t ostream = pb_ostream_from_buffer(buffer, sizeof(buffer));
status = pb_encode(&ostream, SimpleMessage_fields, &simple);
message_length = ostream.bytes_written;
SimpleMessage simple1 = SimpleMessage_init_zero;
simple = simple1;
arr[0] = 0;
pb_istream_t istream = pb_istream_from_buffer(buffer, message_length);
// this function will call read_single_number several times
status = pb_decode(&istream, SimpleMessage_fields, &simple);
printf("\nafter : arr[0] = %d\n",arr[0]);
return EXIT_SUCCESS;
}
и вывод:
перед: обр [0] = 10
Декодировано успешно: 17
после: arr[0] = 0
что я делаю не так?
2 ответа
Вы можете использовать некоторые прототипные флаги для Nanopb, чтобы заставить Nanopb генерировать структуры со статически размещенными массивами.
Однако стандартное поведение протогена nanopb заключается в создании функции обратного вызова, которая вызывается nanopb во время кодирования (один раз для всего списка) и декодирования (один раз для каждого элемента в списке). Это иногда предпочитается во встроенных системах с малым объемом памяти, поскольку вам не нужно выделять более одного элемента за раз.
Итак, для вашего .proto
файл:
message SimpleMessage {
repeated int32 number = 1;
}
Вы можете получить что-то вроде:
typedef struct _SimpleMessage {
pb_callback_t number;
} SimpleMessage;
Это означает, что вам придется создать собственную функцию обратного вызова, которая будет вызываться для каждого элемента подряд.
Итак, для простоты, скажем, у вас есть простой список "переменной длины", подобный этому:
#define MAX_NUMBERS 32
typedef struct
{
int32_t numbers[MAX_NUMBERS];
int32_t numbers_count;
}
IntList;
// add a number to the int list
void IntList_add_number(IntList * list, int32_t number)
{
if (list->numbers_count < MAX_NUMBERS)
{
list->numbers[list->numbers_count] = number;
list->numbers_count++;
}
}
Очевидно, что для такого примера использование обратных вызовов не имело бы никакого смысла, но это делает пример простым.
Кодировка обратного вызова должна выполнять итерацию по списку и записывать тег protobuf и значение для каждого элемента в списке:
bool SimpleMessage_encode_numbers(pb_ostream_t *ostream, const pb_field_t *field, void * const *arg)
{
IntList * source = (IntList*)(*arg);
// encode all numbers
for (int i = 0; i < source->numbers_count; i++)
{
if (!pb_encode_tag_for_field(ostream, field))
{
const char * error = PB_GET_ERROR(ostream);
printf("SimpleMessage_encode_numbers error: %s", error);
return false;
}
if (!pb_encode_svarint(ostream, source->numbers[i]))
{
const char * error = PB_GET_ERROR(ostream);
printf("SimpleMessage_encode_numbers error: %s", error);
return false;
}
}
return true;
}
Декодирование обратного вызова вызывается один раз для каждого элемента и "добавляется" в список:
bool SimpleMessage_decode_single_number(pb_istream_t *istream, const pb_field_t *field, void **arg)
{
IntList * dest = (IntList*)(*arg);
// decode single number
int64_t number;
if (!pb_decode_svarint(istream, &number))
{
const char * error = PB_GET_ERROR(istream);
printf("SimpleMessage_decode_single_number error: %s", error);
return false;
}
// add to destination list
IntList_add_number(dest, (int32_t)number);
return true;
}
С этими двумя на месте, вы должны быть осторожны, чтобы назначить правильный обратный вызов для правильной функции:
uint8_t buffer[128];
size_t total_bytes_encoded = 0;
// encoding
{
// prepare the actual "variable" array
IntList actualData = { 0 };
IntList_add_number(&actualData, 123);
IntList_add_number(&actualData, 456);
IntList_add_number(&actualData, 789);
// prepare the nanopb ENCODING callback
SimpleMessage msg = SimpleMessage_init_zero;
msg.number.arg = &actualData;
msg.number.funcs.encode = SimpleMessage_encode_numbers;
// call nanopb
pb_ostream_t ostream = pb_ostream_from_buffer(buffer, sizeof(buffer));
if (!pb_encode(&ostream, SimpleMessage_fields, &msg))
{
const char * error = PB_GET_ERROR(&ostream);
printf("pb_encode error: %s", error);
return;
}
total_bytes_encoded = ostream.bytes_written;
printf("Encoded size: %d", total_bytes_encoded);
}
И похоже для расшифровки:
// decoding
{
// empty array for decoding
IntList decodedData = { 0 };
// prepare the nanopb DECODING callback
SimpleMessage msg = SimpleMessage_init_zero;
msg.number.arg = &decodedData;
msg.number.funcs.decode = SimpleMessage_decode_single_number;
// call nanopb
pb_istream_t istream = pb_istream_from_buffer(buffer, total_bytes_encoded);
if (!pb_decode(&istream, SimpleMessage_fields, &msg))
{
const char * error = PB_GET_ERROR(&istream);
printf("pb_decode error: %s", error);
return;
}
printf("Bytes decoded: %d", total_bytes_encoded - istream.bytes_left);
}
Если внутри вашего сообщения есть повторяющаяся структура, ваш обратный вызов не будет использовать примитивные функции nanopb (например, pb_decode_varint32
выше), но опять pb_decode
для каждого конкретного типа сообщения. Ваш обратный вызов также может прикрепить новые обратные вызовы к этим вложенным структурам, если это необходимо.
Чтобы дополнить ответ Груо, вот ответы на ваши конкретные вопросы.
1. Теперь, где и что должна содержать функция?
Groo предоставил хорошее объяснение функций обратного вызова. network_server
Пример в репозитории nanopb также использует обратные вызовы и может быть полезной ссылкой: network_server / server.c network_server / client.c
2. Где хранятся сами данные?
Где угодно! Весь смысл обратных вызовов nanopb заключается в том, что он дает вам полную гибкость в принятии решения о том, как хранить ваши данные. В некоторых случаях вы можете даже обрабатывать данные на лету, нигде не сохраняя их.
Например, network_server
Приведенный выше пример получает имена файлов из файловой системы и отправляет их в сеть напрямую - таким образом он может обрабатывать любое количество файлов, не требуя большого объема памяти.
3. Как я могу получить к нему доступ / присвоить ему новые данные?
Теперь это обратная сторона обратных вызовов - вам придется реализовать свои собственные функции доступа и распределения для любого хранилища, которое вы используете. Вот почему для наиболее распространенных случаев: статическое распределение (с фиксированным максимальным размером) или динамическое распределение (которое malloc()
Требуемый объем памяти) удобнее.