NanoPB:在 C 中使用偏移和大小的自定义数组转换器
另请参阅:C++ 版本:如何在 C++ 中处理自定义数组转换器
NanoPB 是一个面向嵌入式系统的代码体积优化的 Protocol Buffers 实现。本文介绍如何在 C 中使用 NanoPB 处理自定义数组转换器。
Proto 定义
首先,创建一个包含 repeated 字段的 .proto 文件:
custom_array.proto
syntax = "proto3";
package example;
message CustomArrayMessage {
repeated uint32 values = 1;
}生成 NanoPB 代码
使用 .options 文件指定最大计数,然后生成 NanoPB 代码:
创建 custom_array.options:
custom_array.options
example.CustomArrayMessage.values max_count:20然后生成:
generate_nanopb_custom_array.sh
protoc --nanopb_out=. custom_array.proto这将生成 custom_array.pb.h 和 custom_array.pb.c。
使用自定义数组转换器的 C 示例
下面是一个实现带偏移和大小的自定义数组转换器的完整 C 示例:
custom_array_example.c
#include <stdio.h>
#include <stdint.h>
#include <stddef.h>
#include <limits.h>
#include "custom_array.pb.h"
#include "pb_encode.h"
#include "pb_decode.h"
// 自定义数组转换器上下文
typedef struct {
const uint32_t* data;
size_t size;
size_t offset;
size_t count;
} array_context_t;
// 带偏移和大小的 uint32 数组编码 callback
bool uint32_array_encode_callback(pb_ostream_t *stream, const pb_field_t *field, void * const *arg) {
const array_context_t* ctx = (const array_context_t*)*arg;
size_t start = ctx->offset;
size_t end = (ctx->offset + ctx->count < ctx->size) ?
ctx->offset + ctx->count : ctx->size;
for (size_t i = start; i < end; i++) {
if (!pb_encode_tag_for_field(stream, field))
return false;
if (!pb_encode_varint(stream, ctx->data[i]))
return false;
}
return true;
}
int main() {
// 编码消息的缓冲区
uint8_t buffer[256];
size_t message_length;
// 创建包含 10 个元素的数组
uint32_t values[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
// --- 编码 ---
example_CustomArrayMessage message = example_CustomArrayMessage_init_zero;
// 设置上下文:编码第 3-7 个元素(offset=2, count=5)
array_context_t ctx = {
.data = values,
.size = 10,
.offset = 2,
.count = 5
};
// 设置 callback
message.values.funcs.encode = uint32_array_encode_callback;
message.values.arg = &ctx;
// 创建编码流
pb_ostream_t ostream = pb_ostream_from_buffer(buffer, sizeof(buffer));
// 编码消息
if (!pb_encode(&ostream, example_CustomArrayMessage_fields, &message)) {
printf("Encoding failed: %s\n", PB_GET_ERROR(&ostream));
return 1;
}
message_length = ostream.bytes_written;
printf("Encoded %zu bytes (elements 3-7)\n", message_length);
// 打印十六进制转储
printf("Encoded data: ");
for (size_t i = 0; i < message_length; i++) {
printf("%02x ", buffer[i]);
}
printf("\n");
// 打印编码的内容
printf("Encoded values: ");
for (size_t i = 2; i < 2 + 5 && i < 10; i++) {
printf("%u ", values[i]);
}
printf("\n");
return 0;
}编译命令
使用 nanopb 编译该示例。NanoPB 通常通过将源文件直接包含到你的项目来使用:
compile_custom_array_example.sh
gcc -o custom_array_example custom_array_example.c custom_array.pb.c pb_common.c pb_encode.c pb_decode.c -I.**注意:**NanoPB 源文件(pb_common.c、pb_encode.c、pb_decode.c)需要直接与你的项目一起编译。你可以从 NanoPB GitHub 仓库 获取这些文件。
要点
- 上下文结构:使用 struct 传递偏移、计数和数据指针
- 偏移逻辑:从指定偏移处开始编码
- 计数限制:在偏移之后最多编码 count 个元素
- Callback 模式:使用 pb_callback_t 配合自定义编码函数
- 灵活性:无需复制即可编码数组的任意切片
- 内存效率:无需创建临时数组
- KKS-Firmware 模式:基于 KKS-Firmware 中的 ArrayConverterWithOffsetAndSize
何时使用自定义数组转换器
- 当你需要编码大型数组的子集时
- 当你想避免复制数据时
- 当实现分页或分块时
- 当数组布局不匹配 protobuf 要求时
- 当你需要数组的自定义编码逻辑时
预期输出
custom_array_expected_output.txt
Encoded 10 bytes (elements 3-7)
Encoded data: 08 03 08 04 08 05 08 06 08 07
Encoded values: 3 4 5 6 7 注意只有元素 3、4、5、6、7 被编码(从偏移 2 开始的 5 个元素)。
进阶:类似模板的通用实现
对于更通用的 C 实现(模拟模板):
custom_array_generic.c
#include <stddef.h>
#include <limits.h>
#define MAX_OFFSET_SIZE (SIZE_MAX / 2)
typedef struct {
const void* data;
size_t element_size;
size_t size;
size_t offset;
size_t count;
bool (*encode_element)(pb_ostream_t*, const pb_field_t*, const void*);
} generic_array_context_t;
bool generic_array_encode_callback(pb_ostream_t *stream, const pb_field_t *field, void * const *arg) {
const generic_array_context_t* ctx = (const generic_array_context_t*)*arg;
size_t start = ctx->offset;
size_t end = (ctx->offset + ctx->count < ctx->size) ?
ctx->offset + ctx->count : ctx->size;
const uint8_t* data = (const uint8_t*)ctx->data;
for (size_t i = start; i < end; i++) {
const void* element = data + (i * ctx->element_size);
if (!ctx->encode_element(stream, field, element)) {
return false;
}
}
return true;
}实际应用
此模式在 KKS-Firmware 中用于:
- 仅编码更大通道数组中的活动通道
- 发送部分传感器数据以节省带宽
- 实现差分更新
- 高效处理稀疏数组
与 C++ 的区别
C 版本与 C++ 版本在以下几个方面不同:
- 没有模板:使用 struct 和函数指针代替
- 手动上下文管理:必须显式传递上下文
- 没有 std::array:使用带大小跟踪的普通 C 数组
- 没有 std::algorithm:手动实现迭代
- 相同的 callback 模式:两者都使用 pb_callback_t
更多 NanoPB 文章
- C++ 中的基本标量类型
- C 中的基本标量类型
- C++ 中的字符串类型
- C 中的字符串类型
- C++ 中的 bytes 类型
- C 中的 bytes 类型
- C++ 中的可选字段
- C 中的可选字段
- C++ 中的 repeated 字段/数组
- C 中的 repeated 字段/数组
- C++ 中的枚举
- C 中的枚举
- C++ 中的嵌套消息
- C 中的嵌套消息
- C++ 中的 oneof/union 类型
- C 中的 oneof/union 类型
- C++ 中的自定义数组转换器
Check out similar posts by category:
Embedded, C/C++, Protocol Buffers
If this post helped you, please consider buying me a coffee or donating via PayPal to support research & publishing of new posts on TechOverflow