kevy_bytes/lib.rs
1//! `SmallBytes` — a 24-byte small-byte-string with inline-SSO optimization.
2//!
3//! ```
4//! use kevy_bytes::SmallBytes;
5//!
6//! // Up to 22 bytes live in the value itself — no allocation, and the
7//! // whole string fits in one 24-byte slot.
8//! let short = SmallBytes::from_slice(b"user:1");
9//! assert_eq!(short.as_slice(), b"user:1");
10//! assert_eq!(short.heap_bytes(), 0);
11//!
12//! // Past the inline capacity it spills to the heap, and says so.
13//! let long = SmallBytes::from_slice(&[b'x'; 64]);
14//! assert_eq!(long.len(), 64);
15//! assert!(long.heap_bytes() >= 64);
16//! ```
17//!
18//! Layout (**little-endian only**): a union of two 24-byte variants, distinguished
19//! by the byte at offset 23:
20//!
21//! - **Inline**: `[u8; 23]` data, then `u8` tag holding the inline length
22//! (0..=22). The whole string lives in the value, no allocation.
23//! - **Heap (64-bit)**: `NonNull<u8>` ptr (8) + `usize` len (8) + `usize`
24//! cap_and_tag (8). The high byte of `cap_and_tag` overlaps byte 23 of
25//! the union and is fixed at `0xFF` (> 22) as the heap discriminator. The
26//! low 56 bits hold the heap capacity (up to 72 PB).
27//! - **Heap (32-bit)**: `NonNull<u8>` ptr (4) + `u32` len (4) + `u32`
28//! cap (4) + 11-byte pad, then `u8` tag fixed at `0xFF`. Same 24-byte
29//! total, same discriminator byte at offset 23 — pointer / len fields
30//! are 32-bit-native so a `wasm32-unknown-unknown` build picks up the
31//! right size without shifting a `usize` past its bit width.
32//!
33//! The 64-bit layout is the one the kevy server runs on, and is locked
34//! against perf-affecting changes (cfg-gated 32-bit alternative lives
35//! alongside it without touching any 64-bit code path).
36//!
37//! This lets us store every byte string up to 22 bytes — covering the vast
38//! majority of Redis-style values — without any pointer-chase, while keeping
39//! `size_of::<SmallBytes>() == 24` (same as `Vec<u8>`). Used by `kevy-store`
40//! to make `Value::Str(SmallBytes)` fit alongside the boxed collection
41//! variants and keep `Entry` at 48 B.
42
43#![warn(missing_docs)]
44#![cfg_attr(not(feature = "std"), no_std)]
45
46extern crate alloc;
47
48#[cfg(target_endian = "big")]
49compile_error!("kevy-bytes requires little-endian: heap-tag byte overlaps inline length byte");
50
51mod find_crlf;
52mod eq;
53mod traits;
54
55mod heap;
56pub(crate) use heap::{Heap, INLINE_CAP, INLINE_LEN_MAX, Inline};
57
58pub use find_crlf::find_crlf;
59
60use alloc::alloc::{Layout, alloc, dealloc, handle_alloc_error};
61use alloc::vec::Vec;
62use core::mem::{self, ManuallyDrop};
63use core::ptr::NonNull;
64use core::slice;
65
66/// A 24-byte owned byte string with inline small-string optimization.
67///
68/// Strings of up to 22 bytes live entirely inside the value (no allocation,
69/// no pointer chase); larger strings spill to a heap buffer. The
70/// discriminator is a single byte at offset 23 (the tag, which doubles as
71/// the inline length 0..=22 OR equals 0xFF when the heap variant is active).
72///
73/// See the crate root for layout details.
74#[repr(C)]
75/// # Examples
76///
77/// Short values live inline; longer ones move to the heap. The API does not
78/// change, but `heap_bytes` reports which happened, which is what the
79/// keyspace's memory accounting reads.
80///
81/// ```
82/// use kevy_bytes::SmallBytes;
83/// let short = SmallBytes::from_slice(b"hello");
84/// assert_eq!(short.as_slice(), b"hello");
85/// assert_eq!(short.len(), 5);
86/// assert_eq!(short.heap_bytes(), 0, "a short value allocates nothing");
87///
88/// let long = SmallBytes::from_slice(&[b'x'; 100]);
89/// assert_eq!(long.len(), 100);
90/// assert!(long.heap_bytes() >= 100, "a long value is on the heap");
91/// ```
92///
93/// ```
94/// use kevy_bytes::SmallBytes;
95/// assert!(SmallBytes::from_slice(b"").is_empty());
96/// ```
97pub union SmallBytes {
98 // pub(crate) so `eq.rs` can branch on the variant directly; the union
99 // itself stays private to this crate's own modules.
100 pub(crate) inline: Inline,
101 pub(crate) heap: Heap,
102}
103
104const _: () = {
105 assert!(mem::size_of::<SmallBytes>() == 24);
106 assert!(mem::align_of::<SmallBytes>() == mem::align_of::<usize>());
107};
108
109unsafe impl Send for SmallBytes {}
110unsafe impl Sync for SmallBytes {}
111
112impl SmallBytes {
113 /// Empty inline `SmallBytes` (zero allocation).
114 ///
115 /// # Examples
116 ///
117 /// `const`, so it can seed a static or an array without a run-time
118 /// initialiser:
119 ///
120 /// ```
121 /// use kevy_bytes::SmallBytes;
122 /// static EMPTY: SmallBytes = SmallBytes::new();
123 /// assert!(EMPTY.is_empty());
124 /// assert_eq!(EMPTY.heap_bytes(), 0);
125 /// ```
126 pub const fn new() -> Self {
127 Self {
128 inline: Inline {
129 data: [0; INLINE_CAP],
130 tag: 0,
131 },
132 }
133 }
134
135 /// Construct from a byte slice — inline if `bytes.len() <= 22`, else heap.
136 ///
137 /// # Examples
138 ///
139 /// Twenty-two is the boundary, and it is exact:
140 ///
141 /// ```
142 /// use kevy_bytes::SmallBytes;
143 /// assert_eq!(SmallBytes::from_slice(&[b'x'; 22]).heap_bytes(), 0);
144 /// assert_eq!(SmallBytes::from_slice(&[b'x'; 23]).heap_bytes(), 23);
145 /// ```
146 pub fn from_slice(bytes: &[u8]) -> Self {
147 if bytes.len() <= INLINE_LEN_MAX as usize {
148 let mut data = [0u8; INLINE_CAP];
149 // SAFETY: bytes.len() ≤ 22 ≤ data.len(); non-overlapping regions.
150 unsafe {
151 core::ptr::copy_nonoverlapping(bytes.as_ptr(), data.as_mut_ptr(), bytes.len());
152 }
153 Self {
154 inline: Inline {
155 data,
156 tag: bytes.len() as u8,
157 },
158 }
159 } else {
160 Self::alloc_heap(bytes)
161 }
162 }
163
164 /// Take ownership of a `Vec<u8>` — inline if `vec.len() <= 22`, else **reuse
165 /// the vec's allocation** (no copy on the heap path).
166 ///
167 /// # Examples
168 ///
169 /// The heap path keeps the vec's own buffer, so a value that arrived as
170 /// a `Vec` is stored without a second copy:
171 ///
172 /// ```
173 /// use kevy_bytes::SmallBytes;
174 /// let v = vec![b'z'; 64];
175 /// let addr = v.as_ptr();
176 /// let b = SmallBytes::from_vec(v);
177 /// assert_eq!(b.as_slice().as_ptr(), addr, "same allocation, not a copy");
178 /// ```
179 ///
180 /// A short vec goes inline instead, and its allocation is released:
181 ///
182 /// ```
183 /// use kevy_bytes::SmallBytes;
184 /// assert_eq!(SmallBytes::from_vec(vec![b'a'; 4]).heap_bytes(), 0);
185 /// ```
186 pub fn from_vec(vec: Vec<u8>) -> Self {
187 if vec.len() <= INLINE_LEN_MAX as usize {
188 Self::from_slice(&vec)
189 } else {
190 let mut v = ManuallyDrop::new(vec);
191 // SAFETY: len > 22 ⇒ cap > 0 ⇒ Vec has an allocation, so the pointer
192 // is non-null. Vec guarantees a non-null pointer for any allocated
193 // Vec (and a dangling-but-non-null for empty, which we don't hit here).
194 let ptr = unsafe { NonNull::new_unchecked(v.as_mut_ptr()) };
195 let len = v.len();
196 let cap = v.capacity();
197 Self {
198 heap: Heap::new(ptr, len, cap),
199 }
200 }
201 }
202
203 #[inline]
204 fn alloc_heap(bytes: &[u8]) -> Self {
205 let len = bytes.len();
206 // `len > 22` (caller has already taken the heap branch) and `len` is
207 // a slice length ⇒ ≤ `isize::MAX` ⇒ well below the `usize::MAX -
208 // (align - 1)` bound `from_size_align_unchecked` needs. u8's align is 1.
209 // SAFETY: see above.
210 let layout = unsafe { Layout::from_size_align_unchecked(len, 1) };
211 // SAFETY: layout.size() > 0 (caller's heap branch guarantees len > 22).
212 let raw = unsafe { alloc(layout) };
213 let Some(ptr) = NonNull::new(raw) else {
214 handle_alloc_error(layout)
215 };
216 // SAFETY: alloc returned a writable region of `len` bytes; source is a
217 // disjoint slice.
218 unsafe {
219 core::ptr::copy_nonoverlapping(bytes.as_ptr(), ptr.as_ptr(), len);
220 }
221 Self {
222 heap: Heap::new(ptr, len, len),
223 }
224 }
225
226 /// True when stored inline; the byte at index 23 is the deciding tag in
227 /// either rep, so the check is a single load + compare.
228 #[inline]
229 fn is_inline(&self) -> bool {
230 // SAFETY: byte 23 is always initialised — either as Inline::tag (0..=22)
231 // or as the high byte of Heap::cap_and_tag (= 0xFF). Reading it through
232 // the Inline view is valid in either case (the union is `repr(C)`).
233 unsafe { self.inline.tag <= INLINE_LEN_MAX }
234 }
235
236 /// Number of bytes stored.
237 ///
238 /// # Examples
239 ///
240 /// The same answer either side of the inline boundary — which is the
241 /// point of the type: where the bytes live is not the caller's problem.
242 ///
243 /// ```
244 /// use kevy_bytes::SmallBytes;
245 /// assert_eq!(SmallBytes::from_slice(&[0u8; 22]).len(), 22);
246 /// assert_eq!(SmallBytes::from_slice(&[0u8; 23]).len(), 23);
247 /// ```
248 #[inline]
249 pub fn len(&self) -> usize {
250 if self.is_inline() {
251 // SAFETY: just verified `inline.tag` ≤ 22.
252 unsafe { self.inline.tag as usize }
253 } else {
254 // SAFETY: tag > 22 ⇒ heap variant is active.
255 unsafe { self.heap.length() }
256 }
257 }
258
259 /// Whether `len() == 0`.
260 ///
261 /// # Examples
262 ///
263 /// ```
264 /// use kevy_bytes::SmallBytes;
265 /// assert!(SmallBytes::from_slice(b"").is_empty());
266 /// assert!(!SmallBytes::from_slice(b"\0").is_empty(), "a NUL byte is a byte");
267 /// ```
268 #[inline]
269 pub fn is_empty(&self) -> bool {
270 self.len() == 0
271 }
272
273 /// Bytes this value holds on the heap (0 when inline). Lets memory-accounting
274 /// callers (e.g. `maxmemory` enforcement) charge only the off-stack footprint
275 /// without re-deriving the inline-length threshold.
276 ///
277 /// # Examples
278 ///
279 /// This is what `maxmemory` charges, so an inline value must cost zero
280 /// — it is already inside the entry the keyspace has counted:
281 ///
282 /// ```
283 /// use kevy_bytes::SmallBytes;
284 /// assert_eq!(SmallBytes::from_slice(b"user:1").heap_bytes(), 0);
285 /// assert_eq!(SmallBytes::from_slice(&[b'x'; 1000]).heap_bytes(), 1000);
286 /// ```
287 #[inline]
288 pub fn heap_bytes(&self) -> usize {
289 if self.is_inline() { 0 } else { self.len() }
290 }
291
292 /// Borrow the bytes (no allocation; same for inline and heap variants).
293 ///
294 /// # Examples
295 ///
296 /// ```
297 /// use kevy_bytes::SmallBytes;
298 /// let b = SmallBytes::from_slice(b"GET");
299 /// assert_eq!(b.as_slice(), b"GET");
300 /// assert_eq!(SmallBytes::new().as_slice(), b"");
301 /// ```
302 #[inline]
303 pub fn as_slice(&self) -> &[u8] {
304 if self.is_inline() {
305 // SAFETY: first `tag` bytes of `data` are valid (zero-init at construction).
306 unsafe {
307 slice::from_raw_parts(self.inline.data.as_ptr(), self.inline.tag as usize)
308 }
309 } else {
310 // SAFETY: heap variant active; ptr/len originate from a Vec or our own alloc.
311 unsafe { slice::from_raw_parts(self.heap.ptr.as_ptr(), self.heap.length()) }
312 }
313 }
314
315 /// Copy into a fresh `Vec<u8>` (clone semantics).
316 ///
317 /// # Examples
318 ///
319 /// ```
320 /// use kevy_bytes::SmallBytes;
321 /// let b = SmallBytes::from_slice(b"copy me");
322 /// assert_eq!(b.to_vec(), b"copy me");
323 /// assert_eq!(b.as_slice(), b"copy me", "the original still holds them");
324 /// ```
325 pub fn to_vec(&self) -> Vec<u8> {
326 self.as_slice().to_vec()
327 }
328
329 /// Consume self and return an owned `Vec<u8>`. The heap path reuses the
330 /// existing allocation; the inline path copies into a new vec.
331 ///
332 /// # Examples
333 ///
334 /// A heap value hands its buffer straight back, so a round trip through
335 /// `SmallBytes` costs no allocation at either end:
336 ///
337 /// ```
338 /// use kevy_bytes::SmallBytes;
339 /// let v = vec![b'q'; 128];
340 /// let addr = v.as_ptr();
341 /// assert_eq!(SmallBytes::from_vec(v).into_vec().as_ptr(), addr);
342 /// ```
343 ///
344 /// ```
345 /// use kevy_bytes::SmallBytes;
346 /// assert_eq!(SmallBytes::from_slice(b"short").into_vec(), b"short");
347 /// ```
348 pub fn into_vec(self) -> Vec<u8> {
349 if self.is_inline() {
350 self.as_slice().to_vec()
351 // self drops as inline — nothing to free.
352 } else {
353 // SAFETY: heap variant active.
354 let (ptr, len, cap) = unsafe {
355 (
356 self.heap.ptr.as_ptr(),
357 self.heap.length(),
358 self.heap.capacity(),
359 )
360 };
361 // Skip our Drop to avoid double-free; Vec::from_raw_parts now owns it.
362 let _do_not_drop = ManuallyDrop::new(self);
363 // SAFETY: ptr/len/cap originated from either a Vec<u8> (from_vec)
364 // or our own `alloc(Layout::array::<u8>(cap))` (alloc_heap, where
365 // cap == len) — both meet Vec::from_raw_parts' requirements.
366 unsafe { Vec::from_raw_parts(ptr, len, cap) }
367 }
368 }
369}
370
371impl Default for SmallBytes {
372 fn default() -> Self {
373 Self::new()
374 }
375}
376
377impl Drop for SmallBytes {
378 fn drop(&mut self) {
379 if self.is_inline() {
380 return;
381 }
382 // SAFETY: heap variant active; layout matches the one used at alloc
383 // time (either from Vec — Vec uses `Layout::array::<u8>(cap)` — or our
384 // own alloc_heap which used the same layout).
385 unsafe {
386 let cap = self.heap.capacity();
387 let layout = Layout::array::<u8>(cap).expect("kevy-bytes: drop layout");
388 dealloc(self.heap.ptr.as_ptr(), layout);
389 }
390 }
391}
392
393impl Clone for SmallBytes {
394 /// Specialised clone that bypasses `as_slice → from_slice → alloc_heap`'s
395 /// two layered length checks. Inline variant is a bitwise union copy (no
396 /// branch through the slice path); heap variant goes straight to a single
397 /// `alloc + memcpy` keyed on the already-known heap length.
398 #[inline]
399 fn clone(&self) -> Self {
400 if self.is_inline() {
401 // SAFETY: `Inline` is `repr(C)` + `Copy`; bitwise copy is sound
402 // when the source is currently in the inline variant (the tag
403 // byte ≤ 22 is part of the bit pattern we're copying, so the
404 // discriminator stays correct).
405 unsafe { Self { inline: self.inline } }
406 } else {
407 // SAFETY: tag > 22 ⇒ heap variant is active.
408 unsafe { self.clone_heap() }
409 }
410 }
411}
412
413impl SmallBytes {
414 /// Heap-fast-path clone. Caller must have established that `self` is in
415 /// the heap variant.
416 ///
417 /// # Safety
418 /// `self.heap` must be the active union variant (i.e. `is_inline()` is
419 /// false). `self.heap.ptr` must point to `self.heap.len` valid bytes.
420 #[inline]
421 unsafe fn clone_heap(&self) -> Self {
422 // SAFETY (covers the three `self.heap.*` reads): caller asserts the
423 // heap variant is active.
424 let (src_ptr, len) = unsafe { (self.heap.ptr.as_ptr(), self.heap.length()) };
425 // `len > 22 ⇒ len > 0`, and the high bits are guarded by `CAP_MASK`
426 // never letting cap exceed 2^56, well below `isize::MAX`, so the
427 // unchecked layout is sound. Allocator alignment for `u8` is 1.
428 let layout = unsafe { Layout::from_size_align_unchecked(len, 1) };
429 // SAFETY: layout.size() > 0.
430 let raw = unsafe { alloc(layout) };
431 let Some(ptr) = NonNull::new(raw) else {
432 handle_alloc_error(layout)
433 };
434 // SAFETY: src has `len` valid bytes; dst is freshly-allocated for `len`
435 // bytes; regions are disjoint.
436 unsafe { core::ptr::copy_nonoverlapping(src_ptr, ptr.as_ptr(), len) };
437 Self {
438 heap: Heap::new(ptr, len, len),
439 }
440 }
441}
442
443
444
445#[cfg(test)]
446mod tests;