std/sys/alloc/
wasm.rs

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