outsource_heap/
lib.rs

1//! # `outsource-heap`
2//! This library provides tools for outsourcing your heap allocations to various places, including:
3//! - A local file
4//! - A network drive
5//! - Anything which implements the `Store` trait, like a `Vec<MaybeUninit<u8>>`
6//!
7//! Generally speaking, using this library is a bad idea.
8//! Once the Rust standard library has a stable allocator API
9//! this library will (hopefully) be made wholly obsolete.
10//!
11//! In the meantime, let's do some cursed nonsense.
12//!
13//! # Getting Started
14//! This crate provides a global allocator, which is used to intercept all
15//! allocations in your program so it can decide where to direct them.
16//! Therefore, the following must be placed somewhere in your project:
17//! ```
18//! #[global_allocator]
19//! static ALLOC: outsource_heap::Global = outsource_heap::Global::system();
20//! ```
21// TODO: put in specific notice that this library should only be used by
22// end binaries, which are expected to control the global allocator
23#![deny(unsafe_op_in_unsafe_fn)]
24use ::core::{ptr::{self, NonNull}, cmp};
25use ::core::cell::Cell;
26use ::std::alloc::{GlobalAlloc, Layout, System};
27use ::std::future::Future;
28use ::std::sync::Mutex;
29use ::std::collections::BTreeMap;
30
31// We're using this inside a Mutex,
32// so the Lazy doesn't need to provide its own synchronization.
33use ::once_cell::unsync::Lazy;
34
35#[cfg(feature = "file-backed")]
36pub mod file_backed;
37
38#[cfg(feature = "bounded")]
39pub mod bounded;
40
41// Note that the Sync requirement is because literally every use of Store
42// provided by this crate needs the Store implementor to be Sync as references
43// to it are essentially sent to and used by every thread.
44// I added this requirement when adding the FileBacked Store, where the linked_list_allocator
45// crate provides a non-Sync allocator.
46/// This trait is a shim for the [`GlobalAlloc`] trait,
47/// defined so that we can implement it for [`std`] defined types like [`Vec`].
48/// See those docs for information on how to implement this trait correctly.
49///
50/// Future versions of this crate may allow for handling allocation failure,
51/// but this is currently not supported.
52pub unsafe trait Store: Sync {
53    unsafe fn alloc(&self, layout: Layout) -> *mut u8;
54    unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout);
55    unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 {
56        let size = layout.size();
57        // SAFETY: the safety contract for `alloc` must be upheld by the caller.
58        let ptr = unsafe { self.alloc(layout) };
59        if !ptr.is_null() {
60            // SAFETY: as allocation succeeded, the region from `ptr`
61            // of size `size` is guaranteed to be valid for writes.
62            unsafe { ptr::write_bytes(ptr, 0, size) };
63        }
64        ptr
65    }
66    unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, new_size: usize) -> *mut u8 {
67        // SAFETY: the caller must ensure that the `new_size` does not overflow.
68        // `layout.align()` comes from a `Layout` and is thus guaranteed to be valid.
69        let new_layout = unsafe { Layout::from_size_align_unchecked(new_size, layout.align()) };
70        // SAFETY: the caller must ensure that `new_layout` is greater than zero.
71        let new_ptr = unsafe { self.alloc(new_layout) };
72        if !new_ptr.is_null() {
73            // SAFETY: the previously allocated block cannot overlap the newly allocated block.
74            // The safety contract for `dealloc` must be upheld by the caller.
75            unsafe {
76                ptr::copy_nonoverlapping(ptr, new_ptr, cmp::min(layout.size(), new_size));
77                self.dealloc(ptr, layout);
78            }
79        }
80        new_ptr
81    }
82}
83
84unsafe impl<T: GlobalAlloc + Sync> Store for T {
85    unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
86        unsafe { GlobalAlloc::alloc(self, layout) }
87    }
88    unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
89        unsafe { GlobalAlloc::dealloc(self, ptr, layout) }
90    }
91    unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 {
92        unsafe { GlobalAlloc::alloc_zeroed(self, layout) }
93    }
94    unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, new_size: usize) -> *mut u8 {
95        unsafe { GlobalAlloc::realloc(self, ptr, layout, new_size) }
96    }
97}
98
99/// Run a closure using a chosen global allocator.
100/// Threads spawned inside this closure will not use the assigned allocator.
101pub fn run<H: Store, T, F: FnOnce() -> T>(store: &'static H, task: F) -> T {
102    let prev = unsafe { stores::register(store) };
103    ::unwind_safe::try_eval(|| {
104        task()
105    }).finally(|_| {
106        unsafe { stores::reregister(prev) };
107    })
108}
109
110/// Run a closure using a chosen global allocator, with a non `'static` lifetime.
111/// # Safety
112/// Ensure that objects allocated with the provided `store`
113/// do not escape the given closure.
114pub unsafe fn run_borrowed<H: Store, T, F: FnOnce() -> T>(store: &H, task: F) -> T {
115    let prev = unsafe { stores::register(store) };
116    ::unwind_safe::try_eval(|| {
117        task()
118    }).finally(|_| {
119        unsafe { stores::reregister(prev) };
120    })
121}
122
123// TODO: this can be implemented by just calling run_async_borrowed, but
124// would require reworking run_async_borrowed's Safety contract
125/// Transform a Future to use a chosen global allocator.
126/// Threads spawned by this Future will not use the assigned allocator.
127pub fn run_async<H: Store, T, F: Future<Output = T>>(store: &'static H, task: F)
128    -> impl Future<Output = T>
129{
130    // Steps:
131    // 1. register the store
132    // 2. deregister the store for each yield
133    // 3. reregister the store for each poll
134    // 4. deregister the store for return
135    use ::core::pin::Pin;
136    use ::core::task::{Context, Poll};
137    ::pin_project_lite::pin_project! {
138        struct Task<S: 'static, F> {
139            store: &'static S,
140            #[pin]
141            task: F,
142        }
143    }
144    impl<S: Store, F: Future> Future for Task<S, F> {
145        type Output = F::Output;
146        fn poll(self: Pin<&mut Self>, ctx: &mut Context<'_>) -> Poll<Self::Output> {
147            let this = self.project();
148            let task = this.task;
149            let store = this.store;
150            run(*store, || task.poll(ctx))
151        }
152    }
153    Task { store, task }
154}
155
156/// Transform a Future to use a chosen global allocator, with a non `'static` lifetime.
157/// # Safety
158/// Ensure that objects allocated with the provided `store`
159/// do not escape the given closure.
160pub unsafe fn run_async_borrowed<H: Store, T, F: Future<Output = T>>(store: &H, task: F) -> impl Future<Output = T> {
161    // Steps:
162    // 1. register the store
163    // 2. deregister the store for each yield
164    // 3. reregister the store for each poll
165    // 4. deregister the store for return
166    use ::core::pin::Pin;
167    use ::core::task::{Context, Poll};
168    ::pin_project_lite::pin_project! {
169        struct Task<F> {
170            store: stores::Hook,
171            #[pin]
172            task: F,
173        }
174    }
175    impl<F: Future> Future for Task<F> {
176        type Output = F::Output;
177        fn poll(self: Pin<&mut Self>, ctx: &mut Context<'_>) -> Poll<Self::Output> {
178            let this = self.project();
179            let task = this.task;
180            let store = this.store;
181            let prev = unsafe { stores::reregister(*store) };
182            ::unwind_safe::try_eval(|| {
183                task.poll(ctx)
184            }).finally(|_| {
185                unsafe { stores::reregister(prev) };
186            })
187        }
188    }
189    let store = stores::to_hook(store);
190    Task { store, task }
191}
192
193/// Perform a task that skips over any scoped hooks
194/// for its allocations.
195fn without_hooks<T, F: FnOnce() -> T>(task: F) -> T {
196    let hook = unsafe { stores::reregister(None) };
197    ::unwind_safe::try_eval(|| {
198        task()
199    }).finally(|_| {
200        unsafe { stores::reregister(hook); }
201    })
202}
203
204// This method is unsafe because it can cause an allocation to
205// be deallocated with the wrong allocator.
206// Primarily meant to be used inside the GlobalAlloc implementation
207// for Global<T>.
208/// Perform a task that skips over any scoped hooks
209/// for its allocations *and* deallocations.
210/// # Safety
211/// Ensure that no deallocations occur which rely on a scoped [`GlobalAlloc`].
212unsafe fn skip_hooks<T, F: FnOnce() -> T>(task: F) -> T {
213    SKIP_HOOKS.with(|cell| cell.replace(true));
214    ::unwind_safe::try_eval(|| {
215        without_hooks(task)
216    }).finally(|_| {
217        SKIP_HOOKS.with(|cell| cell.replace(false));
218    })
219}
220
221mod stores {
222    use ::core::ptr::NonNull;
223    use ::core::cell::Cell;
224    use super::Store;
225    // Lifetime erased trait object,
226    // for use with a store which can't enforce lifetimes.
227    pub(crate) type Hook = Option<NonNull<dyn Store>>;
228    thread_local! {
229        static CURRENT_TARGET: Cell<Hook> = Cell::new(None);
230    }
231    pub(crate) fn to_hook<S: Store>(store: &S) -> Hook {
232        unsafe { Some(NonNull::new_unchecked(store as *const dyn Store as *mut dyn Store)) }
233    }
234    pub(crate) unsafe fn register<S: Store>(store: &S) -> Hook {
235        unsafe { reregister(Some(NonNull::new_unchecked(store as *const dyn Store as *mut dyn Store))) }
236    }
237    pub(crate) unsafe fn reregister(hook: Hook) -> Hook {
238        CURRENT_TARGET.with(|cell| {
239            cell.replace(hook)
240        })
241    }
242
243    // // Note: `deregister` currently only requires a store parameter
244    // // to check an assertion.
245    // pub(crate) unsafe fn deregister<S: Store>(store: &S) {
246    //     // TODO: use sptr for strict provenance annotation-
247    //     // this *does not* require exposing the address.
248    //     let dereg_addr = store as *const dyn Store as *const () as usize;
249    //     let current_addr = CURRENT_TARGET.with(|cell| cell.replace(None).unwrap().as_ptr() as *const () as usize);
250    //     assert_eq!(dereg_addr, current_addr);
251    // }
252
253    pub(crate) fn current() -> Hook {
254        CURRENT_TARGET.with(|cell| cell.get())
255    }
256}
257
258thread_local! {
259    // This is currently only read during GlobalAlloc::dealloc,
260    // since GlobalAlloc::alloc already has something it can
261    // check to decide whether to skip its hooks.
262    // # Safety
263    // Do not write to this outside of skip_hooks.
264    static SKIP_HOOKS: Cell<bool> = Cell::new(false);
265}
266
267/// The top level [`GlobalAlloc`] implementor.
268/// Handles forwarding to various scoped allocators,
269/// and tracking what to deallocate each pointer with.
270pub struct Global<T> {
271    alloc: T,
272    addrs: Mutex<Lazy<BTreeMap<usize, NonNull<dyn Store>>>>,
273}
274// TODO: document why this is correct
275unsafe impl<T> Sync for Global<T> {}
276impl Global<System> {
277    pub const fn system() -> Global<System> {
278        Global {
279            alloc: System,
280            addrs: Mutex::new(Lazy::new(|| BTreeMap::new())),
281        }
282    }
283}
284impl<T> Global<T> {
285    pub const fn new(alloc: T) -> Self {
286        Self { alloc, addrs: Mutex::new(Lazy::new(|| BTreeMap::new())) }
287    }
288}
289unsafe impl<T: GlobalAlloc> GlobalAlloc for Global<T> {
290    // TODO: consider using a macro to merge the declarations of
291    // alloc and alloc_zeroed.
292    // TODO: consider using a macro to merge the declarations of dealloc and realloc
293    // TODO: maybe we can merge all of them into a macro
294    unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
295        match stores::current() {
296            Some(hook) => {
297                // Locking the Mutex may allocate,
298                // and we definitely do not want to
299                // walk down this path inside it, due to
300                // infinite recursion woes.
301                let store = unsafe { hook.as_ref() };
302                // Notably, while GlobalAlloc's contract says you can't
303                // unwind out of it directly, the alloc error hook set by
304                // the Nightly-only set_alloc_error_hook function and called by
305                // handle_alloc_error may end up being allowed to do so.
306                // Therefore, we need to write this like `store.alloc` may panic.
307                // (Do not place it between two things that *must* happen together.)
308                let ptr = unsafe { store.alloc(layout) };
309                unsafe { skip_hooks(|| {
310                    // TODO: remove any and all possibility of this unwinding
311                    let mut addrs = self.addrs.lock().unwrap();
312                    addrs.insert(ptr as usize, hook);
313                }) };
314                ptr
315            },
316            None => unsafe { GlobalAlloc::alloc(&self.alloc, layout) },
317        }
318    }
319    unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
320        if SKIP_HOOKS.with(|c| c.get()) {
321            unsafe { GlobalAlloc::dealloc(&self.alloc, ptr, layout) }
322        } else {
323            let entry = unsafe { skip_hooks(|| {
324                // TODO: remove any and all possibility of this unwinding
325                let mut addrs = self.addrs.lock().unwrap();
326                addrs.remove(&(ptr as usize))
327            }) };
328            match entry {
329                Some(store) => {
330                    let store = unsafe { store.as_ref() };
331                    unsafe { store.dealloc(ptr, layout) }
332                },
333                None => unsafe { GlobalAlloc::dealloc(&self.alloc, ptr, layout) },
334            }
335        }
336    }
337    unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 {
338        match stores::current() {
339            Some(hook) => {
340                let store = unsafe { hook.as_ref() };
341                let ptr = unsafe { store.alloc_zeroed(layout) };
342                unsafe { skip_hooks(|| {
343                    // TODO: remove any and all possibility of this unwinding
344                    let mut addrs = self.addrs.lock().unwrap();
345                    addrs.insert(ptr as usize, hook);
346                }) };
347                ptr
348            },
349            None => unsafe { GlobalAlloc::alloc_zeroed(&self.alloc, layout) },
350        }
351    }
352    unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, new_size: usize) -> *mut u8 {
353        // realloc is a lot like dealloc, as it needs to invoke the same allocator
354        // that the allocation was allocated with
355        if SKIP_HOOKS.with(|c| c.get()) {
356            unsafe { GlobalAlloc::realloc(&self.alloc, ptr, layout, new_size) }
357        } else {
358            let entry = unsafe { skip_hooks(|| {
359                // TODO: remove any and all possibility of this unwinding
360                let mut addrs = self.addrs.lock().unwrap();
361                addrs.remove(&(ptr as usize))
362            }) };
363            match entry {
364                Some(store) => {
365                    let store = unsafe { store.as_ref() };
366                    unsafe { store.realloc(ptr, layout, new_size) }
367                },
368                None => unsafe { GlobalAlloc::realloc(&self.alloc, ptr, layout, new_size) },
369            }
370        }
371    }
372}
373
374#[cfg(test)]
375mod tests {
376    #[test]
377    fn it_works() {
378        let result = 2 + 2;
379        assert_eq!(result, 4);
380    }
381}