Skip to main content

intuicio_data/
lib.rs

1//! Type-erased data primitives that the rest of Intuicio is built on.
2//!
3//! Intuicio moves values around without knowing their Rust types at compile
4//! time, so every value is handled as raw bytes plus a [`type_hash::TypeHash`]
5//! and a drop function. This crate provides the containers that do that:
6//!
7//! - [`data_stack`] - the stack and register storage used to pass values
8//!   between function calls.
9//! - [`lifetime`] - runtime borrow checking for values that outlive Rust's
10//!   static lifetimes.
11//! - [`managed`] - reference counted and garbage collected value boxes.
12//! - [`shared`] - thin `Rc<RefCell>` / `Arc<RwLock>` wrappers.
13//! - [`type_hash`] - cheap runtime type identity.
14//!
15//! # Features
16//!
17//! - `alloc-backtrace` - print a backtrace on every allocation made through
18//!   the `non_zero_*` helpers.
19//! - `typehash_debug_name` - keep the type name inside [`type_hash::TypeHash`]
20//!   for readable diagnostics.
21pub mod data_stack;
22pub mod lifetime;
23pub mod managed;
24pub mod shared;
25pub mod type_hash;
26
27/// Writes a default value through a raw pointer.
28///
29/// Type-erased containers cannot call [`Default::default`], so they call
30/// [`Initialize::initialize_raw`] through a function pointer. Implemented for
31/// every [`Default`] type.
32pub trait Initialize: Sized {
33    /// Returns the initial value of this type.
34    fn initialize() -> Self;
35
36    /// Writes the initial value into already allocated memory.
37    ///
38    /// # Safety
39    ///
40    /// `data` must be non-null, writable and aligned for `Self`, and must not
41    /// hold an initialized value already (the old value is overwritten without
42    /// being dropped).
43    unsafe fn initialize_raw(data: *mut ()) {
44        unsafe { data.cast::<Self>().write(Self::initialize()) };
45    }
46}
47
48impl<T> Initialize for T
49where
50    T: Default,
51{
52    fn initialize() -> Self {
53        Self::default()
54    }
55}
56
57/// Drops a value through a raw pointer.
58///
59/// Type-erased containers keep [`Finalize::finalize_raw`] as a function
60/// pointer and call it when the value goes away. Implemented for every type.
61pub trait Finalize: Sized {
62    /// Drops the value stored at `data` in place.
63    ///
64    /// # Safety
65    ///
66    /// `data` must point at an initialized `Self` that nothing else reads
67    /// afterwards. The value is read out unaligned and dropped, so calling this
68    /// twice on the same pointer is a double free.
69    unsafe fn finalize_raw(data: *mut ()) {
70        unsafe { data.cast::<Self>().read_unaligned() };
71    }
72}
73
74impl<T> Finalize for T {}
75
76/// Drops a value whose type has no Rust counterpart.
77///
78/// A Rust type drops itself through [`Finalize::finalize_raw`], which is a
79/// plain function pointer. A runtime type has no such function, because the
80/// type is a list of fields that is known only at runtime. The object that
81/// holds the field list implements this trait instead.
82///
83/// `intuicio-core` implements it on `Type`, which drops each field in turn.
84pub trait Destructor: Send + Sync {
85    /// Drops the value at `pointer` in place.
86    ///
87    /// # Safety
88    ///
89    /// Same conditions as [`Finalize::finalize_raw`]: `pointer` must hold an
90    /// initialized value of the described type that nothing reads afterwards,
91    /// and calling this twice on one pointer is a double free.
92    unsafe fn destroy(&self, pointer: *mut ());
93}
94
95/// How an owning box drops the value it holds.
96///
97/// A Rust type gives a function pointer. A runtime type needs its field list,
98/// so it keeps a [`Destructor`] alive for as long as a value of that type
99/// exists.
100///
101/// An `unsafe fn(*mut ())` converts into [`Finalizer::Native`].
102#[derive(Clone)]
103pub enum Finalizer {
104    /// A Rust type's own destructor.
105    Native(unsafe fn(*mut ())),
106    /// A runtime type's field walker, kept alive by the value that needs it.
107    Runtime(std::sync::Arc<dyn Destructor>),
108}
109
110impl Finalizer {
111    /// The finalizer for a Rust type.
112    pub fn of<T: Finalize>() -> Self {
113        Self::Native(T::finalize_raw)
114    }
115
116    /// Drops the value at `pointer` in place.
117    ///
118    /// # Safety
119    ///
120    /// Same conditions as [`Finalize::finalize_raw`].
121    pub unsafe fn finalize(&self, pointer: *mut ()) {
122        match self {
123            Self::Native(function) => unsafe { function(pointer) },
124            Self::Runtime(destructor) => unsafe { destructor.destroy(pointer) },
125        }
126    }
127
128    /// Whether this drops the value through a [`Destructor`] instead of a
129    /// function pointer.
130    pub fn is_runtime(&self) -> bool {
131        matches!(self, Self::Runtime(_))
132    }
133
134    /// The plain function pointer, when there is one.
135    ///
136    /// Returns [`None`] for a runtime type, which has no such function pointer.
137    pub fn as_native(&self) -> Option<unsafe fn(*mut ())> {
138        match self {
139            Self::Native(function) => Some(*function),
140            Self::Runtime(_) => None,
141        }
142    }
143}
144
145impl From<unsafe fn(*mut ())> for Finalizer {
146    fn from(value: unsafe fn(*mut ())) -> Self {
147        Self::Native(value)
148    }
149}
150
151impl From<std::sync::Arc<dyn Destructor>> for Finalizer {
152    fn from(value: std::sync::Arc<dyn Destructor>) -> Self {
153        Self::Runtime(value)
154    }
155}
156
157impl std::fmt::Debug for Finalizer {
158    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
159        match self {
160            Self::Native(_) => f.write_str("Finalizer::Native"),
161            Self::Runtime(_) => f.write_str("Finalizer::Runtime"),
162        }
163    }
164}
165
166/// Returns how many bytes must be skipped after `pointer` to reach an address
167/// aligned to `alignment`.
168///
169/// Returns `0` when the pointer is already aligned.
170#[inline]
171pub fn pointer_alignment_padding(pointer: *const u8, alignment: usize) -> usize {
172    let mut result = (pointer as usize) % alignment;
173    if result > 0 {
174        result = alignment - result;
175    }
176    result
177}
178
179/// [`std::alloc::alloc`] that accepts zero-sized layouts.
180///
181/// A zero-sized layout is bumped to one byte, because the standard allocator
182/// refuses zero-sized allocations. Intuicio stores zero-sized types often.
183///
184/// # Safety
185///
186/// Same as [`std::alloc::alloc`]. The returned pointer must be freed with
187/// [`non_zero_dealloc`] using the same layout.
188pub unsafe fn non_zero_alloc(mut layout: std::alloc::Layout) -> *mut u8 {
189    unsafe {
190        if layout.size() == 0 {
191            layout = std::alloc::Layout::from_size_align_unchecked(1, layout.align());
192        }
193        let result = std::alloc::alloc(layout);
194        #[cfg(feature = "alloc-backtrace")]
195        println!(
196            "* Alloc {:p} ({:?}):\n{}",
197            result,
198            layout,
199            std::backtrace::Backtrace::force_capture()
200        );
201        result
202    }
203}
204
205/// [`std::alloc::dealloc`] counterpart of [`non_zero_alloc`].
206///
207/// # Safety
208///
209/// Same as [`std::alloc::dealloc`]. `layout` must be the layout the pointer
210/// was allocated with, before the zero-size bump is applied.
211pub unsafe fn non_zero_dealloc(ptr: *mut u8, mut layout: std::alloc::Layout) {
212    unsafe {
213        if layout.size() == 0 {
214            layout = std::alloc::Layout::from_size_align_unchecked(1, layout.align());
215        }
216        #[cfg(feature = "alloc-backtrace")]
217        println!(
218            "* Dealloc {:p} ({:?}):\n{}",
219            ptr,
220            layout,
221            std::backtrace::Backtrace::force_capture()
222        );
223        std::alloc::dealloc(ptr, layout);
224    }
225}
226
227/// [`std::alloc::realloc`] counterpart of [`non_zero_alloc`].
228///
229/// # Safety
230///
231/// Same as [`std::alloc::realloc`]. `layout` must be the current layout of
232/// `ptr` and `new_size` must be non-zero.
233pub unsafe fn non_zero_realloc(
234    ptr: *mut u8,
235    mut layout: std::alloc::Layout,
236    new_size: usize,
237) -> *mut u8 {
238    unsafe {
239        if layout.size() == 0 {
240            layout = std::alloc::Layout::from_size_align_unchecked(1, layout.align());
241        }
242        let result = std::alloc::realloc(ptr, layout, new_size);
243        #[cfg(feature = "alloc-backtrace")]
244        println!(
245            "* Realloc {:p} -> {:p} ({:?}):\n{}",
246            ptr,
247            result,
248            layout,
249            std::backtrace::Backtrace::force_capture()
250        );
251        result
252    }
253}
254
255/// [`std::alloc::alloc_zeroed`] that accepts zero-sized layouts.
256///
257/// # Safety
258///
259/// Same as [`std::alloc::alloc_zeroed`]. The returned pointer must be freed
260/// with [`non_zero_dealloc`] using the same layout.
261pub unsafe fn non_zero_alloc_zeroed(mut layout: std::alloc::Layout) -> *mut u8 {
262    unsafe {
263        if layout.size() == 0 {
264            layout = std::alloc::Layout::from_size_align_unchecked(1, layout.align());
265        }
266        let result = std::alloc::alloc_zeroed(layout);
267        #[cfg(feature = "alloc-backtrace")]
268        println!(
269            "* Alloc zeroed {:p} ({:?}):\n{}",
270            result,
271            layout,
272            std::backtrace::Backtrace::force_capture()
273        );
274        result
275    }
276}