ida_rs/lib.rs
1#![cfg_attr(not(test), no_std)]
2
3//!
4//! # An ID Allocator for Sparse ID Spaces
5//!
6//! `ida-rs` provides a thread-safe, `no_std` compatible ID allocator suitable for
7//! systems-level programming, such as in OS kernels or embedded environments.
8//!
9//! It is implemented as a radix tree, which makes it highly memory-efficient
10//! when dealing with sparse ID allocations (e.g., allocating ID 5 and ID 5,000,000
11//! without allocating the space in between).
12//!
13//! ## Features
14//! - **`no_std` compatible:** Usable in bare-metal environments.
15//! - **Thread-Safe:** All public methods are thread-safe, using a spinlock for synchronization.
16//! - **Memory-Efficient for Sparse Sets:** Ideal when allocated IDs are far apart.
17//!
18//! ## Example
19//! ```
20//! use ida_rs::Ida;
21//!
22//! let ida = Ida::new();
23//!
24//! // Allocate new IDs
25//! let id1 = ida.alloc().unwrap();
26//! let id2 = ida.alloc().unwrap();
27//!
28//! assert_eq!(id1, 0);
29//! assert_eq!(id2, 1);
30//!
31//! // Free an ID
32//! ida.free(id1);
33//!
34//! // The next allocation reuses the freed ID
35//! let id3 = ida.alloc().unwrap();
36//! assert_eq!(id3, 0);
37//! ```
38
39extern crate alloc;
40
41use alloc::{boxed::Box, collections::btree_map::BTreeMap};
42use core::fmt::Debug;
43use spin::Mutex;
44
45const IDA_SHIFT: usize = 6;
46const IDA_BITMAP_BITS: usize = 1 << IDA_SHIFT;
47// This calculation is the integer division equivalent of `ceil(64 / IDA_SHIFT)`
48// and ensures that we have enough levels to cover the entire 64-bit ID space.
49// We intentionally use this arithmetic to maintain compatibility with older Rust versions
50// that do not have the `div_ceil` function stabilized.
51#[allow(clippy::manual_div_ceil)]
52const IDA_MAX_LEVELS: usize = (64 + IDA_SHIFT - 1) / IDA_SHIFT;
53
54/// A thread-safe ID allocator for sparse ID spaces.
55///
56/// `Ida` (ID Allocator) manages a pool of unique integer IDs, implemented as a
57/// radix tree for memory efficiency. It's particularly well-suited for scenarios
58/// where allocated IDs may be far apart (e.g., allocating IDs 5 and 5,000,000).
59///
60/// # Thread Safety
61///
62/// All methods are thread-safe and can be safely shared across threads using `Arc`.
63/// The implementation uses a spinlock internally for synchronization.
64///
65/// # Memory Efficiency
66///
67/// The radix tree structure ensures that only allocated regions of the ID space
68/// consume memory. Sparse allocations do not waste space on unallocated ranges.
69///
70/// # Examples
71///
72/// Basic usage:
73/// ```
74/// use ida_rs::Ida;
75///
76/// let ida = Ida::new();
77/// let id1 = ida.alloc().unwrap();
78/// let id2 = ida.alloc().unwrap();
79/// ida.free(id1);
80/// let id3 = ida.alloc().unwrap(); // Reuses id1
81/// ```
82///
83/// Multi-threaded usage:
84/// ```
85/// use ida_rs::Ida;
86/// use std::sync::Arc;
87/// use std::thread;
88///
89/// let ida = Arc::new(Ida::new());
90/// let mut handles = vec![];
91///
92/// for _ in 0..4 {
93/// let ida_clone = Arc::clone(&ida);
94/// let handle = thread::spawn(move || {
95/// ida_clone.alloc()
96/// });
97/// handles.push(handle);
98/// }
99///
100/// for handle in handles {
101/// let id = handle.join().unwrap();
102/// println!("Allocated ID: {:?}", id);
103/// }
104/// ```
105#[derive(Debug)]
106pub struct Ida {
107 root: Mutex<IdaNode>,
108}
109
110#[derive(Debug)]
111struct IdaNode {
112 bitmap: u64,
113 children: BTreeMap<usize, Box<IdaNode>>,
114}
115
116impl IdaNode {
117 pub fn new() -> Self {
118 Self {
119 bitmap: 0,
120 children: BTreeMap::new(),
121 }
122 }
123
124 pub fn alloc(&mut self, level: usize) -> Option<usize> {
125 // CASE: We are at a leaf node
126 // The bitmap here represents individual IDs
127 if level == 0 {
128 // All ones means no free IDs
129 if self.bitmap == u64::MAX {
130 return None;
131 }
132 // Using trailing_ones to find the first zero bit,
133 // which is an unallocated ID
134 let bit = self.bitmap.trailing_ones() as usize;
135 self.bitmap |= 1 << bit;
136 return Some(bit);
137 }
138
139 // CASE: We are at an internal node
140 // The bitmap here represents child nodes. We iterate through the unset bits
141 // (0s), which correspond to children that are not full.
142 while self.bitmap != u64::MAX {
143 let i = self.bitmap.trailing_ones() as usize; // Find index of first 0 bit.
144
145 // The child node is either unallocated or not fully allocated, get it.
146 let child = self
147 .children
148 .entry(i)
149 .or_insert_with(|| Box::new(IdaNode::new()));
150
151 // Recursively allocate in the child node.
152 if let Some(id_in_child) = child.alloc(level - 1) {
153 // After the allocation, check if the child is now fully allocated.
154 // If so, set the corresponding bit in this node's bitmap.
155 if child.bitmap == u64::MAX {
156 self.bitmap |= 1 << i;
157 }
158 // Compute the full ID by combining the index and the child's ID.
159 let id = (i << (level * IDA_SHIFT)) | id_in_child;
160 return Some(id);
161 } else {
162 // The child was marked as having space in our bitmap, but the recursive
163 // alloc returned None, implying it's actually full. We fix this
164 // inconsistency here and continue the search in the next available child.
165 self.bitmap |= 1 << i;
166 }
167 }
168
169 None
170 }
171
172 pub fn free(&mut self, id: usize, level: usize) {
173 // Determine which bit index to clear at this level
174 let bit_index = (id >> (level * IDA_SHIFT)) & (IDA_BITMAP_BITS - 1);
175
176 // CASE: We are at a leaf node
177 if level == 0 {
178 // Simply clear the bit corresponding to the ID
179 self.bitmap &= !(1 << bit_index);
180 return;
181 }
182
183 // CASE: We are at an internal node
184 // Clear the bit in this node's bitmap,
185 // mark the child as not full or non-existent
186 self.bitmap &= !(1 << bit_index);
187 // Recurse into the appropriate child node
188 // if it exists, clearing the ID there
189 if let Some(child) = self.children.get_mut(&bit_index) {
190 // Recurse into the child node
191 child.free(id, level - 1);
192 // If the child is now empty, remove it to save space
193 if child.bitmap == 0 && child.children.is_empty() {
194 self.children.remove(&bit_index);
195 }
196 }
197 }
198
199 pub fn is_allocated(&self, id: usize, level: usize) -> bool {
200 let bit_index = (id >> (level * IDA_SHIFT)) & (IDA_BITMAP_BITS - 1);
201
202 if level == 0 {
203 return (self.bitmap >> bit_index) & 1 == 1;
204 }
205
206 if let Some(child) = self.children.get(&bit_index) {
207 child.is_allocated(id, level - 1)
208 } else {
209 // If the child node doesn't exist, no IDs in that range can be allocated.
210 false
211 }
212 }
213}
214
215impl Ida {
216 /// Creates a new, empty ID allocator.
217 ///
218 /// The allocator starts with no IDs allocated. The first call to [`alloc`](Self::alloc)
219 /// will return ID `0`.
220 ///
221 /// # Examples
222 ///
223 /// ```
224 /// use ida_rs::Ida;
225 ///
226 /// let ida = Ida::new();
227 /// assert_eq!(ida.alloc(), Some(0));
228 /// ```
229 pub fn new() -> Self {
230 Self {
231 root: Mutex::new(IdaNode::new()),
232 }
233 }
234
235 /// Allocates and returns the next available ID.
236 ///
237 /// This method always returns the lowest available ID. If an ID has been freed,
238 /// it will be reused before allocating higher IDs.
239 ///
240 /// # Returns
241 ///
242 /// - `Some(id)` - The allocated ID (a `usize` value)
243 /// - `None` - If the allocator has exhausted the available ID space
244 ///
245 /// # Thread Safety
246 ///
247 /// This method is thread-safe and can be called concurrently from multiple threads.
248 /// Each call is guaranteed to return a unique ID.
249 ///
250 /// # Examples
251 ///
252 /// ```
253 /// use ida_rs::Ida;
254 ///
255 /// let ida = Ida::new();
256 ///
257 /// // Allocate IDs sequentially
258 /// let id1 = ida.alloc().unwrap();
259 /// let id2 = ida.alloc().unwrap();
260 /// assert_eq!(id1, 0);
261 /// assert_eq!(id2, 1);
262 ///
263 /// // Free an ID and reallocate
264 /// ida.free(id1);
265 /// let id3 = ida.alloc().unwrap();
266 /// assert_eq!(id3, 0); // Reuses the freed ID
267 /// ```
268 pub fn alloc(&self) -> Option<usize> {
269 let mut root = self.root.lock();
270 root.alloc(IDA_MAX_LEVELS - 1)
271 }
272
273 /// Frees a previously allocated ID, making it available for reuse.
274 ///
275 /// Once freed, the ID becomes available for future allocations. The next call
276 /// to [`alloc`](Self::alloc) will prefer reusing freed IDs before allocating
277 /// new ones.
278 ///
279 /// # Parameters
280 ///
281 /// - `id` - The ID to free. Must be a value that was previously returned by
282 /// [`alloc`](Self::alloc).
283 ///
284 /// # Behavior
285 ///
286 /// - Freeing an already-free ID is safe but has no effect
287 /// - Freeing an ID that was never allocated is safe but has no effect
288 /// - After freeing, the internal tree structure may be compacted to save memory
289 ///
290 /// # Thread Safety
291 ///
292 /// This method is thread-safe and can be called concurrently from multiple threads.
293 ///
294 /// # Examples
295 ///
296 /// ```
297 /// use ida_rs::Ida;
298 ///
299 /// let ida = Ida::new();
300 /// let id = ida.alloc().unwrap();
301 ///
302 /// // Use the ID...
303 ///
304 /// // Free it when done
305 /// ida.free(id);
306 ///
307 /// // The ID can now be reallocated
308 /// let reused_id = ida.alloc().unwrap();
309 /// assert_eq!(id, reused_id);
310 /// ```
311 pub fn free(&self, id: usize) {
312 let mut root = self.root.lock();
313 root.free(id, IDA_MAX_LEVELS - 1);
314 }
315
316 /// Checks if a given ID is currently allocated.
317 ///
318 /// This method queries whether a specific ID has been allocated and not yet freed.
319 /// It's useful for debugging, validation, or tracking ID states.
320 ///
321 /// # Parameters
322 ///
323 /// - `id` - The ID to check
324 ///
325 /// # Returns
326 ///
327 /// - `true` - If the ID is currently allocated
328 /// - `false` - If the ID is free or has never been allocated
329 ///
330 /// # Thread Safety
331 ///
332 /// This method is thread-safe and provides a consistent view at the time of the call.
333 /// Note that in concurrent scenarios, the status may change immediately after this
334 /// method returns.
335 ///
336 /// # Examples
337 ///
338 /// ```
339 /// use ida_rs::Ida;
340 ///
341 /// let ida = Ida::new();
342 ///
343 /// // Initially, no IDs are allocated
344 /// assert!(!ida.is_allocated(0));
345 /// assert!(!ida.is_allocated(100));
346 ///
347 /// // Allocate an ID
348 /// let id = ida.alloc().unwrap();
349 /// assert_eq!(id, 0);
350 ///
351 /// // Check allocation status
352 /// assert!(ida.is_allocated(0));
353 /// assert!(!ida.is_allocated(1));
354 ///
355 /// // Free the ID
356 /// ida.free(0);
357 /// assert!(!ida.is_allocated(0));
358 /// ```
359 pub fn is_allocated(&self, id: usize) -> bool {
360 let root = self.root.lock();
361 root.is_allocated(id, IDA_MAX_LEVELS - 1)
362 }
363}
364
365impl Default for Ida {
366 /// Creates a new ID allocator using the default configuration.
367 ///
368 /// This is equivalent to calling [`Ida::new()`](Self::new).
369 ///
370 /// # Examples
371 ///
372 /// ```
373 /// use ida_rs::Ida;
374 ///
375 /// let ida = Ida::default();
376 /// assert_eq!(ida.alloc(), Some(0));
377 /// ```
378 fn default() -> Self {
379 Self::new()
380 }
381}
382
383#[cfg(test)]
384mod tests {
385 use super::*;
386 use alloc::format;
387 use alloc::vec::Vec;
388 use std::sync::{Arc, Mutex};
389 use std::thread;
390
391 #[test]
392 fn test_alloc_and_free_simple() {
393 let ida = Ida::default();
394 assert_eq!(ida.alloc(), Some(0));
395 assert_eq!(ida.alloc(), Some(1));
396 assert_eq!(ida.alloc(), Some(2));
397
398 ida.free(1); // Free an ID in the middle.
399 assert_eq!(ida.alloc(), Some(1)); // Should be reused.
400
401 ida.free(0);
402 ida.free(2);
403 assert_eq!(ida.alloc(), Some(0));
404 assert_eq!(ida.alloc(), Some(2));
405 }
406
407 #[test]
408 fn test_first_level_boundary() {
409 let ida = Ida::default();
410 // Allocate the first 64 IDs to fill the first leaf.
411 for i in 0..64 {
412 assert_eq!(ida.alloc(), Some(i));
413 }
414
415 // The next allocation should cross the boundary into a new leaf.
416 assert_eq!(ida.alloc(), Some(64));
417 assert_eq!(ida.alloc(), Some(65));
418
419 // Free the boundary-crossing ID and ensure it's reused.
420 ida.free(64);
421 assert_eq!(ida.alloc(), Some(64));
422 }
423
424 #[test]
425 fn test_second_level_boundary() {
426 let ida = Ida::default();
427 let count = IDA_BITMAP_BITS * IDA_BITMAP_BITS; // 64 * 64 = 4096
428
429 // Allocate enough IDs to fill an entire first-level child node.
430 for i in 0..count {
431 assert_eq!(ida.alloc(), Some(i));
432 }
433
434 // The next allocation should cross a major boundary.
435 assert_eq!(ida.alloc(), Some(count));
436
437 // Free it and ensure it's reused.
438 ida.free(count);
439 assert_eq!(ida.alloc(), Some(count));
440 }
441
442 #[test]
443 fn test_free_unallocated() {
444 let ida = Ida::default();
445 ida.free(100); // Free an ID that was never allocated.
446 assert_eq!(ida.alloc(), Some(0)); // The first ID should still be 0.
447 }
448
449 #[test]
450 fn test_stress_and_random_free() {
451 let ida = Ida::default();
452 let mut ids = alloc::vec::Vec::new();
453 let count = 10_000;
454
455 // 1. Allocate a large number of IDs.
456 for _ in 0..count {
457 if let Some(id) = ida.alloc() {
458 ids.push(id);
459 } else {
460 panic!("Allocator ran out of space unexpectedly.");
461 }
462 }
463
464 // 2. Free a random subset of those IDs.
465 let mut freed_ids = alloc::vec::Vec::new();
466 for (i, &id) in ids.iter().enumerate() {
467 if i % 3 == 0 || i % 7 == 0 {
468 // Arbitrary condition for freeing
469 ida.free(id);
470 freed_ids.push(id);
471 }
472 }
473
474 // Sort the freed IDs to make reallocation predictable.
475 freed_ids.sort();
476
477 // 3. Allocate new IDs and check if they are the same as the freed ones.
478 for &expected_id in &freed_ids {
479 assert_eq!(ida.alloc(), Some(expected_id));
480 }
481
482 // 4. Free all remaining IDs.
483 for (i, &id) in ids.iter().enumerate() {
484 if !(i % 3 == 0 || i % 7 == 0) {
485 ida.free(id);
486 }
487 }
488 for id in freed_ids {
489 ida.free(id);
490 }
491
492 // 5. Check that the allocator is empty and starts from 0 again.
493 assert_eq!(ida.alloc(), Some(0));
494 }
495
496 #[test]
497 fn test_is_allocated() {
498 let ida = Ida::default();
499 assert!(!ida.is_allocated(0));
500 assert!(!ida.is_allocated(100));
501
502 let id1 = ida.alloc().unwrap();
503 let id2 = ida.alloc().unwrap();
504
505 assert_eq!(id1, 0);
506 assert_eq!(id2, 1);
507
508 assert!(ida.is_allocated(0));
509 assert!(ida.is_allocated(1));
510 assert!(!ida.is_allocated(2));
511 assert!(!ida.is_allocated(100));
512
513 ida.free(1);
514 assert!(ida.is_allocated(0));
515 assert!(!ida.is_allocated(1));
516 assert!(!ida.is_allocated(2));
517 }
518
519 #[test]
520 fn test_debug_output() {
521 let ida = Ida::default();
522 ida.alloc();
523 ida.alloc();
524 // The exact output is not asserted as it's complex and implementation-dependent,
525 // but this test ensures the Debug trait is implemented and doesn't panic.
526 let _ = format!("{ida:?}");
527 }
528
529 #[test]
530 fn test_multi_threaded_alloc() {
531 let ida = Arc::new(Ida::default());
532 let mut handles = Vec::new();
533 let results = Arc::new(Mutex::new(Vec::new()));
534 let num_threads = 4;
535 let ids_per_thread = 1000;
536
537 for _ in 0..num_threads {
538 let ida_clone = Arc::clone(&ida);
539 let results_clone = Arc::clone(&results);
540 let handle = thread::spawn(move || {
541 for _ in 0..ids_per_thread {
542 if let Some(id) = ida_clone.alloc() {
543 results_clone.lock().unwrap().push(id);
544 }
545 }
546 });
547 handles.push(handle);
548 }
549
550 for handle in handles {
551 handle.join().unwrap();
552 }
553
554 let mut final_ids = results.lock().unwrap();
555 assert_eq!(final_ids.len(), num_threads * ids_per_thread);
556
557 final_ids.sort();
558 let original_len = final_ids.len();
559 final_ids.dedup();
560 assert_eq!(
561 original_len,
562 final_ids.len(),
563 "Duplicate IDs were allocated in a multi-threaded context!"
564 );
565 }
566}