BeBytes
BeBytes is a trait wrapper around the BeBytes derive crate.
BeBytes Derive
Derive is a procedural macro crate that provides a custom derive macro for generating serialization and deserialization methods for network structs in Rust. The macro generates code to convert the struct into a byte representation (serialization) and vice versa (deserialization) supporting both big endian and little endian byte orders. It aims to simplify the process of working with network protocols and message formats by automating the conversion between Rust structs and byte arrays.
For more information, see the BeBytes Derive crate.
Usage
To use BeBytes, add it as a dependency in your Cargo.toml file:
[]
= "2.9.0"
Then, import the BeBytes trait from the bebytes crate and derive it for your struct:
use BeBytes;
// Using big-endian serialization
// Using little-endian serialization
// Deserializing from big-endian bytes
// Deserializing from little-endian bytes
Features
The BeBytes derive macro generates the following methods for your struct:
field_size() -> usize: A method to calculate the size (in bytes) of the struct.
Big-endian methods:
try_from_be_bytes(&[u8]) -> Result<(Self, usize), BeBytesError>: A method to convert a big-endian byte slice into an instance of your struct. It returns a Result containing the deserialized struct and the number of consumed bytes.to_be_bytes(&self) -> Vec<u8>: A method to convert the struct into a big-endian byte representation. It returns aVec<u8>containing the serialized bytes.
Little-endian methods:
try_from_le_bytes(&[u8]) -> Result<(Self, usize), BeBytesError>: A method to convert a little-endian byte slice into an instance of your struct. It returns a Result containing the deserialized struct and the number of consumed bytes.to_le_bytes(&self) -> Vec<u8>: A method to convert the struct into a little-endian byte representation. It returns aVec<u8>containing the serialized bytes.
Buffer Methods:
to_be_bytes_buf(&self) -> Bytes: Convert to big-endian buffer.to_le_bytes_buf(&self) -> Bytes: Convert to little-endian buffer.encode_be_to<B: BufMut>(&self, buf: &mut B) -> Result<(), BeBytesError>: Write directly to buffer (big-endian).encode_le_to<B: BufMut>(&self, buf: &mut B) -> Result<(), BeBytesError>: Write directly to buffer (little-endian).
Bit Field Manipulation
BeBytes provides fine-grained control over bit fields through the bits attribute:
The bits attribute takes a single parameter:
bits(n): The number of bits this field uses
Key points:
- Bit positions are automatically calculated based on field order
- Bits fields MUST complete a full byte before any non-bits field
- The sum of all bits within a group must equal 8 (or a multiple of 8)
Multi-Byte Bit Fields
BeBytes supports bit manipulation on all integer types from u8/i8 to u128/i128:
The same rules apply - all bits fields must complete a byte boundary together.
Enum Bit Packing
Enums can be used with the #[bits()] attribute for automatic bit-width calculation. While #[repr(u8)] is not strictly required, it is recommended as it makes the u8 constraint explicit and provides compile-time guarantees:
// Recommended: ensures discriminants fit in u8 at compile time
Key features:
- Automatic bit calculation:
ceil(log2(max_discriminant + 1)) - No need to specify the bit width in both enum definition and usage
- Type-safe conversion with generated
TryFrom<u8>implementation - Supports byte-spanning fields automatically
- Compile-time validation: discriminants exceeding u8 range (255) will produce an error
- Works without
#[repr(u8)], but using it is recommended for clarity and compile-time safety
Flag Enums
BeBytes supports flag-style Enums marked with #[bebytes(flags)]. These Enums automatically implement bitwise operations (|, &, ^, !) allowing them to be used as bit flags:
// Usage
let read_write = Read | Write; // = 3
let all_perms = Read | Write | Execute | Delete; // = 15
// Check if a flag is set
assert!;
assert!;
// Toggle flags
let perms = Read | Execute;
let toggled = perms ^ Execute as u8; // Removes Execute
// Validate flag combinations
assert_eq!; // Valid: Read|Write|Execute
assert_eq!; // Invalid: 16 is not a valid flag
Key features:
- All Enum variants must have power-of-2 values (1, 2, 4, 8, etc.)
- Zero value is allowed for "None" or empty flags
- Automatic implementation of bitwise operators
contains()method to check if a flag is setfrom_bits()method to validate flag combinations
Supported Types
BeBytes supports:
- Primitives:
u8,u16,u32,u64,u128,i8,i16,i32,i64,i128 - Characters:
charwith full Unicode support - Strings: Standard Rust
Stringtype with attributes for size control - Arrays:
[u8; N],[u16; N], etc. - Enums with named fields (serialized as a single byte)
- Enums with
#[bits()]for automatic bit-width calculation Option<T>where T is a primitive- Nested structs that also implement
BeBytes Vec<T>with some restrictions (see below)
String Support
BeBytes provides comprehensive support for Rust's standard String type with flexible size control:
1. Fixed-Size Strings
Use #[With(size(N))] for strings that must be exactly N bytes:
Note: Fixed-size strings must be padded to the exact length by the user.
2. Variable-Size Strings
Use #[FromField(field_name)] to specify the size from another field:
3. Unbounded Strings
A string as the last field will consume all remaining bytes:
String Features
- UTF-8 Validation: All strings are validated during deserialization
- Standard Types: Uses Rust's familiar
Stringtype - Memory Safe: Proper bounds checking and validation
- No-std Support: Works in embedded environments (requires
alloc)
Character Support
The char type is fully supported with proper Unicode validation:
Characters are stored as 4-byte Unicode scalar values with validation to ensure they represent valid Unicode code points.
Size Expressions (New in 2.3.0)
BeBytes now supports dynamic field sizing using mathematical expressions. This powerful feature enables protocol implementations where field sizes depend on other fields:
Supported Operations
- Mathematical:
+,-,*,/,%with parentheses - Field References: Reference any previously defined field
- Complex Expressions:
#[With(size((width * height) + padding))]
Protocol Examples
// MQTT Connect Packet with variable header and payload
// DNS Query with label compression
// Game Protocol: Player state update with bit-packed data
// HTTP/2 Frame with dynamic payload
// WebSocket Frame with masking
Size expressions work with both Vec<u8> and String fields, enabling dynamic sizing for binary protocols while maintaining compile-time validation of expression syntax.
Vector Support
Vectors require special handling since their size is dynamic. BeBytes provides several ways to handle vectors:
1. Last Field
A vector can be used as the last field in a struct without additional attributes:
2. With Size Hint
Use #[With(size(n))] to specify the exact number of bytes:
3. From Field
Use #[FromField(field_name)] to read the size from another field:
3.1 Nested Field Access
You can also reference fields in nested structures using dot notation:
// Even deeply nested fields are supported:
4. Vectors of Custom Types
BeBytes supports vectors containing custom types that implement the BeBytes trait:
For vectors of custom types, the following rules apply:
- When used as the last field, it will consume all remaining bytes, parsing them as instances of the custom type
- When used elsewhere, you must specify size information with
#[FromField]or#[With] - Each item in the vector is serialized/deserialized using its own BeBytes implementation
Marker Attributes
BeBytes supports delimiter-based field parsing for protocols that use sentinel bytes:
UntilMarker Attribute
Reads bytes until a specific marker is encountered:
// Null-terminated strings
AfterMarker Attribute
Skips bytes until finding a marker, then reads remaining data:
Supported Markers
- Character literals: ASCII characters only (
'\n','\0','\t','\r', etc.) - Byte values: Any u8 value (0x00 through 0xFF)
Behavior
UntilMarker: Marker byte is consumed but not included in the fieldAfterMarker: Skips to marker, marker consumed, remaining bytes become field value- Missing markers: UntilMarker reads all remaining bytes, AfterMarker results in empty field
Buffer Management
BeBytes provides efficient internal buffer management for optimized operations:
use ;
let packet = NetworkPacket ;
// Traditional Vec<u8> approach (still available)
let vec_bytes = packet.to_be_bytes;
// Buffer operations
let bytes_buffer: Bytes = packet.to_be_bytes_buf;
// Direct buffer writing
let mut buf = with_capacity;
packet.encode_be_to.unwrap;
let final_bytes = buf.freeze; // Convert to immutable buffer
// All methods produce identical results
assert_eq!;
assert_eq!;
Buffer Methods Benefits
- Efficient operations: Direct buffer writing without intermediate allocations
- Memory efficiency: Pre-allocated buffers reduce allocations
- Clean API: Consistent buffer-oriented interface
- Compatibility: Works with existing code unchanged
Migration Guide
Existing code continues to work unchanged. To leverage bytes benefits:
// Before (still works)
let data = packet.to_be_bytes;
send_data.await;
// After (optimized buffer operations)
let data = packet.to_be_bytes_buf;
send_data.await; // Same signature, optimized performance
Performance Optimizations
Direct Buffer Writing
use ;
// Traditional approach (allocates)
let bytes = packet.to_be_bytes;
buffer.put_slice;
// Direct writing (no allocation)
packet.encode_be_to?;
The encode_be_to and encode_le_to methods write directly to any BufMut implementation, eliminating the allocation overhead of to_be_bytes(). This is particularly beneficial for high-performance networking code.
Performance Features
- Inline annotations: All generated methods use
#[inline]for better optimization - Pre-allocated capacity: The
to_bytesmethods pre-allocate exact capacity - Direct buffer writing: Efficient buffer operations
- Zero-copy parsing: Deserialization works directly from byte slices
Raw Pointer Methods (New in 2.5.0)
BeBytes provides raw pointer-based encoding methods for eligible structs:
use BeBytes;
let packet = Packet ;
// Check if struct supports raw pointer encoding
if supports_raw_pointer_encoding
Raw pointer methods provide:
- Zero allocations with stack-based methods
- Direct memory writes using compile-time known offsets
- Pointer arithmetic and memcpy operations
Raw pointer methods are available for structs that:
- Have no bit fields
- Are 256 bytes or smaller
- Contain only primitive types and fixed-size arrays
Safety guarantees:
- Stack methods are safe with compile-time array sizing
- Compiler enforces correctness at build time
- Direct buffer methods include capacity validation
- Methods only generated for eligible structs
No-STD Support
BeBytes supports no_std environments:
[]
= { = "2.9.0", = false }
By default, the std feature is enabled. Disable it for no_std support with alloc.
Example: DNS Name Parsing
This example shows how BeBytes can be used to parse a DNS name with dynamic length segments, demonstrating both #[FromField] attribute and vectors of custom types:
// Usage example
Performance Optimizations
BeBytes includes efficient buffer management, providing:
Zero-Copy Operations
use BeBytes;
// Create zero-copy shareable buffer
let msg = Message ;
let bytes_buf = msg.to_be_bytes_buf; // Returns Bytes
// Clone is cheap - just increments reference count
let clone1 = bytes_buf.clone;
let clone2 = bytes_buf.clone;
// Pass to multiple tasks without copying data
spawn;
Direct Buffer Writing
use ;
// Write directly to existing buffer
let mut buf = with_capacity;
// Encode multiple messages without intermediate allocations
msg1.encode_be_to?;
msg2.encode_be_to?;
msg3.encode_be_to?;
// Convert to immutable Bytes for sending
let bytes = buf.freeze;
The buffer management provides significant performance improvements in production workloads.
Contribute
I'm doing this for fun, but all help is appreciated.
License
This project is licensed under the MIT License