Skip to main content

csskit_arena/
lib.rs

1#![doc = include_str!("../README.md")]
2
3use allocator_api2::alloc::{AllocError, Allocator};
4use std::alloc::Layout;
5use std::cell::Cell;
6use std::ptr::NonNull;
7
8#[cfg(feature = "collections")]
9mod arena_box;
10#[cfg(feature = "collections")]
11mod arena_string;
12#[cfg(feature = "collections")]
13mod arena_vec;
14mod pool;
15#[cfg(feature = "collections")]
16mod raw_vec;
17mod vm;
18
19#[cfg(feature = "collections")]
20pub use arena_box::Box;
21#[cfg(feature = "collections")]
22pub use arena_string::String;
23#[cfg(feature = "collections")]
24pub use arena_vec::{Drain, IntoIter, Vec};
25
26/// Required alignment of the arena region (4 GiB), so that `ptr as u32` equals the byte offset
27/// within the region. Where nothing is reserved or 32-bit targets that already express 4gb, this is `1`.
28#[cfg(all(target_pointer_width = "64", any(unix, windows)))]
29pub const BLOCK_ALIGN: usize = 4 * 1024 * 1024 * 1024;
30#[cfg(not(all(target_pointer_width = "64", any(unix, windows))))]
31pub const BLOCK_ALIGN: usize = 1;
32
33/// Maximum usable size of the arena region (just under 2 GiB), so that no allocation crosses a
34/// 4 GiB boundary and every offset fits in a `u32`.
35pub const MAX_BLOCK_SIZE: usize = 2 * 1024 * 1024 * 1024 - 16;
36
37/// Smallest step the arena takes when it needs more room: bytes committed the first time a Windows arena is written
38/// to, and the size of the first chunk where nothing can be reserved. Growth doubles from there.
39const INITIAL_COMMIT: usize = 64 * 1024;
40
41/// Alignment of a chunk from the global allocator: enough for any fundamental type, and raised to the requested
42/// alignment when a single allocation needs more.
43const CHUNK_ALIGN: usize = 16;
44
45/// How a chunk's memory was obtained, which determines what (if anything) happens on drop.
46#[derive(Clone, Copy)]
47enum Backing {
48	/// Address space reserved by this crate. `reservation` and `reserved` identify the whole reservation.
49	Reservation { reservation: NonNull<u8>, reserved: usize },
50	/// A block from the global allocator, for targets that cannot reserve address space lazily.
51	Heap(Layout),
52	/// Memory owned by the caller (e.g. a V8 `ArrayBuffer`). Never freed by the arena.
53	Borrowed,
54}
55
56#[derive(Clone, Copy)]
57struct Region {
58	base: NonNull<u8>,
59	size: usize,
60	backing: Backing,
61}
62
63/// A chunk the arena has bumped past, kept in a chain so every chunk is released when the arena is.
64struct Chunk {
65	region: Region,
66	/// High-water mark of bytes handed out from the chunk, which is how much of it is resident.
67	used: usize,
68	prev: Option<NonNull<Chunk>>,
69}
70
71/// Where the target allows it the region begins at a 4 GiB-aligned address and is at most [`MAX_BLOCK_SIZE`] bytes, so
72/// the low 32 bits of any interior pointer equal its byte offset within the region. Allocation is a pointer bump;
73/// deallocation of individual items is a no-op (every chunk is freed at once on drop; or never if using borrowed
74/// memory).
75pub struct Arena {
76	/// Current region: base, size, and backing.
77	region: Cell<Region>,
78	/// Bump cursor: byte offset of the next free position within the chunk.
79	cursor: Cell<usize>,
80	/// High-water mark of [`Arena::cursor`] for this chunk. A reset rewinds the cursor but not the pages behind it, so
81	/// this and not the cursor is how much of the chunk is resident.
82	resident: Cell<usize>,
83	/// Bytes of the chunk backed by physical memory. Windows has no lazy commit, so the arena commits as it bumps.
84	#[cfg(windows)]
85	committed: Cell<usize>,
86	/// Chunks the arena has bumped past. `None` unless it ran out of room and had to add one.
87	prev: Cell<Option<NonNull<Chunk>>>,
88	/// Bytes handed out from, and total usable size of, the chunks in `prev`.
89	prev_used: Cell<usize>,
90	prev_size: Cell<usize>,
91}
92
93impl Arena {
94	/// Create a self-allocated arena.
95	///
96	/// Where address space can be reserved lazily the first chunk is the full [`MAX_BLOCK_SIZE`] region, costing no
97	/// physical memory until written to, so one non-relocating chunk serves arbitrarily large parses. Elsewhere every
98	/// byte of a chunk is paid for up front, so the arena starts small and adds chunks as it fills.
99	#[inline]
100	pub fn new() -> Self {
101		let size = if vm::RESERVABLE { MAX_BLOCK_SIZE } else { INITIAL_COMMIT };
102		let region = Self::new_chunk(size).expect("arena backing reservation failed");
103		Self::from_region(region)
104	}
105
106	/// Create a self-allocated arena from a capacity hint.
107	///
108	/// On targets with virtual memory the hint is rounded up to a reusable size class. Elsewhere it is used exactly.
109	/// The arena may add chunks when the initial region fills.
110	///
111	/// # Panics
112	/// Panics if the backing allocation fails.
113	pub fn with_capacity(size: usize) -> Self {
114		let requested = size.clamp(1, MAX_BLOCK_SIZE);
115		let region = Self::new_chunk(requested).expect("arena backing reservation failed");
116		Self::from_region(region)
117	}
118
119	/// Take `size` usable bytes for a chunk: reserved address space where the target has it, a block from the global
120	/// allocator otherwise.
121	fn new_chunk(size: usize) -> Option<Region> {
122		let size = size.clamp(1, MAX_BLOCK_SIZE);
123		let reservation_size =
124			if vm::RESERVABLE { size.max(INITIAL_COMMIT).next_power_of_two().min(MAX_BLOCK_SIZE) } else { size };
125		pool::take(reservation_size)
126			.or_else(|| vm::reserve(reservation_size))
127			.map(|reservation| Region {
128				base: reservation.base,
129				size: reservation.size,
130				backing: Backing::Reservation { reservation: reservation.reservation, reserved: reservation.reserved },
131			})
132			.or_else(|| Self::heap_chunk(size, CHUNK_ALIGN))
133	}
134
135	/// Take `size` bytes from the global allocator for a chunk, aligned to at least `align`.
136	///
137	/// `None` for a zero-sized or otherwise unrepresentable request: a chunk with no room is no use to the arena, and
138	/// the global allocator may not be asked for zero bytes.
139	fn heap_chunk(size: usize, align: usize) -> Option<Region> {
140		let layout = Layout::from_size_align(size, align).ok().filter(|layout| layout.size() > 0)?;
141		// SAFETY: the layout is not zero sized.
142		let base = NonNull::new(unsafe { std::alloc::alloc(layout) })?;
143		Some(Region { base, size: layout.size(), backing: Backing::Heap(layout) })
144	}
145
146	fn from_region(region: Region) -> Self {
147		Self {
148			region: Cell::new(region),
149			cursor: Cell::new(0),
150			resident: Cell::new(0),
151			#[cfg(windows)]
152			committed: Cell::new(if matches!(region.backing, Backing::Reservation { .. }) { 0 } else { region.size }),
153			prev: Cell::new(None),
154			prev_used: Cell::new(0),
155			prev_size: Cell::new(0),
156		}
157	}
158
159	/// Create an arena over caller-owned memory.
160	///
161	/// Intended for bindings where the original caller owns the memory, e.g. NAPI JS where `ArrayBuffer` is owned and
162	/// already allocated.
163	///
164	/// # Safety
165	/// `ptr` must be the base of a live, writable region of at least `size` bytes that outlives the arena, aligned to
166	/// [`BLOCK_ALIGN`] and no larger than [`MAX_BLOCK_SIZE`], and it must not be handed to another allocator.
167	pub unsafe fn from_raw_parts(ptr: NonNull<u8>, size: usize) -> Self {
168		debug_assert!((ptr.as_ptr() as usize).is_multiple_of(BLOCK_ALIGN), "borrowed arena base must be 4 GiB aligned");
169		debug_assert!(size <= MAX_BLOCK_SIZE, "borrowed arena must not exceed MAX_BLOCK_SIZE");
170		Self::from_region(Region { base: ptr, size, backing: Backing::Borrowed })
171	}
172	/// The base address of the current usable region.
173	#[inline]
174	pub fn base_ptr(&self) -> NonNull<u8> {
175		self.region.get().base
176	}
177
178	/// Whether every allocation lives in one region starting at [`Arena::base_ptr`], and so whether the low 32 bits of
179	/// every pointer handed out is its offset into that region.
180	///
181	/// - Over a reserved region this is [`Arena::base_ptr`], until the arena has to add a chunk: there is then no
182	///   single region left to be an offset into, and this becomes `None`.
183	/// - Where pointers are 32 bits wide they already are their own offsets, so the base is `0` however many chunks
184	///   the arena holds: the buffer is the whole address space, which on wasm32 is the linear memory the binding
185	///   layer already has.
186	/// - On a 64 bit target with nothing to reserve - wasm64 under `memory64`, whose linear memory may exceed 4 GiB -
187	///   a pointer's upper half is unconstrained, so its low 32 bits mean nothing and this is always `None`.
188	#[inline]
189	pub fn transfer_base(&self) -> Option<usize> {
190		if !cfg!(target_pointer_width = "64") {
191			return Some(0);
192		}
193		// `BLOCK_ALIGN` is 1 without a reservation, so the alignment alone would wave anything through; and a
194		// reservation that fell back to the global allocator is unaligned even though `RESERVABLE` holds.
195		let region = self.region.get();
196		let base = region.base.as_ptr() as usize;
197		(vm::RESERVABLE && self.prev.get().is_none() && base.is_multiple_of(BLOCK_ALIGN)).then_some(base)
198	}
199
200	/// Number of bytes handed out so far.
201	#[inline]
202	pub fn used_bytes(&self) -> usize {
203		self.prev_used.get() + self.cursor.get()
204	}
205	/// Total usable capacity of every chunk in bytes.
206	#[inline]
207	pub fn capacity(&self) -> usize {
208		self.prev_size.get() + self.region.get().size
209	}
210
211	/// Release every allocation at once by rewinding the bump cursor to the start of the first chunk, freeing any chunk
212	/// the arena had to add.
213	///
214	/// Takes `&mut self` so no allocation can outlive the reset. The first chunk's memory is retained.
215	pub fn reset(&mut self) {
216		self.resident.set(self.resident_bytes());
217		self.cursor.set(0);
218		while let Some(node) = self.prev.get() {
219			// SAFETY: every chunk in the chain came from `Box::into_raw`, and `&mut self` proves nothing allocated from
220			// the chunk being dropped is still live.
221			let chunk = *unsafe { std::boxed::Box::from_raw(node.as_ptr()) };
222			// SAFETY: as above.
223			unsafe { release(self.region.get(), self.resident.get()) };
224			self.region.set(chunk.region);
225			self.resident.set(chunk.used);
226			self.prev.set(chunk.prev);
227			// Every chunk the arena added comes from the global allocator, so is backed in full.
228			#[cfg(windows)]
229			self.committed.set(chunk.region.size);
230		}
231		self.prev_used.set(0);
232		self.prev_size.set(0);
233	}
234
235	/// How many bytes of the chunk being bumped from have ever been handed out, and so how much of it is resident.
236	#[inline]
237	fn resident_bytes(&self) -> usize {
238		self.resident.get().max(self.cursor.get())
239	}
240}
241
242/// Give a chunk's memory back to the thread's pool where it is reserved address space and the pool has room, and to the
243/// OS otherwise. `used` is how many bytes of the chunk were handed out.
244///
245/// # Safety
246/// `region` must be exactly what the chunk was built with, and nothing allocated from it may outlive the
247/// call.
248unsafe fn release(region: Region, used: usize) {
249	match region.backing {
250		Backing::Reservation { reservation, reserved } => {
251			let kept = vm::Reservation { reservation, reserved, base: region.base, size: region.size };
252			// SAFETY: the caller guarantees nothing allocated from the chunk is live.
253			if unsafe { pool::give(kept, used) } {
254				return;
255			}
256			// SAFETY: `reservation`/`reserved` are exactly what `vm::reserve` returned.
257			unsafe { vm::release(reservation, reserved) };
258		}
259		// SAFETY: `base`/`layout` are exactly what the global allocator was asked for.
260		Backing::Heap(layout) => unsafe { std::alloc::dealloc(region.base.as_ptr(), layout) },
261		Backing::Borrowed => {}
262	}
263}
264
265/// Bytes to skip from `addr` to reach the next multiple of `align`.
266#[inline]
267fn pad_to(addr: usize, align: usize) -> usize {
268	debug_assert!(align.is_power_of_two(), "Layout alignment is always a power of two");
269	(align - (addr & (align - 1))) & (align - 1)
270}
271
272/// How far to commit when `end` bytes are needed and `committed` are already backed, given a region of `size` bytes.
273///
274/// Growth doubles so that a parse does not pay a syscall per bump, but never overshoots the chunk nor undershoots
275/// what was asked for. Only Windows commits, but the arithmetic is compiled everywhere so it can be tested everywhere.
276#[inline]
277#[cfg(any(test, windows))]
278fn commit_target(end: usize, committed: usize, size: usize) -> usize {
279	end.max(committed.saturating_mul(2)).max(INITIAL_COMMIT).min(size).max(end)
280}
281
282impl Arena {
283	/// Ensure the chunk is backed by physical memory up to `end`, which must be within the chunk.
284	#[cfg(windows)]
285	#[cold]
286	#[inline(never)]
287	fn commit_to(&self, end: usize) -> bool {
288		let region = self.region.get();
289		let committed = self.committed.get();
290		let target = commit_target(end, committed, region.size);
291		// SAFETY: `committed <= target <= size`, so the range lies within the reservation.
292		let ptr = unsafe { NonNull::new_unchecked(region.base.as_ptr().add(committed)) };
293		// SAFETY: ditto.
294		if !unsafe { vm::commit(ptr, target - committed) } {
295			return false;
296		}
297		self.committed.set(target);
298		true
299	}
300	/// Retire the current chunk and bump from a fresh, larger one able to serve `layout`.
301	#[cold]
302	#[inline(never)]
303	fn grow_chunk(&self, layout: Layout) -> Option<NonNull<[u8]>> {
304		let region = self.region.get();
305		if matches!(region.backing, Backing::Borrowed) {
306			return None;
307		}
308		let total = self.capacity();
309		// The arena stays within MAX_BLOCK_SIZE however many chunks it takes, so every offset it hands out still fits
310		// in a u32.
311		let spare = MAX_BLOCK_SIZE.checked_sub(total)?;
312		// Double, so growth costs a logarithmic number of allocations, but never less than the request nor more than
313		// the budget leaves.
314		let size = layout.size().max(total.max(INITIAL_COMMIT).min(spare));
315		if size > spare {
316			return None;
317		}
318		// Aligning the chunk to the request means the allocation fits at its base.
319		let new_region = Self::heap_chunk(size, layout.align().max(CHUNK_ALIGN))?;
320		let retired = Chunk { region, used: self.resident_bytes(), prev: self.prev.get() };
321		self.prev_used.set(self.prev_used.get() + self.cursor.get());
322		self.prev_size.set(total);
323		// SAFETY: `Box::into_raw` never returns null.
324		self.prev
325			.set(Some(unsafe { NonNull::new_unchecked(std::boxed::Box::into_raw(std::boxed::Box::new(retired))) }));
326		self.region.set(new_region);
327		self.cursor.set(0);
328		self.resident.set(0);
329		// A chunk from the global allocator is backed in full.
330		#[cfg(windows)]
331		self.committed.set(new_region.size);
332		// The fresh chunk is big enough and aligned for `layout`, so this cannot recurse again.
333		self.bump(layout)
334	}
335
336	/// Bump-allocate `layout.size()` bytes aligned to `layout.align()`.
337	///
338	/// Returns `None` if the arena is exhausted.
339	#[inline]
340	fn bump(&self, layout: Layout) -> Option<NonNull<[u8]>> {
341		let align = layout.align();
342		let bytes = layout.size();
343		debug_assert!(align.is_power_of_two(), "Layout alignment is always a power of two");
344		let region = self.region.get();
345		let start = self.cursor.get();
346		debug_assert!(start <= region.size, "cursor must never exceed the chunk size");
347		// Round the cursor up to the requested alignment. It is the address that is rounded rather than the offset: a
348		// chunk from the global allocator is only CHUNK_ALIGN aligned, so an aligned offset within it need not be one.
349		let aligned = start + pad_to(region.base.as_ptr() as usize + start, align);
350		let end = aligned.checked_add(bytes)?;
351		if end > region.size {
352			return self.grow_chunk(layout);
353		}
354		#[cfg(windows)]
355		if end > self.committed.get() && !self.commit_to(end) {
356			return None;
357		}
358		debug_assert!(aligned >= start && end >= aligned, "bump must advance the cursor monotonically");
359		self.cursor.set(end);
360		// SAFETY: `aligned + bytes <= size`, so the range is within the chunk.
361		let ptr = unsafe { NonNull::new_unchecked(region.base.as_ptr().add(aligned)) };
362		debug_assert_eq!(ptr.as_ptr() as usize % align, 0, "returned pointer must satisfy the requested alignment");
363		Some(NonNull::slice_from_raw_parts(ptr, bytes))
364	}
365
366	/// A zero-length allocation at the cursor.
367	///
368	/// Nothing is written, so the cursor does not move and the pages need not be committed, but the pointer is still an
369	/// interior pointer of the chunk: handing out an out-of-region dangling pointer would break raw transfer, whose
370	/// offsets are the low 32 bits of every pointer.
371	#[inline]
372	fn empty(&self, align: usize) -> Option<NonNull<[u8]>> {
373		let region = self.region.get();
374		let start = self.cursor.get();
375		let aligned = start + pad_to(region.base.as_ptr() as usize + start, align);
376		if aligned > region.size {
377			return None;
378		}
379		// SAFETY: `aligned <= size`, so the address is within the chunk.
380		let ptr = unsafe { NonNull::new_unchecked(region.base.as_ptr().add(aligned)) };
381		Some(NonNull::slice_from_raw_parts(ptr, 0))
382	}
383}
384
385impl std::fmt::Debug for Arena {
386	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
387		f.debug_struct("Arena").field("capacity", &self.capacity()).field("used", &self.used_bytes()).finish()
388	}
389}
390
391impl Default for Arena {
392	#[inline]
393	fn default() -> Self {
394		Self::new()
395	}
396}
397
398impl Drop for Arena {
399	fn drop(&mut self) {
400		// SAFETY: `&mut self` proves every allocation is dead.
401		unsafe { release(self.region.get(), self.resident_bytes()) };
402		let mut node = self.prev.get();
403		while let Some(chunk) = node {
404			// SAFETY: every chunk in the chain came from `Box::into_raw` and has not been freed.
405			let chunk = *unsafe { std::boxed::Box::from_raw(chunk.as_ptr()) };
406			node = chunk.prev;
407			// SAFETY: as above; every allocation from the chunk is dead.
408			unsafe { release(chunk.region, chunk.used) };
409		}
410	}
411}
412
413unsafe impl Allocator for &Arena {
414	#[inline]
415	fn allocate(&self, layout: Layout) -> Result<NonNull<[u8]>, AllocError> {
416		if layout.size() == 0 {
417			return self.empty(layout.align()).ok_or(AllocError);
418		}
419		self.bump(layout).ok_or(AllocError)
420	}
421
422	#[inline]
423	unsafe fn deallocate(&self, _ptr: NonNull<u8>, _layout: Layout) {
424		// Bump allocator: individual deallocation is a no-op.
425	}
426
427	#[inline]
428	unsafe fn grow(
429		&self,
430		ptr: NonNull<u8>,
431		old_layout: Layout,
432		new_layout: Layout,
433	) -> Result<NonNull<[u8]>, AllocError> {
434		if new_layout.size() == 0 {
435			// Zero-sized: `ptr` holds nothing to copy, so hand back a fresh empty allocation.
436			return self.empty(new_layout.align()).ok_or(AllocError);
437		}
438		if old_layout.size() == 0 {
439			// `ptr` is the empty allocation handed out for a zero-sized request; nothing to copy.
440			return self.bump(new_layout).ok_or(AllocError);
441		}
442		debug_assert!(new_layout.size() >= old_layout.size(), "grow must not shrink");
443		// If `ptr` is the most recent allocation, extend it in place by bumping the cursor, avoiding
444		// stranding the old bytes. This is the common case for a `Vec` growing while it is the last
445		// thing allocated. A pointer from a chunk the arena has bumped past falls through to the copy
446		// below: chunks never overlap, so its offset within this one cannot also be the live cursor.
447		let region = self.region.get();
448		let addr = ptr.as_ptr() as usize;
449		let base = region.base.as_ptr() as usize;
450		let offset = addr.wrapping_sub(base);
451		if new_layout.align() <= old_layout.align()
452			&& offset < region.size
453			&& offset + old_layout.size() == self.cursor.get()
454			&& offset + new_layout.size() <= region.size
455		{
456			let end = offset + new_layout.size();
457			#[cfg(windows)]
458			if end > self.committed.get() && !self.commit_to(end) {
459				return Err(AllocError);
460			}
461			self.cursor.set(end);
462			return Ok(NonNull::slice_from_raw_parts(ptr, new_layout.size()));
463		}
464		// Otherwise allocate fresh and copy the old bytes over.
465		let new = self.bump(new_layout).ok_or(AllocError)?;
466		// SAFETY: `ptr` holds `old_layout.size()` initialised bytes; `new` has room for at least that
467		// many; the regions do not overlap (fresh allocation).
468		unsafe {
469			std::ptr::copy_nonoverlapping(ptr.as_ptr(), new.as_ptr() as *mut u8, old_layout.size());
470		}
471		Ok(new)
472	}
473}
474
475#[cfg(test)]
476mod test {
477	use crate::{Arena, BLOCK_ALIGN, INITIAL_COMMIT, MAX_BLOCK_SIZE, commit_target, vm};
478	use allocator_api2::alloc::Allocator;
479	use std::alloc::Layout;
480
481	/// A growing arena over a chunk from the global allocator, which unlike a reservation is only `CHUNK_ALIGN` aligned
482	/// and small enough to outgrow. This is what [`Arena::new`] builds where nothing can be reserved.
483	fn growing(size: usize) -> Arena {
484		let region = Arena::heap_chunk(size, crate::CHUNK_ALIGN).unwrap();
485		Arena::from_region(region)
486	}
487
488	#[test]
489	fn commit_target_covers_the_request_and_doubles() {
490		let size = MAX_BLOCK_SIZE;
491		// Nothing committed yet: take the initial slab, even for a one byte write.
492		assert_eq!(commit_target(1, 0, size), INITIAL_COMMIT);
493		// Doubling, once the initial slab is outgrown.
494		assert_eq!(commit_target(INITIAL_COMMIT + 1, INITIAL_COMMIT, size), INITIAL_COMMIT * 2);
495		// A request larger than double wins over doubling.
496		assert_eq!(commit_target(9 << 20, 1 << 20, size), 9 << 20);
497		// Never past the end of the region, even when doubling would overshoot.
498		assert_eq!(commit_target(size, size - 1, size), size);
499		// A region smaller than the initial slab commits only what it has.
500		assert_eq!(commit_target(64, 0, 128), 128);
501		// ...but the request still wins, so the committed range always covers what was handed out.
502		assert_eq!(commit_target(size, 0, size), size);
503	}
504
505	#[test]
506	fn base_is_4gib_aligned() {
507		if !vm::RESERVABLE {
508			return;
509		}
510		let arena = Arena::new();
511		assert!((arena.base_ptr().as_ptr() as usize).is_multiple_of(BLOCK_ALIGN));
512		let fixed = Arena::with_capacity(4096);
513		assert!((fixed.base_ptr().as_ptr() as usize).is_multiple_of(BLOCK_ALIGN));
514	}
515
516	#[test]
517	fn ptr_low_32_equals_offset() {
518		let arena = Arena::new();
519		let base = arena.base_ptr().as_ptr() as usize;
520		let layout = Layout::array::<u32>(2).unwrap();
521		let alloc = (&arena).allocate(layout).unwrap();
522		let data = alloc.as_ptr() as *mut u8 as usize;
523		let offset = data - base;
524		assert!(offset <= MAX_BLOCK_SIZE);
525		if vm::RESERVABLE {
526			// A 4 GiB aligned base puts the region offset in the low 32 bits of every pointer into it.
527			assert_eq!(arena.transfer_base(), Some(base));
528			assert_eq!(data as u32 as usize, offset, "ptr low 32 bits must equal arena offset");
529		} else if cfg!(target_pointer_width = "64") {
530			// wasm64: linear memory may exceed 4 GiB and nothing constrains a pointer's upper half.
531			assert_eq!(arena.transfer_base(), None);
532		} else {
533			// Nothing to align, and no need: a 32 bit pointer already is the offset a binding layer reads, into the
534			// whole address space (linear memory, on wasm32) rather than the region.
535			assert_eq!(arena.transfer_base(), Some(0));
536			assert_eq!(data as u32 as usize, data);
537		}
538	}
539
540	#[test]
541	fn an_unaligned_region_promises_no_offsets() {
542		// The shape a 64 bit target with nothing to reserve is stuck with, and the one a failed reservation falls back
543		// to: a single chunk, but from the global allocator, so its base has none of the alignment offsets need.
544		let arena = growing(1024);
545		if cfg!(target_pointer_width = "64") {
546			assert_eq!(arena.transfer_base(), None);
547		} else {
548			assert_eq!(arena.transfer_base(), Some(0));
549		}
550	}
551
552	#[test]
553	fn over_aligned_allocation_is_aligned() {
554		// A chunk from the global allocator is only CHUNK_ALIGN aligned, so an allocation wanting more has to be padded
555		// to an absolute boundary, not to an offset within the chunk.
556		let arena = growing(INITIAL_COMMIT);
557		let _ = (&arena).allocate(Layout::from_size_align(1, 1).unwrap()).unwrap();
558		let a = (&arena).allocate(Layout::from_size_align(64, 64).unwrap()).unwrap();
559		assert_eq!(a.as_ptr() as *mut u8 as usize % 64, 0);
560	}
561
562	#[test]
563	fn bump_alignment_and_accounting() {
564		let arena = Arena::new();
565		let base = arena.base_ptr().as_ptr() as usize;
566		// A 1-byte alloc followed by an 8-aligned alloc: the second must be padded to 8-byte alignment.
567		let a = (&arena).allocate(Layout::from_size_align(1, 1).unwrap()).unwrap();
568		assert_eq!(a.as_ptr() as *mut u8 as usize - base, 0);
569		let b = (&arena).allocate(Layout::from_size_align(8, 8).unwrap()).unwrap();
570		assert_eq!((b.as_ptr() as *mut u8 as usize - base) % 8, 0);
571		assert!(arena.used_bytes() >= 16);
572	}
573
574	#[test]
575	fn zero_sized_allocation_points_into_the_region() {
576		let arena = Arena::new();
577		let a = (&arena).allocate(Layout::from_size_align(0, 4).unwrap()).unwrap();
578		assert_eq!(a.len(), 0);
579		// A zero-sized allocation must still be an aligned interior pointer, so its low 32 bits are a valid offset.
580		let offset = a.as_ptr() as *mut u8 as usize - arena.base_ptr().as_ptr() as usize;
581		assert_eq!(offset, 0);
582		assert_eq!(a.as_ptr() as *mut u8 as usize % 4, 0);
583		if vm::RESERVABLE {
584			assert_eq!(a.as_ptr() as *mut u8 as usize as u32 as usize, offset);
585		}
586	}
587
588	#[test]
589	fn growing_a_zero_sized_allocation_stays_empty() {
590		let arena = Arena::new();
591		let zst = Layout::from_size_align(0, 1).unwrap();
592		let a = (&arena).allocate(zst).unwrap();
593		// Growing zero bytes to zero bytes must not touch the cursor nor dereference the empty allocation.
594		let b = unsafe { (&arena).grow(a.cast::<u8>(), zst, zst) }.unwrap();
595		assert_eq!(b.len(), 0);
596		assert_eq!(arena.used_bytes(), 0);
597		// Growing from zero bytes to a real allocation copies nothing and bumps from the region.
598		let c = unsafe { (&arena).grow(b.cast::<u8>(), zst, Layout::from_size_align(32, 8).unwrap()) }.unwrap();
599		assert_eq!(c.len(), 32);
600		assert_eq!(c.as_ptr() as *mut u8 as usize, arena.base_ptr().as_ptr() as usize);
601	}
602
603	#[test]
604	fn from_raw_parts_over_borrowed_memory_never_frees() {
605		let backing = Arena::with_capacity(8192);
606		let backing_cap = backing.capacity();
607		let borrowed = unsafe { Arena::from_raw_parts(backing.base_ptr(), 4096) };
608		let a = (&borrowed).allocate(Layout::from_size_align(256, 1).unwrap()).unwrap();
609		assert_eq!(a.len(), 256);
610		// Borrowed memory is exactly as big as the caller says, so the arena must not grow past it.
611		assert!((&borrowed).allocate(Layout::from_size_align(8192, 1).unwrap()).is_err());
612		drop(borrowed);
613		// `backing` owns the memory, so dropping the borrow left it alone: it still hands out bytes, and they are still
614		// writable. Capacity is at least 8192 (the requested hint).
615		assert_eq!(backing.capacity(), backing_cap, "capacity unchanged after dropping borrow");
616		assert!(backing_cap >= 8192, "capacity is at least the requested hint");
617		let b = (&backing).allocate(Layout::from_size_align(backing_cap, 1).unwrap()).unwrap();
618		// SAFETY: the allocation is live and exclusively owned here.
619		unsafe { b.cast::<u8>().write_bytes(0x44, backing_cap) };
620	}
621
622	#[test]
623	fn tiny_alloc_commits_little_physical_memory() {
624		if !vm::RESERVABLE {
625			return;
626		}
627		let arena = Arena::new();
628		let _ = (&arena).allocate(Layout::new::<u64>()).unwrap();
629		assert!(arena.used_bytes() < 4096, "tiny alloc used {} bytes", arena.used_bytes());
630		assert!(arena.capacity() >= MAX_BLOCK_SIZE - 16);
631	}
632
633	#[test]
634	fn many_live_arenas_reserve_without_exhausting_memory() {
635		// Reserving must not charge real memory (Windows has no overcommit), so a test binary's worth of concurrently
636		// live full-size arenas has to fit.
637		let arenas: Vec<Arena> = (0..64).map(|_| Arena::new()).collect();
638		for arena in &arenas {
639			let a = (&arena).allocate(Layout::from_size_align(64, 8).unwrap()).unwrap();
640			// SAFETY: 64 bytes were just handed out for exclusive use.
641			unsafe { a.cast::<u8>().write_bytes(0xAB, 64) };
642		}
643		assert_eq!(arenas.len(), 64);
644	}
645
646	#[test]
647	fn allocation_spanning_many_pages_is_usable() {
648		let arena = Arena::with_capacity(8 * 1024 * 1024);
649		let layout = Layout::from_size_align(4 * 1024 * 1024, 8).unwrap();
650		let a = (&arena).allocate(layout).unwrap();
651		// SAFETY: the allocation is live and exclusively owned here.
652		unsafe { a.cast::<u8>().write_bytes(0xCD, layout.size()) };
653		// SAFETY: ditto; every byte was just initialised.
654		let bytes = unsafe { a.as_ref() };
655		assert!(bytes.iter().all(|b| *b == 0xCD));
656	}
657
658	#[test]
659	fn borrowed_arena_exhaustion_returns_alloc_error() {
660		let backing = Arena::new();
661		let borrowed = unsafe { Arena::from_raw_parts(backing.base_ptr(), 128) };
662		assert!((&borrowed).allocate(Layout::from_size_align(64, 1).unwrap()).is_ok());
663		assert!((&borrowed).allocate(Layout::from_size_align(128, 1).unwrap()).is_err());
664	}
665
666	#[test]
667	fn with_capacity_is_a_growable_hint() {
668		let arena = Arena::with_capacity(128);
669		let initial = arena.capacity();
670		assert!((&arena).allocate(Layout::from_size_align(initial, 1).unwrap()).is_ok());
671		assert!((&arena).allocate(Layout::from_size_align(1, 1).unwrap()).is_ok());
672		assert!(arena.capacity() > initial, "the arena added a chunk when the initial region filled");
673		assert_eq!(arena.used_bytes(), initial + 1);
674	}
675
676	#[test]
677	fn capacity_hints_use_reusable_size_classes() {
678		if !vm::RESERVABLE {
679			return;
680		}
681		let above_initial = Arena::with_capacity(INITIAL_COMMIT + 1);
682		let next_class = Arena::with_capacity(INITIAL_COMMIT * 2);
683		assert_eq!(above_initial.capacity(), INITIAL_COMMIT * 2);
684		assert_eq!(above_initial.capacity(), next_class.capacity());
685	}
686
687	#[test]
688	fn growing_the_last_allocation_extends_it_in_place() {
689		let arena = Arena::new();
690		let old = Layout::from_size_align(64, 8).unwrap();
691		let a = (&arena).allocate(old).unwrap();
692		let new = Layout::from_size_align(4096, 8).unwrap();
693		let b = unsafe { (&arena).grow(a.cast::<u8>(), old, new) }.unwrap();
694		assert_eq!(a.as_ptr() as *mut u8, b.as_ptr() as *mut u8, "the last allocation grows without moving");
695		assert_eq!(arena.used_bytes(), 4096, "growing in place strands nothing");
696	}
697
698	#[test]
699	fn a_growing_arena_adds_chunks_and_keeps_counting() {
700		let arena = growing(1024);
701		let layout = Layout::from_size_align(256, 8).unwrap();
702		let allocs: Vec<_> = (0..64)
703			.map(|_| {
704				let a = (&arena).allocate(layout).unwrap();
705				// SAFETY: the allocation is live and exclusively owned here.
706				unsafe { a.cast::<u8>().write_bytes(0xEE, layout.size()) };
707				a
708			})
709			.collect();
710		assert!(arena.capacity() > 1024, "the arena outgrew its first chunk");
711		assert_eq!(arena.used_bytes(), 64 * 256, "allocations in retired chunks are still counted");
712		if cfg!(target_pointer_width = "64") {
713			assert_eq!(arena.transfer_base(), None, "more than one chunk leaves nothing for offsets to be relative to");
714		} else {
715			assert_eq!(arena.transfer_base(), Some(0), "a 32 bit pointer is its own offset whatever the chunking");
716		}
717		// Every allocation from every chunk is still live and holds what was written to it.
718		for a in &allocs {
719			// SAFETY: each allocation is live and every byte was initialised above.
720			assert!(unsafe { a.as_ref() }.iter().all(|b| *b == 0xEE));
721		}
722	}
723
724	#[test]
725	fn a_growing_arena_serves_an_allocation_larger_than_a_chunk() {
726		let arena = growing(1024);
727		let layout = Layout::from_size_align(3 * 1024 * 1024, 8).unwrap();
728		let a = (&arena).allocate(layout).unwrap();
729		// SAFETY: the allocation is live and exclusively owned here.
730		unsafe { a.cast::<u8>().write_bytes(0x11, layout.size()) };
731		assert!(arena.capacity() >= layout.size());
732	}
733
734	#[test]
735	fn growth_stops_at_the_offset_budget() {
736		let arena = growing(1024);
737		// A request that cannot fit within MAX_BLOCK_SIZE has to fail rather than hand out an offset too big for a u32.
738		assert!((&arena).allocate(Layout::from_size_align(MAX_BLOCK_SIZE, 8).unwrap()).is_err());
739		// The arena is untouched and still usable.
740		assert!((&arena).allocate(Layout::from_size_align(64, 8).unwrap()).is_ok());
741	}
742
743	#[test]
744	fn reset_rewinds_to_the_first_chunk() {
745		let mut arena = growing(1024);
746		let layout = Layout::from_size_align(256, 8).unwrap();
747		for _ in 0..64 {
748			let _ = (&arena).allocate(layout).unwrap();
749		}
750		assert!(arena.capacity() > 1024, "the arena outgrew its first chunk");
751		arena.reset();
752		assert_eq!(arena.used_bytes(), 0);
753		assert_eq!(arena.capacity(), 1024, "chunks added since construction are freed");
754		// Still usable, from the top of the first chunk.
755		let a = (&arena).allocate(layout).unwrap();
756		assert_eq!(a.as_ptr() as *mut u8, arena.base_ptr().as_ptr());
757	}
758
759	#[test]
760	fn reset_reuses_the_backing_region() {
761		let mut arena = Arena::with_capacity(2 * 1024 * 1024);
762		let base = arena.base_ptr();
763		let layout = Layout::from_size_align(1 << 20, 8).unwrap();
764		let a = (&arena).allocate(layout).unwrap();
765		// SAFETY: the allocation is live and exclusively owned here.
766		unsafe { a.cast::<u8>().write_bytes(0x22, layout.size()) };
767		arena.reset();
768		assert_eq!(arena.used_bytes(), 0);
769		assert_eq!(arena.base_ptr(), base, "reset keeps the backing region");
770		// The bytes are handed out again, and are still writable: a committed page stays committed.
771		let b = (&arena).allocate(layout).unwrap();
772		assert_eq!(b.as_ptr() as *mut u8, base.as_ptr());
773		// SAFETY: the allocation is live and exclusively owned here.
774		unsafe { b.cast::<u8>().write_bytes(0x33, layout.size()) };
775	}
776}