BaseMax / c-binary-serialization
A lightweight, portable binary serialization library for C with zero external dependencies. Provides a clean API for converting structured data to compact binary format and back, with built-in support for endianness handling, versioning, schema evolution, and validation.
README
C Binary Serialization Library
A lightweight, portable binary serialization library for C with zero external dependencies. Provides a clean API for converting structured data to compact binary format and back, with built-in support for endianness handling, versioning, schema evolution, and validation.
Features
- Compact Binary Format: Efficient space utilization with minimal overhead
- Endianness Safe: Automatic conversion to network byte order (big-endian)
- Schema Versioning: Built-in version tracking for format evolution
- Schema Evolution: Read old data with new code, with sensible defaults
- Data Validation: CRC32 checksum for integrity verification
- File & Network I/O: Ready-to-use functions for common use cases
- Type Safety: Explicit type encoding for validation
- Zero Dependencies: Pure C99, only standard library required
- Clean API: Simple, consistent interface for all operations
Supported Data Types
- Integers:
int8_t,uint8_t,int16_t,uint16_t,int32_t,uint32_t,int64_t,uint64_t - Floating Point:
float,double - Boolean:
bool - Strings: Null-terminated C strings (with length prefix)
- Raw Bytes: Arbitrary byte arrays (with length prefix)
Building
make all
This builds:
libbinserde.a- Static library- Example programs:
example_file,example_network,example_evolution
Running Examples
make run-examples
Or run individually:
./example_file
./example_network
./example_evolution
Cleaning
make clean
Quick Start
Basic Serialization/Deserialization
#include "binserde.h"
/* Serialization */
uint8_t buffer[1024];
binserde_writer_t writer;
binserde_writer_init(&writer, buffer, sizeof(buffer), 1); // version 1
binserde_write_header(&writer);
binserde_write_int32(&writer, 42);
binserde_write_string(&writer, "Hello, World!");
binserde_write_checksum(&writer);
size_t data_size = binserde_writer_position(&writer);
/* Deserialization */
binserde_reader_t reader;
binserde_reader_init(&reader, buffer, data_size);
uint32_t version;
binserde_read_header(&reader, &version);
int32_t value;
binserde_read_int32(&reader, &value);
char *text;
binserde_read_string(&reader, &text);
binserde_validate_checksum(&reader);
free(text);
File I/O
/* Write to file */
binserde_write_file("data.bin", buffer, data_size);
/* Read from file */
uint8_t *file_buffer;
size_t file_size;
binserde_read_file("data.bin", &file_buffer, &file_size);
/* ... use data ... */
free(file_buffer);
Binary Format Specification
The binary format consists of:
[Header]
- Magic Number (4 bytes): 0x42534445 ("BSDE")
- Version (4 bytes): Schema version number
[Data]
- Type-specific encoding (see below)
[Footer]
- Checksum (4 bytes): CRC32 of all preceding data
Type Encoding
-
Integers: Network byte order (big-endian)
- 1 byte: int8_t, uint8_t
- 2 bytes: int16_t, uint16_t
- 4 bytes: int32_t, uint32_t
- 8 bytes: int64_t, uint64_t
-
Floating Point: IEEE 754, network byte order
- 4 bytes: float
- 8 bytes: double
-
Boolean: 1 byte (0 = false, 1 = true)
-
String: Length-prefixed
- Length (4 bytes, uint32_t)
- Data (length bytes, UTF-8)
-
Bytes: Length-prefixed
- Length (4 bytes, uint32_t)
- Data (length bytes)
Schema Evolution
The library supports schema evolution through version numbers:
- Version in Header: Each serialized object includes schema version
- Forward Compatibility: Old readers can detect new versions
- Backward Compatibility: New readers can handle old data
- Default Values: Missing fields get sensible defaults
Example:
/* Version 1 */
typedef struct {
int32_t id;
char name[64];
} UserV1;
/* Version 2 (with new fields) */
typedef struct {
int32_t id;
char name[64];
char email[64]; // New field
uint32_t created_at; // New field
} UserV2;
/* Deserializer handles both versions */
err = binserde_read_header(&reader, &version);
if (version >= 2) {
// Read new fields
} else {
// Use defaults for new fields
}
See example_evolution.c for a complete example.
Error Handling
All functions return binserde_error_t:
BINSERDE_OK(0): SuccessBINSERDE_ERR_NULL_PTR: Null pointer argumentBINSERDE_ERR_BUFFER_TOO_SMALL: Output buffer too smallBINSERDE_ERR_INVALID_DATA: Invalid or corrupted dataBINSERDE_ERR_VERSION_MISMATCH: Schema version mismatchBINSERDE_ERR_CHECKSUM_FAILED: Checksum validation failedBINSERDE_ERR_IO: File I/O error
Use binserde_error_string() to get human-readable error messages.
Network Usage
The library uses network byte order (big-endian) for all multi-byte values, making it suitable for network protocols:
/* Sender */
binserde_write_uint16(&writer, packet_id);
binserde_write_string(&writer, payload);
binserde_write_checksum(&writer);
size_t packet_size = binserde_writer_position(&writer);
send(socket, buffer, packet_size, 0); // POSIX sockets
/* Receiver */
recv(socket, recv_buffer, sizeof(recv_buffer), 0);
binserde_reader_init(&reader, recv_buffer, received_bytes);
binserde_read_header(&reader, &version);
binserde_read_uint16(&reader, &packet_id);
// ... read rest ...
binserde_validate_checksum(&reader);
See example_network.c for a complete simulation.
API Reference
Initialization
binserde_error_t binserde_writer_init(binserde_writer_t *writer,
uint8_t *buffer,
size_t capacity,
uint32_t version);
binserde_error_t binserde_reader_init(binserde_reader_t *reader,
const uint8_t *buffer,
size_t size);
Header Operations
binserde_error_t binserde_write_header(binserde_writer_t *writer);
binserde_error_t binserde_read_header(binserde_reader_t *reader, uint32_t *version);
Write Operations
binserde_error_t binserde_write_int8(binserde_writer_t *writer, int8_t value);
binserde_error_t binserde_write_uint8(binserde_writer_t *writer, uint8_t value);
binserde_error_t binserde_write_int16(binserde_writer_t *writer, int16_t value);
binserde_error_t binserde_write_uint16(binserde_writer_t *writer, uint16_t value);
binserde_error_t binserde_write_int32(binserde_writer_t *writer, int32_t value);
binserde_error_t binserde_write_uint32(binserde_writer_t *writer, uint32_t value);
binserde_error_t binserde_write_int64(binserde_writer_t *writer, int64_t value);
binserde_error_t binserde_write_uint64(binserde_writer_t *writer, uint64_t value);
binserde_error_t binserde_write_float(binserde_writer_t *writer, float value);
binserde_error_t binserde_write_double(binserde_writer_t *writer, double value);
binserde_error_t binserde_write_bool(binserde_writer_t *writer, bool value);
binserde_error_t binserde_write_string(binserde_writer_t *writer, const char *value);
binserde_error_t binserde_write_bytes(binserde_writer_t *writer,
const uint8_t *data, uint32_t length);
Read Operations
binserde_error_t binserde_read_int8(binserde_reader_t *reader, int8_t *value);
binserde_error_t binserde_read_uint8(binserde_reader_t *reader, uint8_t *value);
binserde_error_t binserde_read_int16(binserde_reader_t *reader, int16_t *value);
binserde_error_t binserde_read_uint16(binserde_reader_t *reader, uint16_t *value);
binserde_error_t binserde_read_int32(binserde_reader_t *reader, int32_t *value);
binserde_error_t binserde_read_uint32(binserde_reader_t *reader, uint32_t *value);
binserde_error_t binserde_read_int64(binserde_reader_t *reader, int64_t *value);
binserde_error_t binserde_read_uint64(binserde_reader_t *reader, uint64_t *value);
binserde_error_t binserde_read_float(binserde_reader_t *reader, float *value);
binserde_error_t binserde_read_double(binserde_reader_t *reader, double *value);
binserde_error_t binserde_read_bool(binserde_reader_t *reader, bool *value);
binserde_error_t binserde_read_string(binserde_reader_t *reader, char **value);
binserde_error_t binserde_read_bytes(binserde_reader_t *reader,
uint8_t **data, uint32_t *length);
Note: binserde_read_string() and binserde_read_bytes() allocate memory.
Caller must free the returned pointers.
Validation
binserde_error_t binserde_write_checksum(binserde_writer_t *writer);
binserde_error_t binserde_validate_checksum(binserde_reader_t *reader);
File I/O
binserde_error_t binserde_write_file(const char *filename,
const uint8_t *buffer,
size_t size);
binserde_error_t binserde_read_file(const char *filename,
uint8_t **buffer,
size_t *size);
Note: binserde_read_file() allocates memory. Caller must free the buffer.
Utilities
size_t binserde_writer_position(const binserde_writer_t *writer);
size_t binserde_reader_position(const binserde_reader_t *reader);
const char *binserde_error_string(binserde_error_t error);
Examples
Three complete examples are provided:
- example_file.c - File I/O with serialization
- example_network.c - Network packet serialization (simulated)
- example_evolution.c - Schema versioning and evolution
Build and run with:
make run-examples
Design Principles
- Simplicity: Clean, minimal API that's easy to understand
- Safety: Comprehensive bounds checking, no buffer overruns
- Portability: Pure C99, works on any platform
- Efficiency: Compact encoding, minimal overhead
- Reliability: Checksum validation, version tracking
- Flexibility: Support for schema evolution
Use Cases
- Configuration Files: Compact, versioned config storage
- Network Protocols: Custom binary protocols with integrity checks
- Data Persistence: Save/load application state
- IPC: Inter-process communication with structured data
- Embedded Systems: Minimal footprint serialization
- Game Development: Save files, network sync
Performance
The library is designed for efficiency:
- Zero-copy where possible
- Minimal memory allocations
- Fast bitwise operations for endianness
- Inline functions for hot paths
- No dynamic dispatch overhead
License
See LICENSE file for details.
Contributing
Contributions welcome! Please ensure:
- Code follows existing style (C99)
- All examples still build and run
- No new external dependencies
- Documentation is updated
Roadmap
Potential future enhancements:
- Compression support (optional)
- Stream API for large data
- Schema definition format
- Code generation from schemas
- Additional checksum algorithms
