Skip to main content

multi_trait/
enc_into_array.rs

1// SPDX-License-Identifier: Apache-2.0
2//! Stack-based encoding trait for no-allocation varint encoding.
3//!
4//! This module provides the [`EncodeIntoArray`] trait, which allows encoding
5//! values into a stack-allocated array without any heap allocation. This is
6//! essential for `no_std` environments and embedded systems.
7//!
8//! # Performance Benefits
9//!
10//! Using `EncodeIntoArray` provides:
11//! - **Zero heap allocations**: All data on stack
12//! - **Deterministic performance**: No GC or allocator overhead
13//! - **Embedded-friendly**: Works in `no_std` contexts
14//! - **Predictable memory**: Compile-time known sizes
15//!
16//! # Use Cases
17//!
18//! - Embedded systems without allocator
19//! - Real-time systems requiring deterministic performance
20//! - `no_std` environments
21//! - Memory-constrained devices
22//! - Safety-critical systems
23//!
24//! # Examples
25//!
26//! ## Basic stack encoding
27//!
28//! ```rust
29//! use multi_trait::EncodeIntoArray;
30//!
31//! let (array, len) = 42u8.encode_into_array();
32//! assert_eq!(&array[..len], &[42]);
33//! ```
34//!
35//! ## Working with larger values
36//!
37//! ```rust
38//! use multi_trait::EncodeIntoArray;
39//!
40//! let (array, len) = 1000u16.encode_into_array();
41//! // Varint encoding of 1000 takes 2 bytes
42//! assert_eq!(len, 2);
43//! assert_eq!(&array[..len], &[0xE8, 0x07]);
44//! ```
45//!
46//! ## Maximum encoded sizes
47//!
48//! Each type has a compile-time known maximum encoded size:
49//!
50//! ```rust
51//! use multi_trait::EncodeIntoArray;
52//!
53//! // u8 values fit in 2 bytes max
54//! assert_eq!(<u8 as EncodeIntoArray>::MAX_ENCODED_SIZE, 2);
55//!
56//! // u16 values fit in 3 bytes max
57//! assert_eq!(<u16 as EncodeIntoArray>::MAX_ENCODED_SIZE, 3);
58//!
59//! // u32 values fit in 5 bytes max
60//! assert_eq!(<u32 as EncodeIntoArray>::MAX_ENCODED_SIZE, 5);
61//! ```
62
63use unsigned_varint::encode;
64
65/// Maximum size needed for any varint encoding (u128 max = 19 bytes)
66pub const MAX_VARINT_SIZE: usize = 19;
67
68/// Trait for encoding values into a stack-allocated array.
69///
70/// This trait provides zero-allocation encoding by using stack-allocated arrays.
71/// All types use a fixed-size array of 19 bytes (enough for any varint), with
72/// each type documenting its actual maximum size via `MAX_ENCODED_SIZE`.
73///
74/// # Performance
75///
76/// This trait is optimized for embedded and `no_std` contexts:
77/// - No heap allocations
78/// - Compile-time sized buffers
79/// - Inline hints for hot paths
80/// - Deterministic performance
81///
82/// # Thread Safety
83///
84/// This trait is `Send + Sync` safe. All implementations are stateless and can
85/// be called concurrently from multiple threads.
86///
87/// # Maximum Encoded Sizes
88///
89/// The maximum varint-encoded size for each type is:
90/// - `bool`, `u8`: 2 bytes
91/// - `u16`: 3 bytes
92/// - `u32`: 5 bytes
93/// - `u64`: 10 bytes
94/// - `u128`: 19 bytes
95/// - `usize`: 10 bytes (64-bit) or 5 bytes (32-bit)
96///
97/// # Examples
98///
99/// ```rust
100/// use multi_trait::EncodeIntoArray;
101///
102/// // Encode a value to a stack array
103/// let (array, len) = 42u8.encode_into_array();
104/// assert_eq!(&array[..len], &[42]);
105///
106/// // Maximum size is known at compile time
107/// assert_eq!(<u32 as EncodeIntoArray>::MAX_ENCODED_SIZE, 5);
108/// ```
109///
110/// # Implemented For
111///
112/// - `bool`: Encoded as 0 (false) or 1 (true)
113/// - `u8`, `u16`, `u32`, `u64`, `u128`: Variable-length encoding
114/// - `usize`: Platform-dependent (32-bit or 64-bit)
115pub trait EncodeIntoArray {
116    /// The maximum number of bytes needed to encode any value of this type.
117    ///
118    /// This is a compile-time constant that documents the maximum size
119    /// for this specific type, though all types return a 19-byte array.
120    const MAX_ENCODED_SIZE: usize;
121
122    /// Encode this value into a stack-allocated array.
123    ///
124    /// Returns a tuple of:
125    /// - A 19-byte array containing the encoded bytes (may have unused space)
126    /// - The actual length of the encoded data (≤ `MAX_ENCODED_SIZE`)
127    ///
128    /// # Performance
129    ///
130    /// This method performs no heap allocations. All data is on the stack.
131    ///
132    /// # Examples
133    ///
134    /// ```rust
135    /// use multi_trait::EncodeIntoArray;
136    ///
137    /// let (array, len) = 42u8.encode_into_array();
138    /// assert_eq!(len, 1);
139    /// assert_eq!(&array[..len], &[42]);
140    /// ```
141    fn encode_into_array(&self) -> ([u8; MAX_VARINT_SIZE], usize);
142}
143
144/// Macro to implement `EncodeIntoArray` for unsigned integer types using varint encoding.
145///
146/// This macro eliminates code duplication by generating identical implementations
147/// for different numeric types. Each implementation:
148/// 1. Defines the maximum encoded size for the type
149/// 2. Creates an appropriately sized buffer on the stack
150/// 3. Encodes the value into the buffer
151/// 4. Returns the buffer and actual encoded length
152///
153/// # Usage
154///
155/// ```text
156/// impl_encode_into_array! {
157///     u8, 2 => u8_buffer, u8;
158///     u16, 3 => u16_buffer, u16;
159/// }
160/// ```
161///
162/// # Hygiene
163///
164/// This macro uses fully qualified paths to ensure proper hygiene and avoid
165/// namespace collisions with user code.
166macro_rules! impl_encode_into_array {
167    ($($type:ty, $max_size:expr => $buffer_fn:ident, $encode_fn:ident);+ $(;)?) => {
168        $(
169            #[doc = concat!("Encode a ", stringify!($type), " into a stack-allocated array")]
170            impl EncodeIntoArray for $type {
171                const MAX_ENCODED_SIZE: usize = $max_size;
172
173                #[inline]
174                fn encode_into_array(&self) -> ([u8; MAX_VARINT_SIZE], usize) {
175                    // Create buffer on stack
176                    let mut buf = encode::$buffer_fn();
177
178                    // Encode value into buffer
179                    encode::$encode_fn(*self, &mut buf);
180
181                    // Find the length efficiently by locating the last byte marker
182                    let len = buf
183                        .iter()
184                        .position(|&b| unsigned_varint::decode::is_last(b))
185                        .map_or(buf.len(), |pos| pos + 1);
186
187                    // Copy to fixed-size array
188                    let mut result = [0u8; MAX_VARINT_SIZE];
189                    result[..len].copy_from_slice(&buf[..len]);
190
191                    (result, len)
192                }
193            }
194        )+
195    };
196}
197
198/// Encode a bool into a stack-allocated array
199impl EncodeIntoArray for bool {
200    const MAX_ENCODED_SIZE: usize = 1;
201
202    #[inline]
203    fn encode_into_array(&self) -> ([u8; MAX_VARINT_SIZE], usize) {
204        let mut result = [0u8; MAX_VARINT_SIZE];
205        result[0] = u8::from(*self);
206        (result, 1)
207    }
208}
209
210// Implement EncodeIntoArray for all unsigned integer types using the macro
211//
212// Maximum sizes are calculated as ceil(bits / 7) since varint encoding
213// uses 7 bits per byte for data:
214// - u8 (8 bits): ceil(8/7) = 2 bytes max
215// - u16 (16 bits): ceil(16/7) = 3 bytes max
216// - u32 (32 bits): ceil(32/7) = 5 bytes max
217// - u64 (64 bits): ceil(64/7) = 10 bytes max
218// - u128 (128 bits): ceil(128/7) = 19 bytes max
219impl_encode_into_array! {
220    u8, 2 => u8_buffer, u8;
221    u16, 3 => u16_buffer, u16;
222    u32, 5 => u32_buffer, u32;
223    u64, 10 => u64_buffer, u64;
224    u128, 19 => u128_buffer, u128;
225}
226
227// usize is platform-dependent: use 10 bytes (64-bit) to be safe
228impl EncodeIntoArray for usize {
229    const MAX_ENCODED_SIZE: Self = 10;
230
231    #[inline]
232    fn encode_into_array(&self) -> ([u8; MAX_VARINT_SIZE], usize) {
233        // Create buffer on stack
234        let mut buf = encode::usize_buffer();
235
236        // Encode value into buffer
237        encode::usize(*self, &mut buf);
238
239        // Find the length efficiently by locating the last byte marker
240        let len = buf
241            .iter()
242            .position(|&b| unsigned_varint::decode::is_last(b))
243            .map_or(buf.len(), |pos| pos + 1);
244
245        // Copy to fixed-size array
246        let mut result = [0u8; MAX_VARINT_SIZE];
247        result[..len].copy_from_slice(&buf[..len]);
248
249        (result, len)
250    }
251}
252
253#[cfg(test)]
254mod tests {
255    use super::*;
256
257    #[test]
258    fn test_array_single_byte() {
259        let (array, len) = 42u8.encode_into_array();
260        assert_eq!(len, 1);
261        assert_eq!(&array[..len], &[42]);
262    }
263
264    #[test]
265    fn test_array_multi_byte() {
266        // 128 requires 2 bytes in varint encoding
267        let (array, len) = 128u16.encode_into_array();
268        assert_eq!(len, 2);
269        assert_eq!(&array[..len], &[0x80, 0x01]);
270    }
271
272    #[test]
273    fn test_array_bool() {
274        let (array_true, len_true) = true.encode_into_array();
275        assert_eq!(len_true, 1);
276        assert_eq!(&array_true[..len_true], &[1]);
277
278        let (array_false, len_false) = false.encode_into_array();
279        assert_eq!(len_false, 1);
280        assert_eq!(&array_false[..len_false], &[0]);
281    }
282
283    #[test]
284    fn test_array_zero_values() {
285        let (array, len) = 0u8.encode_into_array();
286        assert_eq!(len, 1);
287        assert_eq!(&array[..len], &[0]);
288
289        let (array, len) = 0u32.encode_into_array();
290        assert_eq!(len, 1);
291        assert_eq!(&array[..len], &[0]);
292    }
293
294    #[test]
295    fn test_array_max_values() {
296        let (_array, len) = u8::MAX.encode_into_array();
297        assert!(len <= <u8 as EncodeIntoArray>::MAX_ENCODED_SIZE);
298
299        let (_array, len) = u16::MAX.encode_into_array();
300        assert!(len <= <u16 as EncodeIntoArray>::MAX_ENCODED_SIZE);
301
302        let (_array, len) = u32::MAX.encode_into_array();
303        assert!(len <= <u32 as EncodeIntoArray>::MAX_ENCODED_SIZE);
304    }
305
306    #[test]
307    fn test_array_max_sizes() {
308        // Verify maximum sizes are correct
309        assert_eq!(<u8 as EncodeIntoArray>::MAX_ENCODED_SIZE, 2);
310        assert_eq!(<u16 as EncodeIntoArray>::MAX_ENCODED_SIZE, 3);
311        assert_eq!(<u32 as EncodeIntoArray>::MAX_ENCODED_SIZE, 5);
312        assert_eq!(<u64 as EncodeIntoArray>::MAX_ENCODED_SIZE, 10);
313        assert_eq!(<u128 as EncodeIntoArray>::MAX_ENCODED_SIZE, 19);
314        assert_eq!(<usize as EncodeIntoArray>::MAX_ENCODED_SIZE, 10);
315        assert_eq!(<bool as EncodeIntoArray>::MAX_ENCODED_SIZE, 1);
316    }
317
318    #[test]
319    fn test_array_sequential_encoding() {
320        // Demonstrate that multiple values can be encoded independently
321        let (array1, len1) = 42u8.encode_into_array();
322        let (_array2, len2) = 1000u16.encode_into_array();
323        let (_array3, len3) = 100_000_u32.encode_into_array();
324
325        // Each encoding is independent
326        assert_eq!(&array1[..len1], &[42]);
327        assert_eq!(len2, 2);
328        assert!(len3 > 1);
329    }
330
331    #[test]
332    fn test_array_no_heap_allocation() {
333        // This test verifies that the array-based encoding doesn't use heap
334        // If it did use heap, this would fail in a no_std environment
335        let (_array, len) = u64::MAX.encode_into_array();
336        assert!(len <= <u64 as EncodeIntoArray>::MAX_ENCODED_SIZE);
337        assert_eq!(len, 10); // u64::MAX takes 10 bytes in varint
338    }
339
340    #[test]
341    fn test_array_deterministic() {
342        // Encoding the same value should always produce the same result
343        let (array1, len1) = 12345u32.encode_into_array();
344        let (array2, len2) = 12345u32.encode_into_array();
345
346        assert_eq!(len1, len2);
347        assert_eq!(&array1[..len1], &array2[..len2]);
348    }
349
350    #[test]
351    fn test_array_unused_space_is_zeroed() {
352        // Verify that unused space in the array is zeroed
353        let (array, len) = 42u8.encode_into_array();
354        assert_eq!(len, 1);
355        // Check that bytes beyond len are zero
356        for &byte in &array[len..MAX_VARINT_SIZE] {
357            assert_eq!(byte, 0);
358        }
359    }
360}