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}