Skip to main content

fromsoftware_shared/
game_allocator.rs

1use std::{alloc::Layout, ptr::NonNull};
2
3use thiserror::Error;
4
5/// An error indicating that an allocation failed for any reason, including
6/// memory exhaustion or because the requested layout didn't meet the
7/// allocator's alignment requirements.
8#[derive(Error, Debug)]
9#[error("Allocation failed")]
10pub struct AllocError;
11
12/// A trait for global allocators within a game that are able to deallocate any
13/// memory on the game's main heap. Generally corresponds to the drop logic for
14/// `DLUT::DLAutoDeletePtr` in games from _Bloodborne_ forward.
15///
16/// The API for this generally matches [`std::alloc::Allocator`] where possible.
17/// However, it diverges in certain places that are necessary for compatibility
18/// with the games' memory management.
19///
20/// ### Allocator compatibility
21///
22/// An allocator is *compatible with* this allocator if memory it allocates can
23/// be passed to [`deallocate`] without causing undefined behavior. A given
24/// [`GameAllocator]` must be compatible with itself, but other allocators may
25/// also be compatible. For example, the `GameAllocator` may track global
26/// allocation arenas and determine which global allocator to use to deallocate
27/// a given pointer based on where it appears in those arenas.
28///
29/// ### Currently allocated memory
30///
31/// Some of the methods require that a memory block is *currently allocated* by
32/// an allocator. This means that:
33///
34/// * the starting address for that memory block was previously returned by
35///   [`allocate`] *or* by another allocator that's known to be [*compatible
36///   with*] this one, and
37/// * the memory block has not subsequently been deallocated.
38///
39/// A memory block is deallocated by a call to [`deallocate`].
40///
41/// [`allocate`]: Self::allocate
42/// [*compatible with*]: #allocator-compatibility
43/// [`deallocate`]: Self::deallocate
44///
45/// ### Memory fitting
46///
47/// Some of the methods require that a `layout` *fit* a memory block or vice
48/// versa. This means that the following conditions must hold:
49///
50/// * the memory block must be *currently allocated* with alignment of
51///   [`layout.align()`], and
52///
53/// * [`layout.size()`] must fall in the range `min ..= max`, where:
54///   - `min` is the size of the layout used to allocate the block, and
55///   - `max` is the actual size returned from [`allocate`].
56///
57/// [`layout.align()`]: Layout::align
58/// [`layout.size()`]: Layout::size
59pub trait GameAllocator {
60    /// Attempts to allocate a block of memory.
61    ///
62    /// On success, returns a `NonNull<[u8]>` meeting the size and alignment
63    /// guarantees of `layout`. The returned block may have a larger size than
64    /// specified by `layout.size()`, and may or may not have its contents
65    /// initialized.
66    ///
67    /// The returned block of memory remains valid until it's passed to
68    /// [`deallocate`].
69    ///
70    /// [`deallocate`]: Self::deallocate
71    ///
72    /// **Note:** Unlike [std::alloc::Allocator], this does not take a reference
73    /// to `&self`. This implies that any state involved in allocation must be
74    /// global, thread-safe, and must be valid for the duration of execution.
75    ///
76    /// ## Errors
77    ///
78    /// Unlike [std::alloc::Allocator], this may return an error for reasons
79    /// other than the memory being exhausted or `layout` not meeting the
80    /// allocator's size or alignment constraints.
81    ///
82    /// Clients wishing to abort computation in response to an allocation error
83    /// are encouraged to call the [`handle_alloc_error`] function, rather than
84    /// directly invoking [`panic!`] or similar.
85    ///
86    /// [`handle_alloc_error`]: std::alloc::handle_alloc_error
87    fn allocate(layout: Layout) -> Result<NonNull<[u8]>, AllocError>;
88
89    /// Deallocates the memory referenced by `ptr`.
90    ///
91    /// # Safety
92    ///
93    /// * `ptr` must denote a block of memory [*currently allocated*] via this
94    ///   allocator, and
95    /// * `layout` must [*fit*] that block of memory.
96    ///
97    /// [*currently allocated*]: #currently-allocated-memory
98    /// [*fit*]: #memory-fitting
99    unsafe fn deallocate(ptr: NonNull<u8>, layout: Layout);
100}
101
102/// A [`GameAllocator`] that never actually allocates or drops memory.
103///
104/// [`allocate`] always returns [`AllocError`] and [`deallocate`] always panics.
105///
106/// [`allocate`]: Self::allocate
107/// [`deallocate`]: Self::deallocate
108///
109/// This is intended to by used as the allocator for types such as [`OwnedPtr`]
110/// that represent memory allocated by the game that *don't* use the game's main
111/// heap and so whose deallocation behavior is unknown. It's always incorrect to
112/// try to create or destroy such types in Rust code.
113///
114/// [`OwnedPtr`]: crate::OwnedPtr
115///
116/// Because [`NoOpAllocator::deallocate`] never has undefined behavior for any
117/// memory blocks, all allocators are technically *compatible with* it.
118pub struct NoOpAllocator;
119
120impl GameAllocator for NoOpAllocator {
121    /// Always returns [`AllocError`].
122    fn allocate(_layout: Layout) -> Result<NonNull<[u8]>, AllocError> {
123        Err(AllocError)
124    }
125
126    /// Always panics.
127    unsafe fn deallocate(_ptr: NonNull<u8>, _layout: Layout) {
128        panic!("Can't drop data with NoOpAllocator");
129    }
130}