Skip to main content

hashmap_mem/
lib.rs

1/*
2 * Copyright (c) Peter Bjorklund. All rights reserved. https://github.com/piot/hashmap-mem
3 * Licensed under the MIT License. See LICENSE in the project root for license information.
4 */
5
6#![no_std]
7#![allow(clippy::similar_names)] // bucket_ptr/buckets_ptr naming is intentional and clear
8#![allow(clippy::cast_possible_truncation)] // Truncation is intentional with validated bounds
9#![allow(clippy::cast_ptr_alignment)] // Working with raw memory layout by design
10#![allow(clippy::branches_sharing_code)] // False positive in matches_key helper
11
12use core::cmp::{max, min};
13use core::hash::Hasher;
14use core::mem::size_of;
15use core::ops::Not;
16use core::{ptr, slice};
17use fxhash::FxHasher64;
18
19#[repr(u8)]
20#[derive(Copy, Clone, PartialEq, Eq, Debug)]
21pub enum BucketStatus {
22    Empty = 0, // Must be zero, do not change!
23    Tombstone = 1,
24    Occupied = 2,
25}
26
27#[repr(C)]
28#[derive(Copy, Clone, Debug)]
29pub struct MapHeader {
30    // Do not change the order of the fields!
31    pub capacity: u16,      // Do not change,
32    pub element_count: u16, // Do not change
33    pub key_size: u32,
34    pub value_size: u32,
35
36    pub value_offset: u32,
37    pub bucket_size: u32,
38
39    pub logical_limit: u16,
40    pub key_offset: u8,
41    pub padding_and_secret_code: u8,
42}
43
44pub struct MapInit {
45    pub key_size: u32,
46    pub key_alignment: u8,
47    pub value_size: u32,
48    pub value_alignment: u8,
49    pub capacity: u16,
50    pub logical_limit: u16,
51    pub total_size: u32,
52}
53
54#[derive(Clone, Copy, Debug)]
55pub struct BucketLayout {
56    pub bucket_size: u32,
57    pub key_offset: u8,
58    pub value_offset: u32,
59}
60
61const MAP_BUCKETS_OFFSET: usize = size_of::<MapHeader>();
62const MAX_PROBE_DISTANCE: usize = 32;
63
64#[inline]
65fn calculate_hash_bytes(key_bytes: &[u8]) -> u64 {
66    let mut hasher = FxHasher64::default();
67    hasher.write(key_bytes);
68    hasher.finish()
69}
70
71#[inline]
72fn index_from_hash(hash: u64, capacity: u16) -> usize {
73    assert!(capacity.is_power_of_two());
74
75    // take the top 16 bits; then mask to the actual size
76    // FxHash have badly mixed lower bits
77    ((hash >> 48) as usize) & ((capacity as usize) - 1)
78}
79
80/// Calculate memory layout for a map bucket
81#[inline]
82#[must_use]
83pub fn calculate_bucket_layout(
84    key_size: u32,
85    key_alignment: u8,
86    value_size: u32,
87    value_alignment: u8,
88) -> BucketLayout {
89    let status_size: u32 = 1;
90    let mut current_offset = status_size;
91
92    // Align key
93    let key_align = u32::from(key_alignment);
94    let key_offset = (current_offset + key_align - 1) & !(key_align - 1);
95    current_offset = key_offset + key_size;
96
97    // Align value
98    let value_align = u32::from(value_alignment);
99    let value_offset = (current_offset + value_align - 1) & !(value_align - 1);
100    current_offset = value_offset + value_size;
101
102    // Calculate final bucket size with proper alignment
103    let bucket_content_alignment = max(key_align, value_align);
104    let bucket_size =
105        (current_offset + bucket_content_alignment - 1) & !(bucket_content_alignment - 1);
106
107    BucketLayout {
108        bucket_size,
109        key_offset: key_offset as u8,
110        value_offset,
111    }
112}
113
114#[must_use]
115pub const fn total_size(capacity: u16, bucket_size: u32) -> u32 {
116    (MAP_BUCKETS_OFFSET + capacity as usize * bucket_size as usize) as u32
117}
118
119#[must_use]
120pub fn layout(
121    key_size: u32,
122    key_alignment: u8,
123    value_size: u32,
124    value_alignment: u8,
125    logical_limit: u16,
126) -> (BucketLayout, MapInit) {
127    let capacity = logical_limit.next_power_of_two();
128    let bucket_layout =
129        calculate_bucket_layout(key_size, key_alignment, value_size, value_alignment);
130    (
131        bucket_layout,
132        MapInit {
133            key_size,
134            key_alignment,
135            value_size,
136            value_alignment,
137            capacity,
138            logical_limit,
139            total_size: total_size(capacity, bucket_layout.bucket_size),
140        },
141    )
142}
143
144pub const SECRET_CODE: u8 = 0x3d;
145
146/// Initialize a new hash map in pre-allocated memory
147///
148/// # Safety
149///
150/// - `map_base` must point to valid, properly aligned memory of sufficient size
151/// - The memory must remain valid for the lifetime of the map
152///
153/// # Panics
154///
155/// Panics if `config.capacity` is not a power of two
156pub unsafe fn init(map_base: *mut u8, config: &MapInit) {
157    assert!(
158        config.capacity.is_power_of_two(),
159        "Capacity must be a power of two"
160    );
161
162    let map_header = map_base.cast::<MapHeader>();
163    let layout = calculate_bucket_layout(
164        config.key_size,
165        config.key_alignment,
166        config.value_size,
167        config.value_alignment,
168    );
169
170    // Initialize header
171    unsafe {
172        ptr::write(
173            map_header,
174            MapHeader {
175                capacity: config.capacity,
176                logical_limit: config.logical_limit,
177                key_size: config.key_size,
178                value_size: config.value_size,
179                bucket_size: layout.bucket_size,
180                key_offset: layout.key_offset,
181                value_offset: layout.value_offset,
182                element_count: 0,
183                padding_and_secret_code: SECRET_CODE,
184            },
185        );
186    }
187
188    // Initialize buckets to empty
189    let buckets_start_ptr = unsafe { map_base.add(MAP_BUCKETS_OFFSET) };
190    let capacity = usize::from(config.capacity);
191    let bucket_size = layout.bucket_size as usize;
192
193    // Zero out all bucket status bytes (Empty = 0)
194    for i in 0..capacity {
195        unsafe {
196            ptr::write(
197                buckets_start_ptr.add(i * bucket_size),
198                BucketStatus::Empty as u8,
199            );
200        }
201    }
202}
203
204/// Validate the map header and structure
205///
206/// # Safety
207///
208/// - `base_ptr` must point to a valid map header
209///
210/// # Returns
211///
212/// `true` if the map is valid, `false` otherwise
213#[inline]
214#[must_use]
215pub const unsafe fn validate(base_ptr: *const u8) -> bool {
216    unsafe {
217        let header = &*base_ptr.cast::<MapHeader>();
218
219        if header.padding_and_secret_code != SECRET_CODE {
220            return false;
221        }
222
223        if header.key_size == 0 {
224            return false;
225        }
226        if header.capacity == 0 {
227            return false;
228        }
229
230        if !header.capacity.is_power_of_two() {
231            return false;
232        }
233
234        if header.element_count > header.capacity {
235            return false;
236        }
237
238        if header.logical_limit > header.capacity {
239            return false;
240        }
241
242        true
243    }
244}
245
246/// Assert that the map is valid
247///
248/// # Safety
249///
250/// - `base_ptr` must point to a valid map header
251///
252/// # Panics
253///
254/// Panics if the map is invalid
255#[inline]
256unsafe fn assert_validate(base_ptr: *const u8) {
257    unsafe {
258        assert!(validate(base_ptr), "Invalid map structure");
259    }
260}
261
262/// Fast key comparison helper
263// TODO: Check if the performance difference is significant
264#[inline]
265unsafe fn matches_key(a: *const u8, b: *const u8, len: usize) -> bool {
266    unsafe {
267        if len <= 16 {
268            if len == 0 {
269                true
270            } else {
271                for i in 0..len {
272                    if *a.add(i) != *b.add(i) {
273                        return false;
274                    }
275                }
276                true
277            }
278        } else {
279            slice::from_raw_parts(a, len) == slice::from_raw_parts(b, len)
280        }
281    }
282}
283
284/// Reserve a new entry in the map
285///
286/// # Safety
287///
288/// - `base_ptr` must point to a valid initialized map
289/// - `key_ptr` must point to a valid key of the size specified in the map header
290///
291/// # Panics
292///
293/// Panics if the key already exists in the map
294///
295/// # Returns
296///
297/// Pointer to the value location, or null if the map is full
298#[inline]
299pub unsafe fn reserve_entry(base_ptr: *mut u8, key_ptr: *const u8) -> *mut u8 {
300    unsafe {
301        assert_validate(base_ptr);
302
303        let header = &*base_ptr.cast::<MapHeader>();
304        let capacity = header.capacity as usize;
305        let key_size = header.key_size as usize;
306        let bucket_size = header.bucket_size as usize;
307        let key_offset = header.key_offset as usize;
308        let value_offset = header.value_offset as usize;
309
310        let buckets_ptr = base_ptr.add(MAP_BUCKETS_OFFSET);
311        let key_slice = slice::from_raw_parts(key_ptr, key_size);
312        let hash = calculate_hash_bytes(key_slice);
313
314        let mut index = index_from_hash(hash, header.capacity);
315
316        let mut first_tombstone = None;
317        let probe_limit = min(capacity, MAX_PROBE_DISTANCE);
318
319        for _ in 0..probe_limit {
320            let bucket_ptr = buckets_ptr.add(index * bucket_size);
321            let status = *bucket_ptr;
322
323            match status {
324                status if status == BucketStatus::Empty as u8 => {
325                    // Use tombstone if found, otherwise use current empty slot
326                    let insert_index = first_tombstone.unwrap_or(index);
327                    let target_bucket = buckets_ptr.add(insert_index * bucket_size);
328
329                    *target_bucket = BucketStatus::Occupied as u8;
330                    let target_key_ptr = target_bucket.add(key_offset);
331                    ptr::copy_nonoverlapping(key_ptr, target_key_ptr, key_size);
332
333                    // Update element count
334                    let header_mut = &mut *base_ptr.cast::<MapHeader>();
335                    header_mut.element_count += 1;
336
337                    return target_bucket.add(value_offset);
338                }
339                status if status == BucketStatus::Occupied as u8 => {
340                    let existing_key_ptr = bucket_ptr.add(key_offset);
341                    assert!(
342                        !matches_key(existing_key_ptr, key_ptr, key_size),
343                        "Key already exists in map"
344                    );
345                }
346                status if status == BucketStatus::Tombstone as u8 => {
347                    if first_tombstone.is_none() {
348                        first_tombstone = Some(index);
349                    }
350                }
351                _ => unreachable!(),
352            }
353
354            // Linear probing with wraparound using bitmask
355            index = (index + 1) & (capacity - 1);
356        }
357
358        // If we found a tombstone during probing, use it
359        if let Some(tombstone_index) = first_tombstone {
360            let target_bucket = buckets_ptr.add(tombstone_index * bucket_size);
361
362            // Mark as occupied and copy key
363            *target_bucket = BucketStatus::Occupied as u8;
364            let target_key_ptr = target_bucket.add(key_offset);
365            ptr::copy_nonoverlapping(key_ptr, target_key_ptr, key_size);
366
367            // Update element count
368            let header_mut = &mut *base_ptr.cast::<MapHeader>();
369            header_mut.element_count += 1;
370
371            return target_bucket.add(value_offset);
372        }
373
374        // Map is full or probe limit exceeded
375        ptr::null_mut()
376    }
377}
378
379/// Get or reserve an entry in the map
380///
381/// # Safety
382///
383/// - `base_ptr` must point to a valid initialized map
384/// - `key_ptr` must point to a valid key of the size specified in the map header
385///
386/// # Returns
387///
388/// Pointer to the value location, or null if the map is full
389#[inline]
390pub unsafe fn get_or_reserve_entry(base_ptr: *mut u8, key_ptr: *const u8) -> *mut u8 {
391    unsafe {
392        assert_validate(base_ptr);
393
394        let header = &*base_ptr.cast::<MapHeader>();
395        let capacity = header.capacity as usize;
396        let key_size = header.key_size as usize;
397        let bucket_size = header.bucket_size as usize;
398        let key_offset = header.key_offset as usize;
399        let value_offset = header.value_offset as usize;
400
401        let buckets_ptr = base_ptr.add(MAP_BUCKETS_OFFSET);
402        let key_slice = slice::from_raw_parts(key_ptr, key_size);
403        let hash = calculate_hash_bytes(key_slice);
404
405        // Initial probe position
406        let mut index = index_from_hash(hash, header.capacity);
407
408        // Track first tombstone for potential reuse
409        let mut first_tombstone = None;
410        let probe_limit = min(capacity, MAX_PROBE_DISTANCE);
411
412        for _ in 0..probe_limit {
413            let bucket_ptr = buckets_ptr.add(index * bucket_size);
414            let status = *bucket_ptr;
415
416            match status {
417                status if status == BucketStatus::Empty as u8 => {
418                    // TODO: Maybe go back to BucketStatus as constants instead, this feel a bit awkward
419                    // Use tombstone if found, otherwise use current empty slot
420                    let insert_index = first_tombstone.unwrap_or(index);
421                    let target_bucket = buckets_ptr.add(insert_index * bucket_size);
422
423                    // Mark as occupied and copy key
424                    *target_bucket = BucketStatus::Occupied as u8;
425                    let target_key_ptr = target_bucket.add(key_offset);
426                    ptr::copy_nonoverlapping(key_ptr, target_key_ptr, key_size);
427
428                    // Update element count
429                    let header_mut = &mut *base_ptr.cast::<MapHeader>();
430                    header_mut.element_count += 1;
431
432                    return target_bucket.add(value_offset);
433                }
434                status if status == BucketStatus::Occupied as u8 => {
435                    // Check if keys match
436                    let existing_key_ptr = bucket_ptr.add(key_offset);
437                    if matches_key(existing_key_ptr, key_ptr, key_size) {
438                        return bucket_ptr.add(value_offset);
439                    }
440                }
441                status if status == BucketStatus::Tombstone as u8 => {
442                    // Remember first tombstone for potential reuse
443                    if first_tombstone.is_none() {
444                        first_tombstone = Some(index);
445                    }
446                }
447                _ => unreachable!(),
448            }
449
450            // Linear probing with wraparound using bitmask
451            index = (index + 1) & (capacity - 1);
452        }
453
454        // If we found a tombstone during probing, use it
455        if let Some(tombstone_index) = first_tombstone {
456            let target_bucket = buckets_ptr.add(tombstone_index * bucket_size);
457
458            // Mark as occupied and copy key
459            *target_bucket = BucketStatus::Occupied as u8;
460            let target_key_ptr = target_bucket.add(key_offset);
461            ptr::copy_nonoverlapping(key_ptr, target_key_ptr, key_size);
462
463            // Update element count
464            let header_mut = &mut *base_ptr.cast::<MapHeader>();
465            header_mut.element_count += 1;
466
467            return target_bucket.add(value_offset);
468        }
469
470        // Map is full or probe limit exceeded
471        ptr::null_mut()
472    }
473}
474
475/// Check if a key exists in the map
476///
477/// # Safety
478///
479/// - `base_ptr` must point to a valid initialized map
480/// - `key_ptr` must point to a valid key of the size specified in the map header
481#[inline]
482#[must_use]
483pub unsafe fn has(base_ptr: *const u8, key_ptr: *const u8) -> bool {
484    unsafe { lookup(base_ptr.cast_mut(), key_ptr).is_null().not() }
485}
486
487/// Lookup an existing entry in the map
488///
489/// # Safety
490///
491/// - `base_ptr` must point to a valid initialized map
492/// - `key_ptr` must point to a valid key of the size specified in the map header
493///
494/// # Returns
495///
496/// Pointer to the found value, or null if not found
497#[inline]
498pub unsafe fn lookup(base_ptr: *mut u8, key_ptr: *const u8) -> *mut u8 {
499    unsafe {
500        assert_validate(base_ptr);
501
502        let header = &*base_ptr.cast::<MapHeader>();
503        let capacity = header.capacity as usize;
504        let key_size = header.key_size as usize;
505        let bucket_size = header.bucket_size as usize;
506        let key_offset = header.key_offset as usize;
507        let value_offset = header.value_offset as usize;
508
509        let buckets_ptr = base_ptr.add(MAP_BUCKETS_OFFSET);
510        let key_slice = slice::from_raw_parts(key_ptr, key_size);
511        let hash = calculate_hash_bytes(key_slice);
512
513        // Initial probe position
514        let mut index = index_from_hash(hash, header.capacity);
515        let probe_limit = min(capacity, MAX_PROBE_DISTANCE);
516
517        for _ in 0..probe_limit {
518            let bucket_ptr = buckets_ptr.add(index * bucket_size);
519            let status = *bucket_ptr;
520
521            match status {
522                status if status == BucketStatus::Empty as u8 => {
523                    // TODO: Maybe go back to constant
524                    // Empty slot means the key is not in the map
525                    return ptr::null_mut();
526                }
527                status if status == BucketStatus::Occupied as u8 => {
528                    // Check if keys match
529                    let existing_key_ptr = bucket_ptr.add(key_offset);
530                    if matches_key(existing_key_ptr, key_ptr, key_size) {
531                        return bucket_ptr.add(value_offset);
532                    }
533                }
534                _ => {} // Continue probing for tombstones
535            }
536
537            index = (index + 1) & (capacity - 1);
538        }
539
540        // Key not found within probe limit
541        ptr::null_mut()
542    }
543}
544
545/// Remove an entry from the map
546///
547/// # Safety
548///
549/// - `base_ptr` must point to a valid initialized map
550/// - `key_ptr` must point to a valid key of the size specified in the map header
551///
552/// # Returns
553///
554/// `true` if the key was found and removed, `false` otherwise
555#[inline]
556pub unsafe fn remove(base_ptr: *mut u8, key_ptr: *const u8) -> bool {
557    unsafe {
558        assert_validate(base_ptr);
559
560        let header = &*base_ptr.cast::<MapHeader>();
561        let capacity = header.capacity as usize;
562        let key_size = header.key_size as usize;
563        let bucket_size = header.bucket_size as usize;
564        let key_offset = header.key_offset as usize;
565
566        let buckets_ptr = base_ptr.add(MAP_BUCKETS_OFFSET);
567        let key_slice = slice::from_raw_parts(key_ptr, key_size);
568        let hash = calculate_hash_bytes(key_slice);
569
570        // Initial probe position
571        let mut index = index_from_hash(hash, header.capacity);
572        let probe_limit = min(capacity, MAX_PROBE_DISTANCE);
573
574        for _ in 0..probe_limit {
575            let bucket_ptr = buckets_ptr.add(index * bucket_size);
576            let status = *bucket_ptr;
577
578            match status {
579                status if status == BucketStatus::Empty as u8 => {
580                    // Empty slot means the key is not in the map
581                    return false;
582                }
583                status if status == BucketStatus::Occupied as u8 => {
584                    // Check if keys match
585                    let existing_key_ptr = bucket_ptr.add(key_offset);
586                    if matches_key(existing_key_ptr, key_ptr, key_size) {
587                        // Convert to tombstone
588                        *bucket_ptr = BucketStatus::Tombstone as u8;
589
590                        // Update count
591                        let header_mut = &mut *base_ptr.cast::<MapHeader>();
592                        header_mut.element_count -= 1;
593
594                        return true;
595                    }
596                }
597                _ => {} // Continue probing for tombstones
598            }
599
600            index = (index + 1) & (capacity - 1);
601        }
602
603        // Key not found within probe limit
604        false
605    }
606}
607
608/// Copy all entries from source map to target map
609///
610/// # Safety
611///
612/// - Both maps must be properly initialized with compatible layouts (capacity can differ)
613/// - Target map must have sufficient capacity
614///
615/// # Panics
616///
617/// Panics if the maps have incompatible layouts (different bucket sizes, key sizes, or value sizes)
618///
619/// # Returns
620///
621/// `true` if the operation succeeded, `false` if the target has insufficient capacity
622#[inline]
623pub unsafe fn overwrite(target_base: *mut u8, source: *const u8) -> bool {
624    unsafe {
625        assert_validate(target_base);
626        assert_validate(source);
627
628        let target_header = &mut *target_base.cast::<MapHeader>();
629        let source_header = &*source.cast::<MapHeader>();
630        // Check if target has enough capacity
631        if target_header.logical_limit < source_header.element_count {
632            return false;
633        }
634
635        // Validate compatible layouts
636        assert_eq!(
637            target_header.bucket_size, source_header.bucket_size,
638            "Incompatible bucket sizes"
639        );
640        assert_eq!(
641            target_header.key_size, source_header.key_size,
642            "Incompatible key sizes"
643        );
644        assert_eq!(
645            target_header.value_size, source_header.value_size,
646            "Incompatible value sizes"
647        );
648
649        let source_buckets_ptr = source.add(MAP_BUCKETS_OFFSET);
650        let bucket_size = source_header.bucket_size as usize;
651        let key_offset = source_header.key_offset as usize;
652        let value_offset = source_header.value_offset as usize;
653        let value_size = source_header.value_size as usize;
654
655        // Copy each occupied bucket
656        for i in 0..source_header.capacity as usize {
657            let source_bucket = source_buckets_ptr.add(i * bucket_size);
658
659            if *source_bucket == BucketStatus::Occupied as u8 {
660                let source_key_ptr = source_bucket.add(key_offset);
661                let source_value_ptr = source_bucket.add(value_offset);
662
663                let target_value_ptr = get_or_reserve_entry(target_base, source_key_ptr);
664
665                if target_value_ptr.is_null() {
666                    return false;
667                }
668
669                ptr::copy_nonoverlapping(source_value_ptr, target_value_ptr, value_size);
670            }
671        }
672
673        true
674    }
675}
676
677/// Find the next valid entry in the map
678///
679/// # Safety
680///
681/// - `base` must point to a valid initialized map
682///
683/// # Returns
684///
685/// Tuple of (`key_ptr`, `value_ptr`, index) of the next valid entry,
686/// or (null, null, 0xFFFF) if no more entries exist
687#[inline]
688pub unsafe fn find_next_valid_entry(base: *mut u8, start_index: u16) -> (*const u8, *mut u8, u16) {
689    unsafe {
690        assert_validate(base);
691
692        let map_header = &*base.cast::<MapHeader>();
693        let bucket_size = map_header.bucket_size as usize;
694        let buckets_start = base.add(MAP_BUCKETS_OFFSET);
695        let key_offset = map_header.key_offset as usize;
696        let value_offset = map_header.value_offset as usize;
697
698        let mut index = start_index as usize;
699
700        while index < map_header.capacity as usize {
701            let entry_ptr = buckets_start.add(index * bucket_size);
702
703            // Properly use the enum instead of magic number
704            if *entry_ptr == BucketStatus::Occupied as u8 {
705                let key_addr = entry_ptr.add(key_offset);
706                let value_addr = entry_ptr.add(value_offset);
707
708                return (key_addr, value_addr, index as u16);
709            }
710
711            index += 1;
712        }
713
714        (ptr::null(), ptr::null_mut(), 0xFFFF)
715    }
716}