@@ -4,8 +4,109 @@ use core::arch::wasm32 as wasm;
44#[ cfg( target_arch = "wasm64" ) ]
55use core:: arch:: wasm64 as wasm;
66use core:: ptr;
7+ use core:: sync:: atomic:: { AtomicU8 , Ordering } ;
78
8- /// System setting for Wasm
9+ extern "C" {
10+ static __heap_base: u8 ;
11+ static __heap_end: u8 ;
12+ }
13+
14+ const PREEXISTING_UNTRIED : u8 = 0 ;
15+ const PREEXISTING_DONATED : u8 = 1 ;
16+ const PREEXISTING_DISABLED : u8 = 2 ;
17+
18+ static PREEXISTING_STATE : AtomicU8 = AtomicU8 :: new ( PREEXISTING_UNTRIED ) ;
19+
20+ fn align_up ( value : usize , alignment : usize ) -> Option < usize > {
21+ value
22+ . checked_add ( alignment - 1 )
23+ . map ( |v| v & !( alignment - 1 ) )
24+ }
25+
26+ fn preexisting_chunk (
27+ size : usize ,
28+ page_size : usize ,
29+ heap_base : usize ,
30+ heap_end : usize ,
31+ current_memory : usize ,
32+ ) -> Option < ( usize , usize ) > {
33+ let start = align_up ( heap_base, page_size) ?;
34+ if start == 0 {
35+ return None ;
36+ }
37+
38+ let end = core:: cmp:: min ( heap_end, current_memory) ;
39+ if end <= start {
40+ return None ;
41+ }
42+
43+ let len = end - start;
44+ if len < size {
45+ return None ;
46+ }
47+
48+ Some ( ( start, len) )
49+ }
50+
51+ fn try_donate_preexisting (
52+ state : & AtomicU8 ,
53+ chunk : Option < ( usize , usize ) > ,
54+ ) -> Option < ( usize , usize ) > {
55+ if state. load ( Ordering :: Relaxed ) != PREEXISTING_UNTRIED {
56+ return None ;
57+ }
58+
59+ match chunk {
60+ Some ( chunk) => {
61+ if state
62+ . compare_exchange (
63+ PREEXISTING_UNTRIED ,
64+ PREEXISTING_DONATED ,
65+ Ordering :: Relaxed ,
66+ Ordering :: Relaxed ,
67+ )
68+ . is_ok ( )
69+ {
70+ Some ( chunk)
71+ } else {
72+ None
73+ }
74+ }
75+ None => {
76+ let _ = state. compare_exchange (
77+ PREEXISTING_UNTRIED ,
78+ PREEXISTING_DISABLED ,
79+ Ordering :: Relaxed ,
80+ Ordering :: Relaxed ,
81+ ) ;
82+ None
83+ }
84+ }
85+ }
86+
87+ fn alloc_via_grow ( size : usize , page_size : usize ) -> ( * mut u8 , usize , u32 ) {
88+ let pages = size. div_ceil ( page_size) ;
89+ let prev = wasm:: memory_grow ( 0 , pages) ;
90+
91+ if prev == usize:: max_value ( ) {
92+ return ( ptr:: null_mut ( ) , 0 , 0 ) ;
93+ }
94+
95+ let prev_page = prev * page_size;
96+ let base_ptr = prev_page as * mut u8 ;
97+ let size = pages * page_size;
98+
99+ if prev_page. wrapping_add ( size) == 0 {
100+ return ( base_ptr, size - 16 , 0 ) ;
101+ }
102+
103+ ( base_ptr, size, 0 )
104+ }
105+
106+ /// System setting for Wasm.
107+ ///
108+ /// This is the default wasm allocator backend and only allocates by growing
109+ /// linear memory with `memory.grow`.
9110pub struct System {
10111 _priv : ( ) ,
11112}
@@ -18,35 +119,73 @@ impl System {
18119
19120unsafe impl Allocator for System {
20121 fn alloc ( & self , size : usize ) -> ( * mut u8 , usize , u32 ) {
21- let pages = size . div_ceil ( self . page_size ( ) ) ;
22- let prev = wasm :: memory_grow ( 0 , pages ) ;
122+ alloc_via_grow ( size , self . page_size ( ) )
123+ }
23124
24- // If the allocation failed, meaning `prev` is -1 or
25- // `usize::max_value()`, then return null.
26- if prev == usize:: max_value ( ) {
27- return ( ptr:: null_mut ( ) , 0 , 0 ) ;
28- }
125+ fn remap ( & self , _ptr : * mut u8 , _oldsize : usize , _newsize : usize , _can_move : bool ) -> * mut u8 {
126+ ptr:: null_mut ( )
127+ }
128+
129+ fn free_part ( & self , _ptr : * mut u8 , _oldsize : usize , _newsize : usize ) -> bool {
130+ false
131+ }
132+
133+ fn free ( & self , _ptr : * mut u8 , _size : usize ) -> bool {
134+ false
135+ }
136+
137+ fn can_release_part ( & self , _flags : u32 ) -> bool {
138+ false
139+ }
140+
141+ fn allocates_zeros ( & self ) -> bool {
142+ true
143+ }
144+
145+ fn page_size ( & self ) -> usize {
146+ 64 * 1024
147+ }
148+ }
149+
150+ /// Opt-in wasm allocator backend that can donate the pre-existing linear
151+ /// memory region to dlmalloc once.
152+ ///
153+ /// This allocator assumes the region between `__heap_base` and `__heap_end`
154+ /// can be exclusively owned by dlmalloc. Only use this if no other runtime or
155+ /// allocator in the module also expects to own that region.
156+ pub struct PreexistingSystem {
157+ _priv : ( ) ,
158+ }
29159
30- let prev_page = prev * self . page_size ( ) ;
31- let base_ptr = prev_page as * mut u8 ;
32- let size = pages * self . page_size ( ) ;
33-
34- // Additionally check to see if we just allocated the final bit of the
35- // address space. In such a situation it's not valid in Rust for a
36- // pointer to actually wrap around to from the top of the address space
37- // to 0, so it's not valid to allocate the entire region. Fake the last
38- // few bytes as being un-allocated meaning that the actual size of this
39- // allocation won't be page aligned, which should be handled by
40- // dlmalloc.
41- if prev_page. wrapping_add ( size) == 0 {
42- return ( base_ptr, size - 16 , 0 ) ;
160+ impl PreexistingSystem {
161+ /// Creates a new opt-in preexisting-heap allocator backend.
162+ pub const fn new ( ) -> PreexistingSystem {
163+ PreexistingSystem { _priv : ( ) }
164+ }
165+ }
166+
167+ unsafe impl Allocator for PreexistingSystem {
168+ fn alloc ( & self , size : usize ) -> ( * mut u8 , usize , u32 ) {
169+ let page_size = self . page_size ( ) ;
170+
171+ if size != 0 {
172+ let current_memory = match wasm:: memory_size ( 0 ) . checked_mul ( page_size) {
173+ Some ( v) => v,
174+ None => return ( ptr:: null_mut ( ) , 0 , 0 ) ,
175+ } ;
176+ let heap_base = unsafe { & __heap_base as * const u8 as usize } ;
177+ let heap_end = unsafe { & __heap_end as * const u8 as usize } ;
178+
179+ let chunk = preexisting_chunk ( size, page_size, heap_base, heap_end, current_memory) ;
180+ if let Some ( ( base, len) ) = try_donate_preexisting ( & PREEXISTING_STATE , chunk) {
181+ return ( base as * mut u8 , len, 0 ) ;
182+ }
43183 }
44184
45- ( base_ptr , size, 0 )
185+ alloc_via_grow ( size, page_size )
46186 }
47187
48188 fn remap ( & self , _ptr : * mut u8 , _oldsize : usize , _newsize : usize , _can_move : bool ) -> * mut u8 {
49- // TODO: I think this can be implemented near the end?
50189 ptr:: null_mut ( )
51190 }
52191
@@ -71,6 +210,108 @@ unsafe impl Allocator for System {
71210 }
72211}
73212
213+ #[ cfg( test) ]
214+ mod tests {
215+ use super :: {
216+ align_up, preexisting_chunk, try_donate_preexisting, PREEXISTING_DISABLED ,
217+ PREEXISTING_DONATED , PREEXISTING_UNTRIED ,
218+ } ;
219+ use core:: sync:: atomic:: { AtomicU8 , Ordering } ;
220+
221+ fn legacy_grow_only (
222+ size : usize ,
223+ page_size : usize ,
224+ grow_result : usize ,
225+ ) -> Option < ( usize , usize ) > {
226+ let pages = size. div_ceil ( page_size) ;
227+ if grow_result == usize:: MAX {
228+ return None ;
229+ }
230+ Some ( ( grow_result * page_size, pages * page_size) )
231+ }
232+
233+ #[ test]
234+ fn uses_preexisting_memory_when_growth_fails ( ) {
235+ let page_size = 64 * 1024 ;
236+ let heap_base = page_size;
237+ let heap_end = page_size * 4 ;
238+ let current_memory = page_size * 4 ;
239+
240+ let chunk = preexisting_chunk ( 16 , page_size, heap_base, heap_end, current_memory) . unwrap ( ) ;
241+ let state = AtomicU8 :: new ( PREEXISTING_UNTRIED ) ;
242+ let new_behavior = try_donate_preexisting ( & state, Some ( chunk) ) ;
243+ let legacy_behavior = legacy_grow_only ( 16 , page_size, usize:: MAX ) ;
244+
245+ assert_eq ! ( new_behavior, Some ( ( page_size, page_size * 3 ) ) ) ;
246+ assert_eq ! ( legacy_behavior, None ) ;
247+ assert_eq ! ( state. load( Ordering :: Relaxed ) , PREEXISTING_DONATED ) ;
248+ }
249+
250+ #[ test]
251+ fn uses_heap_end_as_upper_bound ( ) {
252+ let page_size = 64 * 1024 ;
253+ let heap_base = page_size;
254+ let heap_end = page_size * 4 ;
255+ let current_memory = page_size * 8 ;
256+
257+ let chunk = preexisting_chunk ( 16 , page_size, heap_base, heap_end, current_memory) . unwrap ( ) ;
258+ assert_eq ! ( chunk, ( page_size, page_size * 3 ) ) ;
259+ }
260+
261+ #[ test]
262+ fn one_chunk_or_never_disables_after_failure ( ) {
263+ let page_size = 64 * 1024 ;
264+ let heap_base = page_size;
265+ let heap_end = page_size * 2 ;
266+ let current_memory = page_size * 8 ;
267+ let state = AtomicU8 :: new ( PREEXISTING_UNTRIED ) ;
268+
269+ let first = preexisting_chunk (
270+ page_size * 3 ,
271+ page_size,
272+ heap_base,
273+ heap_end,
274+ current_memory,
275+ ) ;
276+ assert_eq ! ( first, None ) ;
277+ assert_eq ! ( try_donate_preexisting( & state, first) , None ) ;
278+ assert_eq ! ( state. load( Ordering :: Relaxed ) , PREEXISTING_DISABLED ) ;
279+
280+ let second = preexisting_chunk ( 16 , page_size, heap_base, heap_end, current_memory) ;
281+ assert_eq ! ( second, Some ( ( page_size, page_size) ) ) ;
282+ assert_eq ! ( try_donate_preexisting( & state, second) , None ) ;
283+ }
284+
285+ #[ test]
286+ fn one_chunk_donates_only_once ( ) {
287+ let state = AtomicU8 :: new ( PREEXISTING_UNTRIED ) ;
288+
289+ assert_eq ! (
290+ try_donate_preexisting( & state, Some ( ( 64 * 1024 , 128 * 1024 ) ) ) ,
291+ Some ( ( 64 * 1024 , 128 * 1024 ) )
292+ ) ;
293+ assert_eq ! ( state. load( Ordering :: Relaxed ) , PREEXISTING_DONATED ) ;
294+ assert_eq ! (
295+ try_donate_preexisting( & state, Some ( ( 64 * 1024 , 64 * 1024 ) ) ) ,
296+ None
297+ ) ;
298+ }
299+
300+ #[ test]
301+ fn rejects_zero_start ( ) {
302+ let page_size = 64 * 1024 ;
303+ let heap_base = 0 ;
304+ let heap_end = page_size * 4 ;
305+ let current_memory = page_size * 8 ;
306+
307+ assert_eq ! (
308+ preexisting_chunk( 16 , page_size, heap_base, heap_end, current_memory) ,
309+ None
310+ ) ;
311+ assert_eq ! ( align_up( heap_base, page_size) , Some ( 0 ) ) ;
312+ }
313+ }
314+
74315#[ cfg( feature = "global" ) ]
75316pub fn acquire_global_lock ( ) {
76317 // single threaded, no need!
0 commit comments