Skip to main content

fbl/
inline_array.rs

1// Copyright 2026 The Fuchsia Authors
2//
3// Use of this source code is governed by a MIT-style
4// license that can be found in the LICENSE file or at
5// https://opensource.org/licenses/MIT
6
7use core::mem::MaybeUninit;
8use core::ops::{Deref, DerefMut};
9use core::pin::Pin;
10use kalloc::{AllocError, Allocator, Box, DefaultAllocator};
11use pin_init::Init;
12
13/// Runtime-determined, fixed size arrays that are "inlined" (e.g., on the stack) if the size at
14/// most `N` or heap-allocated otherwise.
15///
16/// All allocations are explicit and fallible.
17///
18/// # Note on memory layout This type does not precisely match the C++ `InlineArray` because it
19/// avoids using a self-referential pointer for the inline case to remain address-insensitive
20/// (movable). We can fix this issue to match the C++ layout precisely once we have the `pin_init`
21/// crate available.
22#[repr(C)]
23pub struct InlineArray<T, const N: usize, A: Allocator = DefaultAllocator> {
24    /// The number of elements in the array.  Invariant: If `count <= N`, elements are stored in
25    /// `inline_storage`.  If `count > N`, elements are stored on the heap, pointed to by `ptr`.
26    count: usize,
27
28    /// Pointer to the data.
29    ///
30    /// Invariant: If `count <= N` (inline mode), this pointer is null.  Unlike C++ which points to
31    /// `inline_storage`, we avoid pointing to `inline_storage` to keep the type address-insensitive
32    /// (movable).  If `count > N` (heap mode), this points to the heap-allocated storage.
33    ptr: *mut T,
34
35    /// Inline storage used when `count <= N`.
36    ///
37    /// Invariant: The first `count` elements are initialized when `count <= N`.
38    inline_storage: [MaybeUninit<T>; N],
39
40    /// The allocator used for heap allocations.
41    ///
42    /// Note: This takes space if `A` is not a Zero Sized Type (ZST).
43    allocator: A,
44}
45
46zr::static_assert!(core::mem::size_of::<InlineArray<u32, 4>>() == 32);
47zr::static_assert!(core::mem::align_of::<InlineArray<u32, 4>>() == 8);
48
49impl<T, const N: usize, A: Allocator> InlineArray<T, N, A> {
50    /// Returns an in-place initializer for an `InlineArray`.
51    ///
52    /// This initializes the array directly in its final location (e.g. on the caller's stack via
53    /// `stack_try_pin_init!`), avoiding a temporary copy of the inline storage.
54    pub fn init_in_with<F>(count: usize, allocator: A, mut f: F) -> impl Init<Self, AllocError>
55    where
56        F: FnMut() -> T,
57    {
58        let is_inline = count <= N;
59        // Number of elements that live in `inline_storage`; zero in heap mode.
60        let inline_count = if is_inline { count } else { 0 };
61        // Note: `init!` shadows each field name with an accessor once that field is initialized,
62        // so plain captured values (`count`, `allocator`) are consumed before their fields.
63        pin_init::init!(Self {
64            ptr: if is_inline {
65                core::ptr::null_mut()
66            } else {
67                let mut heap_data = Box::try_new_uninit_slice_in(count, allocator.clone())?;
68                for item in heap_data.iter_mut() {
69                    item.write(f());
70                }
71                // SAFETY: We just initialized all the values.
72                let heap_data = unsafe { heap_data.assume_init() };
73                Box::into_raw_with_allocator(heap_data).0 as *mut T
74            },
75            inline_storage <- pin_init::init_array_from_fn(|i| {
76                if i < inline_count { MaybeUninit::new(f()) } else { MaybeUninit::uninit() }
77            }),
78            allocator,
79            count,
80        }? AllocError)
81    }
82
83    /// Returns an in-place initializer for an `InlineArray`, initializing elements using
84    /// `T::default()`.
85    pub fn init_in(count: usize, allocator: A) -> impl Init<Self, AllocError>
86    where
87        T: Default,
88    {
89        Self::init_in_with(count, allocator, T::default)
90    }
91
92    /// Tries to create a new `InlineArray` of the given length with a specific allocator,
93    /// initializing elements with the provided closure.
94    pub fn try_new_in_with<F>(count: usize, allocator: A, f: F) -> Result<Self, AllocError>
95    where
96        F: FnMut() -> T,
97    {
98        let mut slot = MaybeUninit::<Self>::uninit();
99        let init = Self::init_in_with(count, allocator, f);
100        // SAFETY: `slot` is a valid pointer to uninitialized memory for `Self`, and `init` fully
101        // initializes it when it returns `Ok`.
102        unsafe {
103            init.__init(slot.as_mut_ptr())?;
104            Ok(slot.assume_init())
105        }
106    }
107
108    /// Tries to create a new `InlineArray` of the given length with a specific allocator.
109    /// Elements are initialized using `T::default()`.
110    pub fn try_new_in(count: usize, allocator: A) -> Result<Self, AllocError>
111    where
112        T: Default,
113    {
114        Self::try_new_in_with(count, allocator, T::default)
115    }
116
117    /// Returns true if the array is stored inline.
118    const fn is_inline(&self) -> bool {
119        self.count <= N
120    }
121
122    /// Returns the number of elements in the array.
123    pub const fn len(&self) -> usize {
124        self.count
125    }
126
127    /// Returns true if the array is empty.
128    pub const fn is_empty(&self) -> bool {
129        self.count == 0
130    }
131
132    /// Returns a shared reference to the array as a slice.
133    pub fn as_slice(&self) -> &[T] {
134        if self.is_inline() {
135            // SAFETY: The first `self.count` elements are initialized.
136            unsafe { self.inline_storage[..self.count].assume_init_ref() }
137        } else {
138            // SAFETY: `self.ptr` points to valid heap data of size `self.count`.
139            unsafe { core::slice::from_raw_parts(self.ptr, self.count) }
140        }
141    }
142
143    /// Returns a mutable reference to the array as a slice.
144    pub fn as_mut_slice(&mut self) -> &mut [T] {
145        if self.is_inline() {
146            // SAFETY: The first `self.count` elements are initialized.
147            unsafe { self.inline_storage[..self.count].assume_init_mut() }
148        } else {
149            // SAFETY: `self.ptr` points to valid heap data of size `self.count`.
150            unsafe { core::slice::from_raw_parts_mut(self.ptr, self.count) }
151        }
152    }
153
154    /// Returns a mutable reference to the array elements for a pinned `InlineArray`.
155    pub fn as_mut_slice_pinned(self: Pin<&mut Self>) -> &mut [T] {
156        // SAFETY: Borrowing elements as `&mut [T]` does not move the `InlineArray` or its elements
157        // out of memory.
158        unsafe { self.get_unchecked_mut().as_mut_slice() }
159    }
160}
161
162impl<T, const N: usize> InlineArray<T, N, DefaultAllocator> {
163    /// Returns an in-place initializer for an `InlineArray` using the default allocator.
164    /// Elements are initialized using `T::default()`.
165    pub fn init(count: usize) -> impl Init<Self, AllocError>
166    where
167        T: Default,
168    {
169        Self::init_in(count, DefaultAllocator)
170    }
171
172    /// Returns an in-place initializer for an `InlineArray` using the default allocator,
173    /// initializing elements with the provided closure.
174    pub fn init_with<F>(count: usize, f: F) -> impl Init<Self, AllocError>
175    where
176        F: FnMut() -> T,
177    {
178        Self::init_in_with(count, DefaultAllocator, f)
179    }
180
181    /// Tries to create a new `InlineArray` of the given length using the default allocator.
182    /// Elements are initialized using `T::default()`.
183    pub fn try_new(count: usize) -> Result<Self, AllocError>
184    where
185        T: Default,
186    {
187        Self::try_new_in(count, DefaultAllocator)
188    }
189
190    /// Tries to create a new `InlineArray` of the given length using the default allocator,
191    /// initializing elements with the provided closure.
192    pub fn try_new_with<F>(count: usize, f: F) -> Result<Self, AllocError>
193    where
194        F: FnMut() -> T,
195    {
196        Self::try_new_in_with(count, DefaultAllocator, f)
197    }
198}
199
200impl<T, const N: usize, A: Allocator> Deref for InlineArray<T, N, A> {
201    type Target = [T];
202    fn deref(&self) -> &Self::Target {
203        self.as_slice()
204    }
205}
206
207impl<T, const N: usize, A: Allocator> DerefMut for InlineArray<T, N, A> {
208    fn deref_mut(&mut self) -> &mut Self::Target {
209        self.as_mut_slice()
210    }
211}
212
213impl<T, const N: usize, A: Allocator> Drop for InlineArray<T, N, A> {
214    fn drop(&mut self) {
215        if self.is_inline() {
216            unsafe {
217                self.inline_storage[..self.count].assume_init_drop();
218            }
219        } else {
220            // Reconstruct box and drop it
221            // SAFETY: In non-inline mode, ptr is guaranteed to be valid and non-null because we
222            // only set it on successful allocation in try_new_in.
223            unsafe {
224                let _ = Box::from_raw_in(
225                    core::ptr::slice_from_raw_parts_mut(self.ptr, self.count),
226                    self.allocator.clone(),
227                );
228            }
229        }
230    }
231}
232
233#[cfg(test)]
234mod tests {
235    use super::*;
236    use core::cell::Cell;
237    use core::ptr::NonNull;
238
239    #[derive(Debug, PartialEq, Eq)]
240    struct TestState {
241        live_obj_count: Cell<usize>,
242        ctor_count: Cell<usize>,
243        dtor_count: Cell<usize>,
244        alloc_count: Cell<usize>,
245        fail_threshold: Cell<usize>,
246    }
247
248    impl Default for TestState {
249        fn default() -> Self {
250            Self {
251                live_obj_count: Cell::new(0),
252                ctor_count: Cell::new(0),
253                dtor_count: Cell::new(0),
254                alloc_count: Cell::new(0),
255                fail_threshold: Cell::new(usize::MAX),
256            }
257        }
258    }
259
260    #[derive(Clone)]
261    struct TestAllocator<'a> {
262        state: &'a TestState,
263    }
264
265    impl<'a> Allocator for TestAllocator<'a> {
266        fn allocate(&self, layout: core::alloc::Layout) -> Result<NonNull<[u8]>, AllocError> {
267            let current = self.state.alloc_count.get();
268            self.state.alloc_count.set(current + 1);
269            if current >= self.state.fail_threshold.get() {
270                return Err(AllocError);
271            }
272            DefaultAllocator::default().allocate(layout)
273        }
274
275        unsafe fn deallocate(&self, ptr: NonNull<u8>, layout: core::alloc::Layout) {
276            unsafe { DefaultAllocator::default().deallocate(ptr, layout) }
277        }
278
279        unsafe fn grow(
280            &self,
281            ptr: NonNull<u8>,
282            old_layout: core::alloc::Layout,
283            new_layout: core::alloc::Layout,
284        ) -> Result<NonNull<[u8]>, AllocError> {
285            let current = self.state.alloc_count.get();
286            self.state.alloc_count.set(current + 1);
287            if current >= self.state.fail_threshold.get() {
288                return Err(AllocError);
289            }
290            unsafe { DefaultAllocator::default().grow(ptr, old_layout, new_layout) }
291        }
292
293        unsafe fn shrink(
294            &self,
295            ptr: NonNull<u8>,
296            old_layout: core::alloc::Layout,
297            new_layout: core::alloc::Layout,
298        ) -> Result<NonNull<[u8]>, AllocError> {
299            let current = self.state.alloc_count.get();
300            self.state.alloc_count.set(current + 1);
301            if current >= self.state.fail_threshold.get() {
302                return Err(AllocError);
303            }
304            unsafe { DefaultAllocator::default().shrink(ptr, old_layout, new_layout) }
305        }
306
307        fn allocate_zeroed(
308            &self,
309            layout: core::alloc::Layout,
310        ) -> Result<NonNull<[u8]>, AllocError> {
311            let current = self.state.alloc_count.get();
312            self.state.alloc_count.set(current + 1);
313            if current >= self.state.fail_threshold.get() {
314                return Err(AllocError);
315            }
316            DefaultAllocator::default().allocate_zeroed(layout)
317        }
318    }
319
320    #[derive(Debug)]
321    struct TestObject<'a> {
322        state: &'a TestState,
323    }
324
325    impl<'a> TestObject<'a> {
326        fn new(state: &'a TestState) -> Self {
327            state.live_obj_count.set(state.live_obj_count.get() + 1);
328            state.ctor_count.set(state.ctor_count.get() + 1);
329            TestObject { state }
330        }
331    }
332
333    impl<'a> Drop for TestObject<'a> {
334        fn drop(&mut self) {
335            self.state.live_obj_count.set(self.state.live_obj_count.get() - 1);
336            self.state.dtor_count.set(self.state.dtor_count.get() + 1);
337        }
338    }
339
340    #[test]
341    fn test_inline() {
342        let state = TestState::default();
343
344        for sz in 0..=3 {
345            state.ctor_count.set(0);
346            state.dtor_count.set(0);
347            {
348                let ia =
349                    InlineArray::<TestObject<'_>, 3>::try_new_with(sz, || TestObject::new(&state))
350                        .unwrap();
351                assert_eq!(ia.len(), sz);
352            }
353            assert_eq!(state.ctor_count.get(), sz);
354            assert_eq!(state.dtor_count.get(), sz);
355        }
356    }
357
358    #[test]
359    fn test_non_inline() {
360        let state = TestState::default();
361
362        let test_sizes = [4, 5, 6, 10, 100];
363
364        for &sz in &test_sizes {
365            state.ctor_count.set(0);
366            state.dtor_count.set(0);
367            {
368                let ia =
369                    InlineArray::<TestObject<'_>, 3>::try_new_with(sz, || TestObject::new(&state))
370                        .unwrap();
371                assert_eq!(ia.len(), sz);
372            }
373            assert_eq!(state.ctor_count.get(), sz);
374            assert_eq!(state.dtor_count.get(), sz);
375        }
376    }
377
378    #[test]
379    fn test_allocation_failure() {
380        let state = TestState::default();
381        state.fail_threshold.set(0); // Fail immediately
382
383        // Inline allocation should still work!
384        let ia = InlineArray::<u32, 3, TestAllocator<'_>>::try_new_in(
385            3,
386            TestAllocator { state: &state },
387        );
388        assert!(ia.is_ok());
389
390        // Heap allocation should fail!
391        let ia = InlineArray::<u32, 3, TestAllocator<'_>>::try_new_in(
392            4,
393            TestAllocator { state: &state },
394        );
395        assert!(ia.is_err());
396    }
397
398    #[test]
399    fn test_pinned_init() {
400        let state = TestState::default();
401
402        for sz in 0..=3 {
403            state.ctor_count.set(0);
404            state.dtor_count.set(0);
405            {
406                pin_init::stack_try_pin_init!(
407                    let ia = InlineArray::<TestObject<'_>, 3>::init_with(sz, || {
408                        TestObject::new(&state)
409                    })
410                );
411                let ia = ia.unwrap();
412                assert_eq!(ia.len(), sz);
413            }
414            assert_eq!(state.ctor_count.get(), sz);
415            assert_eq!(state.dtor_count.get(), sz);
416        }
417    }
418}