在 c 中的 nanopb 中为 protobuf 消息中的重复字段创建回调和结构
creating callbacks and structs for repeated field in a protobuf message in nanopb in 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;
}
输出为:
before : arr[0] = 10
Decoded successfully: 17
after : arr[0] = 0
我做错了什么?
您可以使用一些 来强制 nanopb 生成具有静态分配数组的结构。
然而,nanopb 原型的默认行为是生成一个回调函数,在编码(整个列表)和解码(列表中的每个项目一次)。这在低内存嵌入式系统中有时是首选,因为您不需要一次分配多个项目。
因此,对于您的 .proto
文件:
message SimpleMessage {
repeated int32 number = 1;
}
你可能会得到类似的东西:
typedef struct _SimpleMessage {
pb_callback_t number;
} SimpleMessage;
这意味着您必须创建自己的回调函数,该函数将连续为每个项目调用。
为简单起见,假设您有一个简单的 "variable length" 列表,如下所示:
#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;
}
为每个项目调用一次解码回调,"appends" 到列表:
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
。如果需要,您的回调还可以将新回调附加到那些嵌套结构。
为了补充 Groo 的回答,以下是对您的具体问题的回答。
1.现在,函数应该包含什么?
Groo 对回调函数提供了很好的解释。 nanopb 存储库中的 network_server
示例也使用回调,可以作为有用的参考:network_server/server.c network_server/client.c
2。数据本身存储在哪里?
随心所欲! nanopb 回调的全部意义在于,它使您可以完全灵活地决定如何存储数据。在某些情况下,您甚至可能希望即时处理数据,而不是将其存储在任何地方。
例如,上面的 network_server
示例从文件系统获取文件名并将它们直接发送到网络 - 这样它就可以处理任意数量的文件而不需要太多内存。
3。我如何访问它/向它分配新数据?
现在这是回调的缺点 - 您必须为您使用的任何存储实现您自己的访问和分配函数。这就是为什么对于大多数常见情况,静态分配(具有固定的最大大小)或动态分配(malloc()
s 需要的内存量)更方便。
我有一个原型消息定义为:
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;
}
输出为:
before : arr[0] = 10
Decoded successfully: 17
after : arr[0] = 0
我做错了什么?
您可以使用一些
然而,nanopb 原型的默认行为是生成一个回调函数,在编码(整个列表)和解码(列表中的每个项目一次)。这在低内存嵌入式系统中有时是首选,因为您不需要一次分配多个项目。
因此,对于您的 .proto
文件:
message SimpleMessage {
repeated int32 number = 1;
}
你可能会得到类似的东西:
typedef struct _SimpleMessage {
pb_callback_t number;
} SimpleMessage;
这意味着您必须创建自己的回调函数,该函数将连续为每个项目调用。
为简单起见,假设您有一个简单的 "variable length" 列表,如下所示:
#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;
}
为每个项目调用一次解码回调,"appends" 到列表:
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
。如果需要,您的回调还可以将新回调附加到那些嵌套结构。
为了补充 Groo 的回答,以下是对您的具体问题的回答。
1.现在,函数应该包含什么?
Groo 对回调函数提供了很好的解释。 nanopb 存储库中的 network_server
示例也使用回调,可以作为有用的参考:network_server/server.c network_server/client.c
2。数据本身存储在哪里?
随心所欲! nanopb 回调的全部意义在于,它使您可以完全灵活地决定如何存储数据。在某些情况下,您甚至可能希望即时处理数据,而不是将其存储在任何地方。
例如,上面的 network_server
示例从文件系统获取文件名并将它们直接发送到网络 - 这样它就可以处理任意数量的文件而不需要太多内存。
3。我如何访问它/向它分配新数据?
现在这是回调的缺点 - 您必须为您使用的任何存储实现您自己的访问和分配函数。这就是为什么对于大多数常见情况,静态分配(具有固定的最大大小)或动态分配(malloc()
s 需要的内存量)更方便。