Skip to main content

reloaded_memory_buffers/structs/internal/
locator_header.rs

1extern crate alloc;
2use crate::internal::buffer_allocator::allocate;
3use crate::structs::errors::ItemAllocationError;
4use crate::structs::internal::LocatorItem;
5use crate::structs::params::BufferAllocatorSettings;
6use crate::structs::SafeLocatorItem;
7use crate::utilities::cached::get_sys_info;
8use crate::utilities::wrappers::Unaligned;
9use core::alloc::Layout;
10use core::cell::Cell;
11use core::cmp::min;
12use core::mem::size_of;
13use core::ptr::null_mut;
14use core::sync::atomic::{AtomicI32, Ordering};
15
16/// Static length of this locator.
17pub(crate) const LENGTH: usize = 4096;
18
19/// Length of buffers preallocated in this locator.
20///
21/// # Remarks
22///
23/// On Windows there is an allocation granularity (normally 64KB) which means that
24/// minimum amount of bytes you can allocate is 64KB; even if you only need 1 byte.
25///
26/// Our locator is a 4096 byte structure which means that it would be a waste to not
27/// do anything with the remaining data. So we chunk the remaining data by this amount
28/// and pre-register them as buffers.
29pub(crate) const LENGTH_OF_PREALLOCATED_CHUNKS: u32 = 16384;
30
31/// Returns the maximum possible amount of items in this locator.
32pub(crate) const MAX_ITEM_COUNT: u32 =
33    ((LENGTH - size_of::<LocatorHeader>()) / size_of::<LocatorItem>()) as u32;
34
35/// Represents the header of an individual memory locator.
36#[repr(C, align(1))]
37pub struct LocatorHeader {
38    pub this_address: Unaligned<*mut LocatorHeader>,
39    pub next_locator_ptr: Unaligned<*mut LocatorHeader>,
40    pub is_locked: AtomicI32,
41    pub flags: u8,
42    pub num_items: u8,
43    padding: [u8; 2],
44}
45
46impl LocatorHeader {
47    /// Creates a new LocatorHeader instance.
48    #[allow(clippy::new_without_default)]
49    #[cfg(test)]
50    pub fn new() -> Self {
51        extern crate std;
52        LocatorHeader {
53            this_address: Unaligned::new(std::ptr::null_mut()),
54            next_locator_ptr: Unaligned::new(std::ptr::null_mut::<LocatorHeader>()),
55            is_locked: AtomicI32::new(0),
56            flags: 0,
57            num_items: 0,
58            padding: [0; 2],
59        }
60    }
61
62    /// Initializes the locator header values at a specific address.
63    /// # Arguments
64    ///
65    /// * `length` - Number of bytes available.
66    pub(crate) fn initialize(&mut self, length: usize) {
67        self.set_default_values();
68        let remaining_bytes = (length - LENGTH) as u32;
69
70        // We allocate to allocation_granularity, however, under some platforms (*cough* M1 macOS)
71        // W^X policy is enforced, in which case, we cannot allocate executable memory here,
72        // as the header would also be affected.
73
74        // We will use the remaining space for more headers on these affected platforms, and
75        // on non-W^X platforms, we will use it for buffers.
76        #[cfg(all(target_os = "macos", target_arch = "aarch64"))]
77        Self::initialize_remaining_space_as_headers(self as *mut LocatorHeader, remaining_bytes);
78
79        #[cfg(not(all(target_os = "macos", target_arch = "aarch64")))]
80        self.initialize_remaining_space_as_buffers(remaining_bytes);
81    }
82
83    fn set_default_values(&mut self) {
84        self.this_address = Unaligned::new(self as *mut LocatorHeader);
85        self.next_locator_ptr = Unaligned::new(null_mut());
86        self.is_locked = AtomicI32::new(0);
87        self.flags = 0;
88        self.num_items = 0;
89    }
90
91    fn initialize_remaining_space_as_buffers(&mut self, mut remaining_bytes: u32) {
92        let mut num_items = 0u8;
93        unsafe {
94            let buffer_address = (self.this_address.value as *mut u8).add(LENGTH);
95            let mut current_item = self.get_first_item();
96
97            while remaining_bytes > 0 {
98                let this_length = min(LENGTH_OF_PREALLOCATED_CHUNKS, remaining_bytes);
99                *current_item = LocatorItem::new(buffer_address as usize, this_length);
100                current_item = current_item.offset(1);
101                remaining_bytes -= this_length;
102                num_items += 1;
103            }
104        }
105
106        self.num_items = num_items;
107    }
108
109    #[cfg(all(target_os = "macos", target_arch = "aarch64"))]
110    fn initialize_remaining_space_as_headers(header: *mut LocatorHeader, mut remaining_bytes: u32) {
111        unsafe {
112            let mut current_header = header;
113            while remaining_bytes >= LENGTH as u32 {
114                let next_header = (current_header as *mut u8).add(LENGTH) as *mut LocatorHeader;
115                (*next_header).set_default_values();
116                (*current_header).next_locator_ptr = Unaligned::new(next_header);
117                current_header = next_header;
118                remaining_bytes -= LENGTH as u32;
119            }
120        }
121    }
122
123    /// Returns the version represented by the first 3 bits of `flags`.
124    #[allow(dead_code)]
125    pub fn version(&self) -> u8 {
126        self.flags & 0x07
127    }
128
129    /// Sets the version represented by the first 3 bits of `flags`.
130    #[allow(dead_code)]
131    pub fn set_version(&mut self, value: u8) {
132        self.flags = (self.flags & 0xF8) | (value & 0x07);
133    }
134
135    /// Returns true if next locator is present.
136    pub fn has_next_locator(&self) -> bool {
137        !self.next_locator_ptr.value.is_null()
138    }
139
140    /// Returns true if this buffer is full.
141    pub fn is_full(&self) -> bool {
142        self.num_items as usize >= MAX_ITEM_COUNT as usize
143    }
144
145    /// Tries to acquire the lock.
146    ///
147    /// Returns: True if the lock was successfully acquired, false otherwise.
148    pub fn try_lock(&mut self) -> bool {
149        // Since Rust doesn't have a direct equivalent of C#'s `Interlocked.CompareExchange`,
150        // we need to use the atomic operations from the `std::sync::atomic` module.
151        self.is_locked
152            .compare_exchange(0, 1, Ordering::AcqRel, Ordering::Acquire)
153            .is_ok()
154    }
155
156    /// Acquires the lock, blocking until it can do so.
157    pub fn lock(&mut self) {
158        while !self.try_lock() {
159            #[cfg(all(feature = "std", not(unix), not(windows)))]
160            {
161                std::thread::yield_now();
162            }
163
164            #[cfg(unix)]
165            unsafe {
166                libc::sched_yield();
167            }
168
169            #[cfg(windows)]
170            unsafe {
171                windows_sys::Win32::System::Threading::SwitchToThread();
172            }
173        }
174    }
175
176    /// Unlocks the object in a thread-safe manner.
177    ///
178    /// # Panics
179    ///
180    /// If the buffer is already unlocked, this error is thrown.
181    /// It is only thrown in debug mode.
182    pub fn unlock(&mut self) {
183        // Set _is_locked to 0 and return the original value.
184        let original = self.is_locked.swap(0, Ordering::AcqRel);
185
186        // If the original value was already 0, something went wrong.
187        debug_assert_ne!(
188            original, 0,
189            "Attempted to unlock a LocatorHeader that wasn't locked"
190        );
191    }
192
193    /// Gets the first item.
194    pub fn get_first_item(&self) -> *mut LocatorItem {
195        // Add 1 to the address to get the address of the first item.
196        unsafe { (self as *const LocatorHeader).add(1) as *mut LocatorItem }
197    }
198
199    /// Gets the item at a specific index.
200    ///
201    /// index: Index to get item at.
202    pub fn get_item(&self, index: usize) -> *mut LocatorItem {
203        unsafe { self.get_first_item().add(index) }
204    }
205
206    /// Gets the first available item with a lock.
207    ///
208    /// # Arguments
209    ///
210    /// * `size` - Required size of the buffer.
211    /// * `min_address` - Minimum address for the allocation.
212    /// * `max_address` - Maximum address for the allocation.
213    ///
214    /// # Safety
215    ///
216    /// Uses raw pointers, thus is technically unsafe.
217    ///
218    /// # Returns
219    ///
220    /// Returns a locked locator item. Make sure to properly dispose of it using the appropriate method,
221    /// as disposing will release the lock.
222    pub unsafe fn get_first_available_item_locked(
223        &self,
224        size: u32,
225        min_address: usize,
226        max_address: usize,
227    ) -> Option<SafeLocatorItem> {
228        let mut current_item = self.get_first_item();
229        let final_item = current_item.add(self.num_items as usize);
230        while current_item < final_item {
231            let item_ref = &mut *current_item;
232            if item_ref.can_use(size, min_address, max_address) && item_ref.try_lock() {
233                return Some({
234                    let item: *mut LocatorItem = item_ref;
235                    SafeLocatorItem {
236                        item: Cell::new(item),
237                    }
238                });
239            }
240
241            current_item = current_item.offset(1);
242        }
243
244        None
245    }
246
247    /// Tries to allocate an additional item in the header, if possible.
248    ///
249    /// # Arguments
250    ///
251    /// * `size` - Required size of buffer.
252    /// * `min_address` - Minimum address for the allocation.
253    /// * `max_address` - Maximum address for the allocation.
254    ///
255    /// # Returns
256    ///
257    /// Returns the successfully allocated buffer (locked) wrapped in an Option.
258    ///
259    /// # Remarks
260    ///
261    /// If an item can't be allocated, there are simply no slots left.
262    ///
263    /// # Errors
264    ///
265    /// If the memory cannot be allocated within the needed constraints, this function will return `Err(MemoryBufferAllocationException)`.
266    ///
267    /// # Safety
268    ///
269    /// This function is unsafe as it requires the caller to ensure that calls are properly synchronized.
270    /// Concurrent access without synchronization can lead to undefined behavior.
271    pub fn try_allocate_item(
272        &mut self,
273        size: u32,
274        min_address: usize,
275        max_address: usize,
276    ) -> Result<SafeLocatorItem, ItemAllocationError> {
277        if self.is_full() {
278            return Err(ItemAllocationError::NoSpaceInHeader);
279        }
280
281        // Note: We don't need to check if an item was created while we were waiting for the lock,
282        // because the item in question will be locked by the one who created it.
283        // We only need to (re)check if there's space.
284        self.lock();
285
286        if self.is_full() {
287            self.unlock();
288            return Err(ItemAllocationError::NoSpaceInHeader);
289        }
290
291        let mut settings = BufferAllocatorSettings::new();
292        settings.min_address = min_address;
293        settings.max_address = max_address;
294        settings.size = size;
295        let result = allocate(&mut settings);
296
297        match result {
298            Ok(mut allocated_memory) => {
299                allocated_memory.lock();
300
301                unsafe {
302                    let target = self.get_item(self.num_items as usize);
303                    *target = allocated_memory;
304                    let item = SafeLocatorItem {
305                        item: Cell::new(target),
306                    };
307
308                    self.num_items += 1;
309                    self.unlock();
310                    Ok(item)
311                }
312            }
313            Err(_) => {
314                self.unlock();
315                Err(ItemAllocationError::CannotAllocateMemory)
316            }
317        }
318    }
319
320    /// Gets the next header in the chain, allocating it if necessary.
321    ///
322    /// # Returns
323    ///
324    /// Result with the address of next header, or error string.
325    ///
326    pub fn get_next_locator(&mut self) -> Result<*mut LocatorHeader, &'static str> {
327        // No-op if already exists.
328        if self.has_next_locator() {
329            return Ok(self.next_locator_ptr.value);
330        }
331
332        self.lock();
333
334        // Check again, in case it was created while we were waiting for the lock.
335        if self.has_next_locator() {
336            self.unlock();
337            return Ok(self.next_locator_ptr.value);
338        }
339
340        // Allocate the next locator.
341        let sys_info = get_sys_info();
342        let alloc_size = sys_info.allocation_granularity;
343        unsafe {
344            let addr = alloc::alloc::alloc(
345                Layout::from_size_align(alloc_size as usize, sys_info.page_size as usize).unwrap(),
346            );
347            if addr.is_null() {
348                self.unlock();
349                return Err(
350                    "Failed to allocate memory for LocatorHeader. Is this process out of memory?",
351                );
352            }
353
354            self.next_locator_ptr.value = addr as *mut LocatorHeader;
355            (*self.next_locator_ptr.value).initialize(alloc_size as usize);
356            self.unlock();
357
358            Ok(self.next_locator_ptr.value)
359        }
360    }
361}
362
363#[cfg(test)]
364mod tests {
365    extern crate std;
366    use crate::structs::internal::locator_header::{Unaligned, LENGTH, MAX_ITEM_COUNT};
367    use crate::structs::internal::LocatorHeader;
368    use crate::utilities::cached::get_sys_info;
369    use memoffset::offset_of;
370    use std::alloc::{alloc, Layout};
371    use std::mem::{align_of, size_of};
372    use std::sync::atomic::Ordering;
373
374    // Ternary Operator
375    macro_rules! expected_offset {
376        ($true_value:expr, $false_value:expr) => {
377            if size_of::<usize>() == 8 {
378                $true_value
379            } else {
380                $false_value
381            }
382        };
383    }
384
385    #[test]
386    fn is_correct_size() {
387        let expected = if size_of::<usize>() == 4 { 16 } else { 24 };
388        assert_eq!(size_of::<LocatorHeader>(), expected);
389
390        assert_eq!(
391            expected_offset!(0, 0),
392            offset_of!(LocatorHeader, this_address)
393        );
394        assert_eq!(
395            expected_offset!(8, 4),
396            offset_of!(LocatorHeader, next_locator_ptr)
397        );
398        assert_eq!(
399            expected_offset!(16, 8),
400            offset_of!(LocatorHeader, is_locked)
401        );
402        assert_eq!(expected_offset!(20, 12), offset_of!(LocatorHeader, flags));
403        assert_eq!(
404            expected_offset!(21, 13),
405            offset_of!(LocatorHeader, num_items)
406        );
407    }
408
409    #[test]
410    fn has_correct_max_item_count() {
411        let expected = if size_of::<usize>() == 4 { 255 } else { 203 };
412        assert_eq!(MAX_ITEM_COUNT, expected);
413    }
414
415    #[test]
416    fn try_lock_should_lock_header_when_lock_is_available() {
417        // Arrange
418        let mut header = LocatorHeader::new();
419
420        // Act
421        let result = header.try_lock();
422
423        // Assert
424        assert!(result);
425        assert_eq!(1, header.is_locked.load(Ordering::Acquire));
426    }
427
428    #[test]
429    fn try_lock_should_not_lock_header_when_lock_is_already_acquired() {
430        // Arrange
431        let mut header = LocatorHeader::new();
432        header.try_lock();
433
434        // Act
435        let result = header.try_lock();
436
437        // Assert
438        assert!(!result);
439        assert_eq!(1, header.is_locked.load(Ordering::Acquire));
440    }
441
442    #[test]
443    fn lock_should_acquire_lock_when_lock_is_available() {
444        // Arrange
445        let mut header = LocatorHeader::new();
446
447        // Act
448        header.lock();
449
450        // Assert
451        assert_eq!(1, header.is_locked.load(Ordering::Acquire));
452    }
453
454    #[test]
455    fn unlock_should_release_lock_when_header_is_locked() {
456        // Arrange
457        let mut header = LocatorHeader::new();
458        header.lock();
459
460        // Act
461        header.unlock();
462
463        // Assert
464        assert_eq!(0, header.is_locked.load(Ordering::Acquire));
465    }
466
467    #[test]
468    fn version_should_be_3_bits() {
469        let mut header = LocatorHeader::new();
470
471        for value in 0..8 {
472            // 3 bits can represent 8 different values
473            header.set_version(value);
474            assert_eq!(header.version(), value);
475        }
476
477        // Values larger than 3 bits should overflow and only retain the least significant 3 bits
478        header.set_version(8);
479        assert_eq!(header.version(), 0);
480    }
481
482    #[cfg(debug_assertions)] // This code will only be compiled in debug mode
483    #[test]
484    #[should_panic(expected = "Attempted to unlock a LocatorHeader that wasn't locked")]
485    fn unlock_should_throw_exception_when_header_is_not_locked() {
486        let mut header = LocatorHeader::new();
487        header.unlock();
488    }
489
490    #[test]
491    fn get_first_available_item_locked_should_return_expected_result() {
492        unsafe {
493            // Arrange
494            let mut header_buf: [u8; LENGTH] = [0; LENGTH];
495            let header: *mut LocatorHeader = header_buf.as_mut_ptr() as *mut LocatorHeader;
496
497            (*header).this_address = Unaligned::new(header);
498            (*header).num_items = 2;
499
500            let first_item = (*header).get_first_item();
501            (*first_item).base_address = Unaligned::new(100);
502            (*first_item).size = 50;
503            (*first_item).position = 25;
504
505            let second_item = (*header).get_item(1);
506            (*second_item).base_address = Unaligned::new(200);
507            (*second_item).size = 50;
508            (*second_item).position = 25;
509
510            // Act
511            let result = (*header).get_first_available_item_locked(25, 100, 300);
512
513            // Assert
514            assert!(result.is_some());
515            let result = result.unwrap();
516            let locator_item = result.item.get();
517            let base_address = (*locator_item).base_address.value;
518            assert_eq!(base_address, 100);
519            assert!((*locator_item).is_taken());
520        }
521    }
522
523    #[test]
524    fn get_first_available_item_locked_should_return_null_if_no_available_item_because_size_is_insufficient(
525    ) {
526        unsafe {
527            // Arrange
528            let mut header_buf: [u8; LENGTH] = [0; LENGTH];
529            let header: *mut LocatorHeader = header_buf.as_mut_ptr() as *mut LocatorHeader;
530
531            (*header).this_address = Unaligned::new(header);
532            (*header).num_items = 2;
533
534            let first_item = (*header).get_first_item();
535            (*first_item).base_address = Unaligned::new(100);
536            (*first_item).size = 50;
537            (*first_item).position = 30;
538
539            let second_item = (*header).get_item(1);
540            (*second_item).base_address = Unaligned::new(200);
541            (*second_item).size = 50;
542            (*second_item).position = 30;
543
544            // Act
545            let result = (*header).get_first_available_item_locked(25, 100, 300);
546
547            // Assert
548            assert!(result.is_none());
549        }
550    }
551
552    #[test]
553    fn get_first_available_item_locked_should_return_null_if_no_available_item_because_no_buffer_fits_range(
554    ) {
555        unsafe {
556            // Arrange
557            let mut header_buf: [u8; LENGTH] = [0; LENGTH];
558            let header: *mut LocatorHeader = header_buf.as_mut_ptr() as *mut LocatorHeader;
559
560            (*header).this_address = Unaligned::new(header);
561            (*header).num_items = 2;
562
563            let first_item = (*header).get_first_item();
564            (*first_item).base_address = Unaligned::new(100);
565            (*first_item).size = 50;
566            (*first_item).position = 0;
567
568            let second_item = (*header).get_item(1);
569            (*second_item).base_address = Unaligned::new(200);
570            (*second_item).size = 50;
571            (*second_item).position = 0;
572
573            // Act
574            let result = (*header).get_first_available_item_locked(25, 0, 100);
575
576            // Assert
577            assert!(result.is_none());
578        }
579    }
580
581    #[test]
582    fn try_allocate_item_should_allocate_item_when_header_is_not_full_and_within_address_limits() {
583        // Arrange
584        let ptr =
585            unsafe { alloc(Layout::from_size_align(LENGTH, align_of::<LocatorHeader>()).unwrap()) };
586        let header_ptr = ptr as *mut LocatorHeader;
587        let header = unsafe { &mut *header_ptr };
588        header.initialize(LENGTH);
589
590        let size = 100;
591        let min_address = get_sys_info().max_address / 2;
592        let max_address = get_sys_info().max_address;
593
594        // Act
595        let item_count = header.num_items;
596        let result = header.try_allocate_item(size, min_address, max_address);
597        assert_eq!(item_count + 1, header.num_items);
598
599        // Assert
600        assert!(result.is_ok());
601        unsafe {
602            let item = result.unwrap_unchecked();
603            let address = (*item.item.get()).base_address.value;
604            assert!(address >= min_address);
605            assert!(address <= max_address);
606        }
607    }
608
609    #[test]
610    fn try_allocate_item_should_not_allocate_item_when_header_is_full() {
611        // Arrange
612        let ptr =
613            unsafe { alloc(Layout::from_size_align(LENGTH, align_of::<LocatorHeader>()).unwrap()) };
614        let header_ptr = ptr as *mut LocatorHeader;
615        let header = unsafe { &mut *header_ptr };
616        header.initialize(LENGTH);
617        header.num_items = MAX_ITEM_COUNT as u8;
618
619        let size = 100;
620        let min_address = get_sys_info().max_address / 2;
621        let max_address = get_sys_info().max_address;
622
623        // Act
624        let item_count = header.num_items;
625        let result = header.try_allocate_item(size, min_address, max_address);
626        assert_eq!(item_count, header.num_items);
627
628        // Assert
629        assert!(result.is_err());
630    }
631
632    #[test]
633    fn try_allocate_item_should_not_allocate_item_when_outside_address_limits() {
634        // Arrange
635        let ptr =
636            unsafe { alloc(Layout::from_size_align(LENGTH, align_of::<LocatorHeader>()).unwrap()) };
637        let header_ptr = ptr as *mut LocatorHeader;
638        let header = unsafe { &mut *header_ptr };
639        header.initialize(LENGTH);
640
641        let size = 100;
642        let min_address = 0;
643        let max_address = 10; // Set maxAddress to a small value to make allocation impossible
644
645        // Act
646        assert!(header
647            .try_allocate_item(size, min_address, max_address)
648            .is_err());
649    }
650
651    #[test]
652    fn get_next_locator_should_allocate_when_newly_created() {
653        // Arrange
654        let ptr =
655            unsafe { alloc(Layout::from_size_align(LENGTH, align_of::<LocatorHeader>()).unwrap()) };
656        let header_ptr = ptr as *mut LocatorHeader;
657        let header = unsafe { &mut *header_ptr };
658        header.initialize(LENGTH);
659        header.num_items = MAX_ITEM_COUNT as u8;
660
661        // Act
662        let next = header.get_next_locator().unwrap();
663        let next_cached = header.get_next_locator().unwrap();
664
665        // Assert
666        assert_eq!(next as usize, next_cached as usize);
667        assert_ne!(next as usize, 0);
668    }
669}