malloc() 和 serial_alloc() 行为之间的区别
在这个例子中,我们将实现自定义malloc()和free()函数,并在自定义分配器中使用它们serial_allocator,这将把数据放入由 Protobuf-C 库静态分配的连续内存块中。
malloc()如何和工作之间的区别serial_alloc()如下图所示。
堆上 malloc() 分配的表示
静态缓冲区上 serial_alloc() “分配”的表示
通常,malloc() 在堆上“随机”分配内存,导致内存碎片整理。我们的自定义 serial_alloc() 按顺序在静态分配的内存上“分配”内存,这导致没有堆使用和内存碎片整理。
环境设置本文中显示的代码在 Ubuntu 22.04 LTS 上进行了测试。
要安装protoc-c编译器Protocol Buffers C Runtime,只需运行:
sudo apt install libprotobuf-c-dev protobuf-c-compiler
通过运行检查它是否有效:
protoc-c --version
那应该返回已安装的版本:
纯文本
protobuf-c 1.3.3
libprotoc 3.12.4
如果您需要源代码或需要从源代码构建,请参阅GitHub 存储库。
信息在此示例中,在文件中创建了一个简单的MessageProtobuf 消息。message.proto
原始缓冲区
syntax = "proto3";
message Message
{
bool flag = 1;
float value = 2;
}
代码生成
要生成代码,只需运行:
protoc-c -I=. --c_out=. message.proto
这将生成两个文件:message.pb-ch和message.pb-cc。
程序编译要使用生成的代码编译您的 C 程序并链接到protobuf-c library,您只需运行:
gcc -Wall -Wextra -Wpedantic main.c message.pb-c.c -lprotobuf-c -o protobuf-c-custom_allocator
代码概述
一般来说,代码使用 Protobuf-C 对静态缓冲区进行序列化/编码/打包pack_buffer,然后对另一个静态缓冲区进行反序列化/解码/解包,out:
C
#include "message.pb-ch"br#include <stdbool.h>br#include <stdio.h>br#include <string.h>brbr静态 uint8_t pack_buffer [ 100 ];brbr主函数 ()br{br 留言 ;_br message__init ( & in );brbr 在. 标志 = 真;br 在. 值 = 1.234f ;brbr // 序列化:br message__pack ( & in , pack_buffer );brbr // 反序列化:br unpacked_message_wrapper 出来;br 消息* outPtr = unpack_to_message_wrapper_from_buffer ( message__get_packed_size ( & in ), pack_buffer , & out );brbr 如果(空 != outPtr)br {br 断言(in.flag == out.message.flag);_ _ _ _ _ br 断言(in.value == out.message.value);_ _ _ _ _ brbr 断言(in.flag == outPtr - > flag ); br 断言(in.value == outPtr - > value ); br }br 别的br {br printf ( "错误: 解压到串行缓冲区失败!可能 MAX_UNPACKED_MESSAGE_LENGTH 太小或请求的大小不正确。\n" );br }brbr 返回 0 ;br}
在unpack_to_message_wrapper_from_buffer()中,我们创建对象并用和函数ProtobufCAllocator填充它(替换和)。然后,我们通过调用和传递来解包消息:serial_alloc()serial_free()malloc()free()message__unpackserial_allocator
C
Message* unpack_to_message_wrapper_from_buffer(const size_t packed_message_length, const uint8_t* buffer, unpacked_message_wrapper* wrapper)
{
wrapper->next_free_index = 0;
// Here is the trick: We pass `wrapper` (not wrapper.buffer) as `allocator_data`, to track number of allocations in `serial_alloc()`.
ProtobufCAllocator serial_allocator = {.alloc = serial_alloc, .free = serial_free, .allocator_data = wrapper};
return message__unpack(&serial_allocator, packed_message_length, buffer);
}
malloc()-Based 与serial_alloc-Based 方法的比较
您可以在下面找到默认 Protobuf-C 行为(malloc()基于 -)和使用自定义分配器的自定义行为之间的比较:
Protobuf-C 默认行为 →使用动态内存分配:
C
tatic uint8_t buffer[SOME_BIG_ENOUGH_SIZE];
...
// NULL in this context means -> use malloc():
Message* parsed = message__unpack(NULL, packed_size, bufer);
// dynamic memory allocation occurred above
...
// somewhere below memory must be freed:
free(me)
Protobuf-C 使用自定义分配器 →不使用动态内存分配:
C
// statically allocated buffer inside some wrapper around the unpacked proto message:
typedef struct
{
uint8_t buffer[SOME_BIG_ENOUGH_SIZE];
...
} unpacked_message_wrapper;
...
// malloc and free functions replacements:
static void* serial_alloc(void* allocator_data, size_t size) { ... }
static void serial_free(void* allocator_data, void* ignored) { ... }
...
ProtobufCAllocator serial_allocator = { .alloc = serial_alloc,
.free = serial_free,
.allocator_data = wrapper};
// now, instead of NULL we pass serial_allocator:
if (NULL == message__unpack(&serial_allocator, packed_message_length, input_buffer))
{
printf("Unpack to serial buffer failed!\n");
}
最有趣的部分是unpacked_message_wrapper结构和serial_alloc() serial_free()实现,下面将对其进行解释。
围绕原型消息进行结构unpacked_message_wrapperstruct 只是一个简单的 proto 包装器,并且在 union 中Message足够大,可以存储解压缩的数据并跟踪该缓冲区中的已用空间:buffernext_free_index
C
#define MAX_UNPACKED_MESSAGE_LENGTH 100
typedef struct
{
size_t next_free_index;
union
{
uint8_t buffer[MAX_UNPACKED_MESSAGE_LENGTH];
Message message; // Replace `Message` with your own type - generated from your own .proto message
};
} unpacked_message_wrapper;
对象的大小Message不会改变它的大小,但Message可以是一个扩展的 .proto(请参阅本文的“提示和技巧”部分),例如包含重复字段,这通常涉及多个malloc()调用。所以你可能需要更多的尺寸,而不是 Message它本身的尺寸。为了实现这一点,buffer和message成员在一个工会中。
MAX_UNPACKED_MESSAGE_LENGTH必须足够大以适应最坏的情况。有关更多信息,请查看“提示和技巧”部分。
该结构的目的是将预定义的内存缓冲区unpacked_message_wrapper保存在一个位置,并跟踪该缓冲区上的“分配”。
实施serial_alloc()和serial_free()的签名serial_alloc()遵循以下ProtobufCAllocator要求:
C
static void* serial_alloc(void* allocator_data, size_t size)
serial_alloc()size在上分配请求allocator_data,然后递增next_free_index到下一个字边界的开头(这是一种优化,将连续的数据块对齐到下一个字边界)。size在解析/解码数据时来自 Protobuf-C 内部。
C
static void* serial_alloc(void* allocator_data, size_t size)
{
void* ptr_to_memory_block = NULL;
unpacked_message_wrapper* const wrapper = (unpacked_message_wrapper*)allocator_data;
// Optimization: Align to next word boundary.
const size_t temp_index = wrapper->next_free_index ((size sizeof(int)) & ~(sizeof(int)));
if ((size > 0) && (temp_index <= MAX_UNPACKED_MESSAGE_LENGTH))
{
ptr_to_memory_block = (void*)&wrapper->buffer[wrapper->next_free_index];
wrapper->next_free_index = temp_index;
}
return ptr_to_memory_block;
}
第一次调用时serial_alloc(),它设置next_free_index为分配的大小并返回指向缓冲区开头的指针:
在第二次调用时,它重新计算next_free_index值并将地址返回到下一个数据块: