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}