Flexible Array Members (FAM)
Motivation
While working on a SIP parser, I encountered a performance issue: allocating memory for each string value in a SIP message was proving costly. To optimize, I aimed to use a single malloc call to allocate memory for the entire SIP message, including all associated strings.
Here's a simplified version of the struct I was using:
struct cmsc_SipMessage {
uint32_t present_mask; // Indicates which fields are present
bool is_request;
uint32_t sip_status_code;
enum cmsc_SipMethod sip_method;
enum cmsc_SipMsgType sip_msg_type;
struct cmsc_SipProtoVer sip_proto_ver;
struct cmsc_SipVia via_l;
// More fields...
};
The goal was to embed a buffer within this structure, avoiding multiple heap allocations for strings. However, I also wanted to avoid wasting memory by allocating a large fixed buffer for messages that might be small (e.g., \~500 characters).
The Problem with Fixed Buffers
Allocating a large buffer (e.g., 2–3 KB) for every message would be wasteful, especially for smaller SIP messages. But shrinking the buffer introduces the risk of overflow. So how do we keep the flexibility and the one-malloc approach?
Enter Flexible Array Members
C99 introduced Flexible Array Members—a powerful feature that allows you to define a variable-sized array as the last member of a struct.
Here’s how to modify the struct:
struct cmsc_SipMessage {
// Previous fields...
struct cmsc_SipVia via_l;
// More fields...
char buffer[]; // Flexible array member
};
The key point: the buffer[] does not contribute to sizeof(struct cmsc_SipMessage). This allows you to dynamically allocate extra space:
malloc(sizeof(struct cmsc_SipMessage) + sizeof(char) * 500);
This allocation gives you space for the struct plus 500 bytes for the buffer.
Example Code
#include <errno.h>
#include <inttypes.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
struct cmsc_StringsBuffer {
uint32_t buffer_size;
uint32_t buffer_len;
char buffer[];
};
int main(void) {
struct cmsc_StringsBuffer *small_buffer =
malloc(sizeof(struct cmsc_StringsBuffer) + (sizeof(char) * 255));
if (!small_buffer) {
puts("Cannot allocate memory for `small_buffer`");
return ENOMEM;
}
small_buffer->buffer_size = 255;
small_buffer->buffer_len = 0;
for (int i = 0; i < small_buffer->buffer_size; i++) {
small_buffer->buffer[i] = (i % 120) + 40;
}
printf("%s\n\n", small_buffer->buffer);
free(small_buffer);
struct cmsc_StringsBuffer *big_buffer =
malloc(sizeof(struct cmsc_StringsBuffer) + (sizeof(char) * 1024));
if (!big_buffer) {
puts("Cannot allocate memory for `big_buffer`");
return ENOMEM;
}
big_buffer->buffer_size = 1024;
big_buffer->buffer_len = 0;
for (int i = 0; i < big_buffer->buffer_size; i++) {
big_buffer->buffer[i] = (i % 120) + 40;
}
printf("%s\n", big_buffer->buffer);
free(big_buffer);
return 0;
}
Limitations of Flexible Array Members
Using FAMs comes with important restrictions:
- Must be the last member - No other field may follow the flexible array in the struct.
- Only one per struct - A struct can have only one flexible array member.
- Not counted in
sizeof- The compiler treats the FAM as 0-length in size calculations. You must add the desired size manually inmalloc. - No initializer support - You can’t initialize the array like
{ ..., {1, 2, 3} }. - Requires C99 or later - Not available in C89/C90. Some compilers support
char buf[0]as a non-standard workaround. - Cannot be in unions - FAMs are not allowed inside
uniontypes. - Needs a size tracker - You'll need to manually store and track the array size, e.g., using
buffer_sizeandbuffer_len.
Conclusion
Flexible Array Members are a valuable tool when you need to manage variable-sized data efficiently, especially in performance-critical applications like SIP parsers. They reduce memory fragmentation and allocation overhead while maintaining clean and predictable memory usage.
With a clear understanding of their limitations and proper usage, FAMs can help you write both high-performance and maintainable C code.