]> git.lizzy.rs Git - rust.git/blob - library/std/src/sys/wasm/alloc.rs
Rollup merge of #103104 - SUPERCILEX:sep-ref, r=dtolnay
[rust.git] / library / std / src / sys / wasm / alloc.rs
1 //! This is an implementation of a global allocator on wasm targets when
2 //! emscripten is not in use. In that situation there's no actual runtime for us
3 //! to lean on for allocation, so instead we provide our own!
4 //!
5 //! The wasm instruction set has two instructions for getting the current
6 //! amount of memory and growing the amount of memory. These instructions are the
7 //! foundation on which we're able to build an allocator, so we do so! Note that
8 //! the instructions are also pretty "global" and this is the "global" allocator
9 //! after all!
10 //!
11 //! The current allocator here is the `dlmalloc` crate which we've got included
12 //! in the rust-lang/rust repository as a submodule. The crate is a port of
13 //! dlmalloc.c from C to Rust and is basically just so we can have "pure Rust"
14 //! for now which is currently technically required (can't link with C yet).
15 //!
16 //! The crate itself provides a global allocator which on wasm has no
17 //! synchronization as there are no threads!
18
19 use crate::alloc::{GlobalAlloc, Layout, System};
20
21 static mut DLMALLOC: dlmalloc::Dlmalloc = dlmalloc::Dlmalloc::new();
22
23 #[stable(feature = "alloc_system_type", since = "1.28.0")]
24 unsafe impl GlobalAlloc for System {
25     #[inline]
26     unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
27         // SAFETY: DLMALLOC access is guaranteed to be safe because the lock gives us unique and non-reentrant access.
28         // Calling malloc() is safe because preconditions on this function match the trait method preconditions.
29         let _lock = lock::lock();
30         unsafe { DLMALLOC.malloc(layout.size(), layout.align()) }
31     }
32
33     #[inline]
34     unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 {
35         // SAFETY: DLMALLOC access is guaranteed to be safe because the lock gives us unique and non-reentrant access.
36         // Calling calloc() is safe because preconditions on this function match the trait method preconditions.
37         let _lock = lock::lock();
38         unsafe { DLMALLOC.calloc(layout.size(), layout.align()) }
39     }
40
41     #[inline]
42     unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
43         // SAFETY: DLMALLOC access is guaranteed to be safe because the lock gives us unique and non-reentrant access.
44         // Calling free() is safe because preconditions on this function match the trait method preconditions.
45         let _lock = lock::lock();
46         unsafe { DLMALLOC.free(ptr, layout.size(), layout.align()) }
47     }
48
49     #[inline]
50     unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, new_size: usize) -> *mut u8 {
51         // SAFETY: DLMALLOC access is guaranteed to be safe because the lock gives us unique and non-reentrant access.
52         // Calling realloc() is safe because preconditions on this function match the trait method preconditions.
53         let _lock = lock::lock();
54         unsafe { DLMALLOC.realloc(ptr, layout.size(), layout.align(), new_size) }
55     }
56 }
57
58 #[cfg(target_feature = "atomics")]
59 mod lock {
60     use crate::sync::atomic::{AtomicI32, Ordering::SeqCst};
61
62     static LOCKED: AtomicI32 = AtomicI32::new(0);
63
64     pub struct DropLock;
65
66     pub fn lock() -> DropLock {
67         loop {
68             if LOCKED.swap(1, SeqCst) == 0 {
69                 return DropLock;
70             }
71             // Ok so here's where things get a little depressing. At this point
72             // in time we need to synchronously acquire a lock, but we're
73             // contending with some other thread. Typically we'd execute some
74             // form of `i32.atomic.wait` like so:
75             //
76             //     unsafe {
77             //         let r = core::arch::wasm32::i32_atomic_wait(
78             //             LOCKED.as_mut_ptr(),
79             //             1,  //     expected value
80             //             -1, //     timeout
81             //         );
82             //         debug_assert!(r == 0 || r == 1);
83             //     }
84             //
85             // Unfortunately though in doing so we would cause issues for the
86             // main thread. The main thread in a web browser *cannot ever
87             // block*, no exceptions. This means that the main thread can't
88             // actually execute the `i32.atomic.wait` instruction.
89             //
90             // As a result if we want to work within the context of browsers we
91             // need to figure out some sort of allocation scheme for the main
92             // thread where when there's contention on the global malloc lock we
93             // do... something.
94             //
95             // Possible ideas include:
96             //
97             // 1. Attempt to acquire the global lock. If it fails, fall back to
98             //    memory allocation via `memory.grow`. Later just ... somehow
99             //    ... inject this raw page back into the main allocator as it
100             //    gets sliced up over time. This strategy has the downside of
101             //    forcing allocation of a page to happen whenever the main
102             //    thread contents with other threads, which is unfortunate.
103             //
104             // 2. Maintain a form of "two level" allocator scheme where the main
105             //    thread has its own allocator. Somehow this allocator would
106             //    also be balanced with a global allocator, not only to have
107             //    allocations cross between threads but also to ensure that the
108             //    two allocators stay "balanced" in terms of free'd memory and
109             //    such. This, however, seems significantly complicated.
110             //
111             // Out of a lack of other ideas, the current strategy implemented
112             // here is to simply spin. Typical spin loop algorithms have some
113             // form of "hint" here to the CPU that it's what we're doing to
114             // ensure that the CPU doesn't get too hot, but wasm doesn't have
115             // such an instruction.
116             //
117             // To be clear, spinning here is not a great solution.
118             // Another thread with the lock may take quite a long time to wake
119             // up. For example it could be in `memory.grow` or it could be
120             // evicted from the CPU for a timeslice like 10ms. For these periods
121             // of time our thread will "helpfully" sit here and eat CPU time
122             // until it itself is evicted or the lock holder finishes. This
123             // means we're just burning and wasting CPU time to no one's
124             // benefit.
125             //
126             // Spinning does have the nice properties, though, of being
127             // semantically correct, being fair to all threads for memory
128             // allocation, and being simple enough to implement.
129             //
130             // This will surely (hopefully) be replaced in the future with a
131             // real memory allocator that can handle the restriction of the main
132             // thread.
133             //
134             //
135             // FIXME: We can also possibly add an optimization here to detect
136             // when a thread is the main thread or not and block on all
137             // non-main-thread threads. Currently, however, we have no way
138             // of knowing which wasm thread is on the browser main thread, but
139             // if we could figure out we could at least somewhat mitigate the
140             // cost of this spinning.
141         }
142     }
143
144     impl Drop for DropLock {
145         fn drop(&mut self) {
146             let r = LOCKED.swap(0, SeqCst);
147             debug_assert_eq!(r, 1);
148
149             // Note that due to the above logic we don't actually need to wake
150             // anyone up, but if we did it'd likely look something like this:
151             //
152             //     unsafe {
153             //         core::arch::wasm32::atomic_notify(
154             //             LOCKED.as_mut_ptr(),
155             //             1, //     only one thread
156             //         );
157             //     }
158         }
159     }
160 }
161
162 #[cfg(not(target_feature = "atomics"))]
163 mod lock {
164     #[inline]
165     pub fn lock() {} // no atomics, no threads, that's easy!
166 }