rootcause_internals/attachment/raw.rs
1//! Type-erased attachment pointer types.
2//!
3//! This module encapsulates the `ptr` field of [`RawAttachment`] and
4//! [`RawAttachmentRef`], ensuring it is only visible within this module. This
5//! visibility restriction guarantees the safety invariant: **the pointer always
6//! comes from `Box<AttachmentData<A>>`**.
7//!
8//! # Safety Invariant
9//!
10//! Since the `ptr` field can only be set via [`RawAttachment::new`] (which
11//! creates it from `Box::into_raw`), and cannot be modified afterward (no `pub`
12//! or `pub(crate)` fields), the pointer provenance remains valid throughout the
13//! value's lifetime.
14//!
15//! The [`RawAttachment::drop`] implementation relies on this invariant to safely
16//! reconstruct the `Box` and deallocate the memory.
17//!
18//! # Type Erasure
19//!
20//! The concrete type parameter `A` is erased by casting to `AttachmentData<Erased>`.
21//! The vtable stored within the `AttachmentData` provides the runtime type
22//! information needed to safely downcast and format attachments.
23
24use alloc::boxed::Box;
25use core::{any::TypeId, ptr::NonNull};
26
27use crate::{
28 attachment::data::AttachmentData,
29 handlers::{AttachmentFormattingStyle, AttachmentHandler, FormattingFunction},
30 util::{CastTo, Erased},
31};
32
33/// A pointer to an [`AttachmentData`] that is guaranteed to point to an
34/// initialized instance of an [`AttachmentData<A>`] for some specific `A`,
35/// though we do not know which actual `A` it is.
36///
37/// However, the pointer is allowed to transition into a non-initialized state
38/// inside the [`RawAttachment::drop`] method.
39///
40/// The pointer is guaranteed to have been created using [`Box::into_raw`].
41///
42/// We cannot use a [`Box<AttachmentData<A>>`] directly, because that does not
43/// allow us to type-erase the `A`.
44#[repr(transparent)]
45pub struct RawAttachment {
46 /// Pointer to the inner attachment data
47 ptr: NonNull<AttachmentData<Erased>>,
48}
49
50impl RawAttachment {
51 /// Creates a new [`RawAttachment`] with the specified handler and
52 /// attachment.
53 #[inline]
54 pub fn new<A, H>(attachment: A) -> Self
55 where
56 A: 'static,
57 H: AttachmentHandler<A>,
58 {
59 let ptr = Box::new(AttachmentData::new::<H>(attachment));
60 let ptr: *const AttachmentData<A> = Box::into_raw(ptr);
61 let ptr: *mut AttachmentData<Erased> = ptr as _;
62
63 // SAFETY: `Box::into_raw` returns a non-null pointer
64 let ptr: NonNull<AttachmentData<Erased>> = unsafe { NonNull::new_unchecked(ptr) };
65
66 Self { ptr }
67 }
68
69 /// Returns a reference to the [`AttachmentData`] instance.
70 #[inline]
71 pub fn as_ref(&self) -> RawAttachmentRef<'_> {
72 RawAttachmentRef {
73 ptr: self.ptr,
74 _marker: core::marker::PhantomData,
75 }
76 }
77}
78
79impl core::ops::Drop for RawAttachment {
80 #[inline]
81 fn drop(&mut self) {
82 let vtable = self.as_ref().vtable();
83
84 // SAFETY: The vtable drop method has three safety requirements:
85 // - The pointer must come from `Box<AttachmentData<A>>` via `Box::into_raw`
86 // - The `A` type in `AttachmentData<A>` must match the vtable's `A` type
87 // - The pointer must not be used after this call
88 //
89 // These are satisfied because:
90 // - The only way to construct or alter a `RawAttachment` is through the
91 // `RawAttachment::new` method
92 // - The only way to construct or alter an `AttachmentData` is through the
93 // `AttachmentData::new` method
94 // - This is guaranteed by the fact that we are in the `drop()` function
95 unsafe {
96 vtable.drop(self.ptr);
97 }
98 }
99}
100
101/// A lifetime-bound pointer to an [`AttachmentData`] that is guaranteed to
102/// point to an initialized instance of an [`AttachmentData<A>`] for some
103/// specific `A`, though we do not know which actual `A` it is.
104///
105/// We cannot use a [`&'a AttachmentData<A>`] directly, because that would
106/// require us to know the actual type of the attachment, which we do not.
107///
108/// [`&'a AttachmentData<A>`]: AttachmentData
109#[derive(Clone, Copy)]
110#[repr(transparent)]
111pub struct RawAttachmentRef<'a> {
112 /// Pointer to the inner attachment data
113 ptr: NonNull<AttachmentData<Erased>>,
114 /// Marker to tell the compiler that we should
115 /// behave the same as a `&'a AttachmentData<Erased>`
116 _marker: core::marker::PhantomData<&'a AttachmentData<Erased>>,
117}
118
119impl<'a> RawAttachmentRef<'a> {
120 /// Casts the [`RawAttachmentRef`] to an [`AttachmentData<A>`] reference.
121 ///
122 /// # Safety
123 ///
124 /// The caller must ensure that the type `A` matches the actual attachment
125 /// type stored in the [`AttachmentData`].
126 #[inline]
127 pub(super) unsafe fn cast_inner<A: CastTo>(self) -> &'a AttachmentData<A::Target> {
128 // Debug assertion to catch type mismatches in case of bugs
129 debug_assert_eq!(self.vtable().type_id(), TypeId::of::<A>());
130
131 let this = self.ptr.cast::<AttachmentData<A::Target>>();
132 // SAFETY: Our caller guarantees that we point to an AttachmentData<A>.
133 // Converting NonNull to a reference is safe because:
134 // - The pointer is valid and aligned (from Box allocation in RawAttachment::new)
135 // - The data is initialized (Box allocation initializes)
136 // - The lifetime 'a is tied to the RawAttachmentRef<'a>, preventing use-after-free
137 // - Shared access through RawAttachmentRef prevents aliasing violations
138 unsafe { this.as_ref() }
139 }
140
141 /// Returns a [`NonNull`] pointer to the [`AttachmentData`] instance.
142 #[inline]
143 pub(super) fn as_ptr(self) -> *const AttachmentData<Erased> {
144 self.ptr.as_ptr()
145 }
146
147 /// Returns the [`TypeId`] of the attachment.
148 #[inline]
149 pub fn attachment_type_id(self) -> TypeId {
150 self.vtable().type_id()
151 }
152
153 /// Returns the [`TypeId`] of the attachment.
154 #[inline]
155 pub fn attachment_handler_type_id(self) -> TypeId {
156 self.vtable().handler_type_id()
157 }
158
159 /// Checks if the type of the attachment matches the specified type and
160 /// returns a reference to it if it does.
161 #[inline]
162 pub fn attachment_downcast<A: 'static>(self) -> Option<&'a A> {
163 if self.attachment_type_id() == core::any::TypeId::of::<A>() {
164 // SAFETY: We must ensure that the `A` in the AttachmentData matches the `A` we
165 // are using as an argument. However, we have just checked that the
166 // types match, so that is fine.
167 unsafe { Some(self.attachment_downcast_unchecked::<A>()) }
168 } else {
169 None
170 }
171 }
172
173 /// Formats the attachment by using the [`AttachmentHandler::display`]
174 /// method specified by the handler used to create the
175 /// [`AttachmentData`].
176 #[inline]
177 pub fn attachment_display(self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
178 let vtable = self.vtable();
179 // SAFETY: We must ensure that the `A` of the `AttachmentData` matches the `A`
180 // of the `AttachmentVtable`. However, the only way to construct an
181 // `AttachmentData` is through the `AttachmentData::new` method,
182 // which ensures this fact.
183 unsafe { vtable.display(self, formatter) }
184 }
185
186 /// Formats the attachment by using the [`AttachmentHandler::debug`] method
187 /// specified by the handler used to create the [`AttachmentData`].
188 #[inline]
189 pub fn attachment_debug(self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
190 let vtable = self.vtable();
191 // SAFETY: We must ensure that the `A` of the `AttachmentData` matches the `A`
192 // of the `AttachmentVtable`. However, the only way to construct an
193 // `AttachmentData` is through the `AttachmentData::new` method,
194 // which ensures this fact.
195 unsafe { vtable.debug(self, formatter) }
196 }
197
198 /// The formatting style preferred by the attachment when formatted as part
199 /// of a report.
200 ///
201 /// # Arguments
202 ///
203 /// - `report_formatting_function`: Whether the report in which this
204 /// attachment will be embedded is being formatted using [`Display`]
205 /// formatting or [`Debug`]
206 ///
207 /// [`Display`]: core::fmt::Display
208 /// [`Debug`]: core::fmt::Debug
209 #[inline]
210 pub fn preferred_formatting_style(
211 self,
212 report_formatting_function: FormattingFunction,
213 ) -> AttachmentFormattingStyle {
214 let vtable = self.vtable();
215 // SAFETY: We must ensure that the `A` of the `AttachmentData` matches the `A`
216 // of the `AttachmentVtable`. However, the only way to construct an
217 // `AttachmentData` is through the `AttachmentData::new` method,
218 // which ensures this fact.
219 unsafe { vtable.preferred_formatting_style(self, report_formatting_function) }
220 }
221}
222
223#[cfg(test)]
224mod tests {
225 use alloc::string::String;
226
227 use super::*;
228 use crate::handlers::AttachmentHandler;
229
230 struct HandlerI32;
231 impl AttachmentHandler<i32> for HandlerI32 {
232 fn display(value: &i32, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
233 core::fmt::Display::fmt(value, formatter)
234 }
235
236 fn debug(value: &i32, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
237 core::fmt::Debug::fmt(value, formatter)
238 }
239 }
240
241 struct HandlerString;
242 impl AttachmentHandler<String> for HandlerString {
243 fn display(value: &String, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
244 core::fmt::Display::fmt(value, formatter)
245 }
246
247 fn debug(value: &String, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
248 core::fmt::Debug::fmt(value, formatter)
249 }
250 }
251
252 #[test]
253 fn test_raw_attachment_size() {
254 assert_eq!(
255 core::mem::size_of::<RawAttachment>(),
256 core::mem::size_of::<usize>()
257 );
258 assert_eq!(
259 core::mem::size_of::<Option<RawAttachment>>(),
260 core::mem::size_of::<usize>()
261 );
262 assert_eq!(
263 core::mem::size_of::<Result<(), RawAttachment>>(),
264 core::mem::size_of::<usize>()
265 );
266 assert_eq!(
267 core::mem::size_of::<Result<String, RawAttachment>>(),
268 core::mem::size_of::<String>()
269 );
270 assert_eq!(
271 core::mem::size_of::<Option<Option<RawAttachment>>>(),
272 core::mem::size_of::<Option<usize>>()
273 );
274
275 assert_eq!(
276 core::mem::size_of::<RawAttachmentRef<'_>>(),
277 core::mem::size_of::<usize>()
278 );
279 assert_eq!(
280 core::mem::size_of::<Option<RawAttachmentRef<'_>>>(),
281 core::mem::size_of::<usize>()
282 );
283 assert_eq!(
284 core::mem::size_of::<Result<(), RawAttachmentRef<'_>>>(),
285 core::mem::size_of::<usize>()
286 );
287 assert_eq!(
288 core::mem::size_of::<Result<String, RawAttachmentRef<'_>>>(),
289 core::mem::size_of::<String>()
290 );
291 assert_eq!(
292 core::mem::size_of::<Option<Option<RawAttachmentRef<'_>>>>(),
293 core::mem::size_of::<Option<usize>>()
294 );
295 }
296
297 #[test]
298 fn test_raw_attachment_get_refs() {
299 let attachment = RawAttachment::new::<i32, HandlerI32>(100);
300 let attachment_ref = attachment.as_ref();
301
302 // Accessing the pointer multiple times should be safe and consistent
303 let ptr1 = attachment_ref.as_ptr();
304 let ptr2 = attachment_ref.as_ptr();
305 assert_eq!(ptr1, ptr2);
306 }
307
308 #[test]
309 fn test_raw_attachment_downcast() {
310 let int_attachment = RawAttachment::new::<i32, HandlerI32>(42);
311 let string_attachment = RawAttachment::new::<String, HandlerString>(String::from("test"));
312
313 let int_ref = int_attachment.as_ref();
314 let string_ref = string_attachment.as_ref();
315
316 // Are TypeIds what we expect?
317 assert_eq!(int_ref.attachment_type_id(), TypeId::of::<i32>());
318 assert_eq!(string_ref.attachment_type_id(), TypeId::of::<String>());
319
320 // The vtables should be different
321 assert!(!core::ptr::eq(int_ref.vtable(), string_ref.vtable()));
322
323 // Cross-type downcasting should fail safely
324 assert!(int_ref.attachment_downcast::<String>().is_none());
325 assert!(string_ref.attachment_downcast::<i32>().is_none());
326
327 // Correct downcasting should work
328 assert_eq!(int_ref.attachment_downcast::<i32>().unwrap(), &42);
329 assert_eq!(string_ref.attachment_downcast::<String>().unwrap(), "test");
330 }
331
332 #[test]
333 fn test_raw_attachment_display_debug() {
334 use alloc::format;
335
336 let int_attachment = RawAttachment::new::<i32, HandlerI32>(42);
337 let string_attachment = RawAttachment::new::<String, HandlerString>(String::from("test"));
338
339 let int_ref = int_attachment.as_ref();
340 let string_ref = string_attachment.as_ref();
341
342 // Test display formatting
343 let display_int = format!(
344 "{}",
345 TestDisplayFormatter::new(|f| int_ref.attachment_display(f))
346 );
347 let display_string = format!(
348 "{}",
349 TestDisplayFormatter::new(|f| string_ref.attachment_display(f))
350 );
351
352 assert_eq!(display_int, "42");
353 assert_eq!(display_string, "test");
354
355 // Test debug formatting
356 let debug_int = format!(
357 "{}",
358 TestDisplayFormatter::new(|f| int_ref.attachment_debug(f))
359 );
360 let debug_string = format!(
361 "{}",
362 TestDisplayFormatter::new(|f| string_ref.attachment_debug(f))
363 );
364
365 assert_eq!(debug_int, "42");
366 assert_eq!(debug_string, "\"test\"");
367 }
368
369 // Helper struct for testing display/debug formatting
370 struct TestDisplayFormatter<F> {
371 formatter_fn: F,
372 }
373
374 impl<F> TestDisplayFormatter<F>
375 where
376 F: Fn(&mut core::fmt::Formatter<'_>) -> core::fmt::Result,
377 {
378 fn new(formatter_fn: F) -> Self {
379 Self { formatter_fn }
380 }
381 }
382
383 impl<F> core::fmt::Display for TestDisplayFormatter<F>
384 where
385 F: Fn(&mut core::fmt::Formatter<'_>) -> core::fmt::Result,
386 {
387 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
388 (self.formatter_fn)(f)
389 }
390 }
391
392 #[test]
393 fn test_send_sync() {
394 static_assertions::assert_not_impl_any!(RawAttachment: Send, Sync);
395 static_assertions::assert_not_impl_any!(RawAttachmentRef<'_>: Send, Sync);
396 }
397}