azul_core/refany.rs
1//! Type-erased, reference-counted smart pointer with runtime borrow checking.
2//!
3//! # Safety
4//!
5//! This module provides `RefAny`, a type-erased container similar to `Arc<RefCell<dyn Any>>`,
6//! but designed for FFI compatibility and cross-language interoperability.
7//!
8//! ## Memory Safety Guarantees
9//!
10//! 1. **Proper Alignment**: Fixed in commit addressing Miri UB - memory is allocated with correct
11//! alignment for the stored type using `Layout::from_size_align()`.
12//!
13//! 2. **Atomic Reference Counting**: All reference counts use `AtomicUsize` with `SeqCst` ordering,
14//! ensuring thread-safe access and preventing use-after-free.
15//!
16//! 3. **Runtime Type Safety**: Type IDs are checked before downcasting, preventing invalid pointer
17//! casts that would cause undefined behavior.
18//!
19//! 4. **Runtime Borrow Checking**: Shared and mutable borrows are tracked at runtime, enforcing
20//! Rust's borrowing rules dynamically (similar to `RefCell`).
21//!
22//! ## Thread Safety
23//!
24//! - `RefAny` is `Send`: Can be transferred between threads (data is heap-allocated)
25//! - `RefAny` is `Sync`: Can be shared between threads (atomic operations + `&mut self` for
26//! borrows)
27//!
28//! The `SeqCst` (Sequentially Consistent) memory ordering provides the strongest guarantees:
29//! all atomic operations appear in a single global order visible to all threads, preventing
30//! race conditions where one thread doesn't see another's reference count updates.
31
32use alloc::boxed::Box;
33use alloc::string::String;
34use core::{
35 alloc::Layout,
36 ffi::c_void,
37 fmt,
38 sync::atomic::{AtomicUsize, Ordering as AtomicOrdering},
39};
40
41use azul_css::AzString;
42
43/// C-compatible destructor function type for `RefAny`.
44/// Called when the last reference to a `RefAny` is dropped.
45pub type RefAnyDestructorType = extern "C" fn(*mut c_void);
46
47// NOTE: JSON serialization/deserialization callback types are defined in azul_layout::json
48// The actual types are:
49// RefAnySerializeFnType = extern "C" fn(RefAny) -> Json
50// RefAnyDeserializeFnType = extern "C" fn(Json) -> ResultRefAnyString
51// In azul_core, we only store function pointers as usize (0 = not set).
52
53/// Internal reference counting metadata for `RefAny`.
54///
55/// This struct tracks:
56///
57/// - How many `RefAny` clones exist (`num_copies`)
58/// - How many shared borrows are active (`num_refs`)
59/// - How many mutable borrows are active (`num_mutable_refs`)
60/// - Memory layout information for correct deallocation
61/// - Type information for runtime type checking
62///
63/// # Thread Safety
64///
65/// All counters are `AtomicUsize` with `SeqCst` ordering, making them safe to access
66/// from multiple threads simultaneously. The strong ordering ensures no thread can
67/// observe inconsistent states (e.g., both seeing count=1 during final drop).
68#[derive(Debug)]
69#[repr(C)]
70// `_internal_*` are C-ABI field names exposed in api.json; the `_` prefix is the
71// intentional "internal" convention and cannot be renamed without breaking the ABI.
72#[allow(clippy::pub_underscore_fields)]
73pub struct RefCountInner {
74 /// Type-erased pointer to heap-allocated data.
75 ///
76 /// SAFETY: Must be properly aligned for the stored type (guaranteed by
77 /// `Layout::from_size_align` in `new_c`). Never null for non-ZST types.
78 ///
79 /// This pointer is shared by all `RefAny` clones, so `replace_contents`
80 /// updates are visible to all clones.
81 pub _internal_ptr: *const c_void,
82
83 /// Number of `RefAny` instances sharing the same data.
84 /// When this reaches 0, the data is deallocated.
85 pub num_copies: AtomicUsize,
86
87 /// Number of active shared borrows (`Ref<T>`).
88 /// While > 0, mutable borrows are forbidden.
89 pub num_refs: AtomicUsize,
90
91 /// Number of active mutable borrows (`RefMut<T>`).
92 /// While > 0, all other borrows are forbidden.
93 pub num_mutable_refs: AtomicUsize,
94
95 /// Size of the stored type in bytes (from `size_of::<T>()`).
96 pub _internal_len: usize,
97
98 /// Layout size for deallocation (from `Layout::size()`).
99 pub _internal_layout_size: usize,
100
101 /// Required alignment for the stored type (from `align_of::<T>()`).
102 /// CRITICAL: Must match the alignment used during allocation to prevent UB.
103 pub _internal_layout_align: usize,
104
105 /// Runtime type identifier computed from `TypeId::of::<T>()`.
106 /// Used to prevent invalid downcasts.
107 pub type_id: u64,
108
109 /// Human-readable type name (e.g., "`MyStruct`") for debugging.
110 pub type_name: AzString,
111
112 /// Function pointer to correctly drop the type-erased data.
113 /// SAFETY: Must be called with a pointer to data of the correct type.
114 pub custom_destructor: extern "C" fn(*mut c_void),
115
116 /// Function pointer to serialize `RefAny` to JSON (0 = not set).
117 /// Cast to `RefAnySerializeFnType` (defined in `azul_layout::json`) when called.
118 /// Type: extern "C" fn(RefAny) -> Json
119 pub serialize_fn: usize,
120
121 /// Function pointer to deserialize JSON to new `RefAny` (0 = not set).
122 /// Cast to `RefAnyDeserializeFnType` (defined in `azul_layout::json`) when called.
123 /// Type: extern "C" fn(Json) -> `ResultRefAnyString`
124 pub deserialize_fn: usize,
125
126 /// Function pointer to an on-update observer (0 = not set).
127 /// Cast to `extern "C" fn(*const c_void, usize)` — the (data ptr, byte len)
128 /// of the *pre-mutation* data — and fired from `downcast_mut` BEFORE the
129 /// mutable borrow is handed out. This is the foundation for undo/redo
130 /// snapshots and client/server state sync. Set via `RefAny::set_update_fn`.
131 pub update_fn: usize,
132}
133
134/// Wrapper around a heap-allocated `RefCountInner`.
135///
136/// This is the shared metadata that all `RefAny` clones point to.
137/// The `RefCount` is responsible for all memory management:
138///
139/// - `RefCount::clone()` increments `num_copies` in `RefCountInner`
140/// - `RefCount::drop()` decrements `num_copies` and, if it reaches 0:
141/// 1. Frees the `RefCountInner`
142/// 2. Calls the custom destructor on the data
143/// 3. Deallocates the data memory
144///
145/// # Why `run_destructor: bool`
146///
147/// This flag tracks whether this `RefCount` instance should decrement
148/// `num_copies` when dropped. Set to `true` for all clones (including
149/// those created by `RefAny::clone()` and `AZ_REFLECT` macros).
150/// Set to `false` after the decrement has been performed to prevent
151/// double-decrement.
152#[derive(Hash, PartialEq, PartialOrd, Ord, Eq)]
153#[repr(C)]
154pub struct RefCount {
155 pub ptr: *const RefCountInner,
156 pub run_destructor: bool,
157}
158
159impl fmt::Debug for RefCount {
160 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
161 self.downcast().fmt(f)
162 }
163}
164
165impl Clone for RefCount {
166 /// Clones the `RefCount` and increments the reference count.
167 ///
168 /// # Safety
169 ///
170 /// This is safe because:
171 /// - The ptr is valid (created from `Box::into_raw`)
172 /// - `num_copies` is atomically incremented with `SeqCst` ordering
173 /// - This ensures the `RefCountInner` is not freed while clones exist
174 fn clone(&self) -> Self {
175 // CRITICAL: Must increment num_copies so the RefCountInner is not freed
176 // while this clone exists. The C macros (AZ_REFLECT) use AzRefCount_clone
177 // to create Ref/RefMut guards, and those guards must keep the data alive.
178 if !self.ptr.is_null() {
179 // SAFETY: `ptr` is non-null (checked) and came from `Box::into_raw`
180 // in `RefCount::new`; it stays alive as long as any clone exists
181 // because every clone increments `num_copies` here.
182 unsafe {
183 (*self.ptr).num_copies.fetch_add(1, AtomicOrdering::SeqCst);
184 }
185 }
186 Self {
187 ptr: self.ptr,
188 run_destructor: true,
189 }
190 }
191}
192
193impl Drop for RefCount {
194 /// Decrements the reference count when a `RefCount` clone is dropped.
195 ///
196 /// If this was the last reference (`num_copies` reaches 0), this will also
197 /// free the `RefCountInner` and call the custom destructor.
198 #[allow(clippy::used_underscore_binding)] // `_`-prefixed fields are an intentional FFI/api.json naming convention; internal access is required
199 fn drop(&mut self) {
200 // Only decrement if run_destructor is true (meaning this is a clone)
201 // and the pointer is valid
202 if !self.run_destructor || self.ptr.is_null() {
203 return;
204 }
205 self.run_destructor = false;
206
207 // Take the inner pointer and NULL the field before doing anything
208 // else. The C ABI reaches this drop via `AzRefCount_delete` →
209 // `drop_in_place` on C-owned struct memory, and writes through
210 // `&mut self` persist in that memory. Nulling the pointer here
211 // (mirroring the `ptr = 0` convention the AZ_REFLECT C macros use
212 // for their downcast guards) makes a SECOND delete of the same
213 // struct — easy to hit in C example failure paths, and unguarded
214 // in pre-0.2.1 copies of azul.h — a safe no-op via the null check
215 // above, instead of a double-free of the RefCountInner allocation
216 // or a read through a dangling pointer.
217 let inner = self.ptr;
218 self.ptr = core::ptr::null();
219
220 // Atomically decrement and get the PREVIOUS value. `checked_sub`
221 // refuses to underflow: an unmatched decrement (e.g. a C caller
222 // deleting a byte-copied Ref struct twice) becomes a no-op instead
223 // of wrapping `num_copies` to `usize::MAX` and corrupting the
224 // reference count for the rest of the process.
225 // SAFETY: `inner` is non-null (guarded above) and points to the live
226 // `RefCountInner` from `Box::into_raw`; only the atomic field is touched.
227 let current_copies = unsafe {
228 match (*inner).num_copies.fetch_update(
229 AtomicOrdering::SeqCst,
230 AtomicOrdering::SeqCst,
231 |n| n.checked_sub(1),
232 ) {
233 Ok(prev) => prev,
234 Err(_zero) => return,
235 }
236 };
237
238 // If previous value wasn't 1, other references still exist
239 if current_copies != 1 {
240 return;
241 }
242
243 // We're the last reference! Clean up.
244 // SAFETY: ptr came from Box::into_raw, and we're the last reference
245 let sharing_info = unsafe { Box::from_raw(inner.cast_mut()) };
246 let sharing_info = *sharing_info; // Box deallocates RefCountInner here
247
248 // Get the data pointer
249 let data_ptr = sharing_info._internal_ptr;
250
251 // Handle zero-sized types specially
252 if sharing_info._internal_len == 0
253 || sharing_info._internal_layout_size == 0
254 || data_ptr.is_null()
255 {
256 let mut _dummy: [u8; 0] = [];
257 // Call destructor even for ZSTs (may have side effects)
258 (sharing_info.custom_destructor)(_dummy.as_mut_ptr().cast::<c_void>());
259 } else {
260 // Reconstruct the layout used during allocation. Removed the
261 // `unsafe { Layout::from_size_align_unchecked(..) }`: these size/align
262 // were produced by a valid `Layout` in `new_c` (`layout.size()` /
263 // `layout.align()`), so the safe checked constructor always succeeds
264 // and is behaviorally identical here — no unsafe needed.
265 let layout = Layout::from_size_align(
266 sharing_info._internal_layout_size,
267 sharing_info._internal_layout_align,
268 )
269 .expect("RefCount::drop: stored layout was invalid");
270
271 // Phase 1: Run the custom destructor
272 (sharing_info.custom_destructor)(data_ptr.cast_mut());
273
274 // Phase 2: Deallocate the memory
275 // SAFETY: `data_ptr` was allocated in `new_c` (or `replace_contents`)
276 // with exactly this `layout`, and we are the last reference, so no
277 // other clone can observe the freed block.
278 unsafe {
279 alloc::alloc::dealloc(data_ptr as *mut u8, layout);
280 }
281 }
282 }
283}
284
285/// Debug-friendly snapshot of `RefCountInner` with non-atomic values.
286#[derive(Debug, Clone)]
287pub(crate) struct RefCountInnerDebug {
288 pub(crate) num_copies: usize,
289 pub(crate) num_refs: usize,
290 pub(crate) num_mutable_refs: usize,
291 pub(crate) _internal_len: usize,
292 pub(crate) _internal_layout_size: usize,
293 pub(crate) _internal_layout_align: usize,
294 pub(crate) type_id: u64,
295 pub(crate) type_name: AzString,
296 pub(crate) custom_destructor: usize,
297 /// Serialization function pointer (0 = not set)
298 pub(crate) serialize_fn: usize,
299 /// Deserialization function pointer (0 = not set)
300 pub(crate) deserialize_fn: usize,
301}
302
303impl RefCount {
304 /// Creates a new `RefCount` by boxing the metadata on the heap.
305 ///
306 /// # Safety
307 ///
308 /// Safe because we're creating a new allocation with `Box::new`,
309 /// then immediately leaking it with `into_raw` to get a stable pointer.
310 fn new(ref_count: RefCountInner) -> Self {
311 Self {
312 ptr: Box::into_raw(Box::new(ref_count)),
313 run_destructor: true,
314 }
315 }
316
317 /// Dereferences the raw pointer to access the metadata.
318 ///
319 /// # Safety
320 ///
321 /// Safe because:
322 /// - The pointer is created from `Box::into_raw`, so it's valid and properly aligned
323 /// - The lifetime is tied to `&self`, ensuring the pointer is still alive
324 /// - Reference counting ensures the data isn't freed while references exist
325 fn downcast(&self) -> &RefCountInner {
326 assert!(!self.ptr.is_null(), "[RefCount::downcast] FATAL: self.ptr is null!");
327 // SAFETY: `ptr` is non-null (asserted) and came from `Box::into_raw`; the
328 // returned reference is bounded by `&self`, and refcounting keeps the
329 // `RefCountInner` alive for at least that long.
330 unsafe { &*self.ptr }
331 }
332
333 /// Creates a debug snapshot of the current reference counts.
334 ///
335 /// Loads all atomic values with `SeqCst` ordering to get a consistent view.
336 #[allow(clippy::used_underscore_binding)] // `_`-prefixed fields are an intentional FFI/api.json naming convention; internal access is required
337 pub(crate) fn debug_get_refcount_copied(&self) -> RefCountInnerDebug {
338 let dc = self.downcast();
339 RefCountInnerDebug {
340 num_copies: dc.num_copies.load(AtomicOrdering::SeqCst),
341 num_refs: dc.num_refs.load(AtomicOrdering::SeqCst),
342 num_mutable_refs: dc.num_mutable_refs.load(AtomicOrdering::SeqCst),
343 _internal_len: dc._internal_len,
344 _internal_layout_size: dc._internal_layout_size,
345 _internal_layout_align: dc._internal_layout_align,
346 type_id: dc.type_id,
347 type_name: dc.type_name.clone(),
348 custom_destructor: dc.custom_destructor as usize,
349 serialize_fn: dc.serialize_fn,
350 deserialize_fn: dc.deserialize_fn,
351 }
352 }
353
354 /// Runtime check: can we create a shared borrow?
355 ///
356 /// Returns `true` if there are no active mutable borrows.
357 /// Multiple shared borrows can coexist (like `&T` in Rust).
358 ///
359 /// # Memory Ordering
360 ///
361 /// Uses `SeqCst` to ensure we see the most recent state from all threads.
362 /// If another thread just released a mutable borrow, we'll see it.
363 #[must_use] pub fn can_be_shared(&self) -> bool {
364 self.downcast()
365 .num_mutable_refs
366 .load(AtomicOrdering::SeqCst)
367 == 0
368 }
369
370 /// Runtime check: can we create a mutable borrow?
371 ///
372 /// Returns `true` only if there are ZERO active borrows of any kind.
373 /// This enforces Rust's exclusive mutability rule (like `&mut T`).
374 ///
375 /// # Memory Ordering
376 ///
377 /// Uses `SeqCst` to ensure we see all recent borrows from all threads.
378 /// Both counters must be checked atomically to prevent races.
379 #[must_use] pub fn can_be_shared_mut(&self) -> bool {
380 let info = self.downcast();
381 info.num_mutable_refs.load(AtomicOrdering::SeqCst) == 0
382 && info.num_refs.load(AtomicOrdering::SeqCst) == 0
383 }
384
385 /// Increments the shared borrow counter.
386 ///
387 /// Called when a `Ref<T>` is created. The `Ref::drop` will decrement it.
388 ///
389 /// # Memory Ordering
390 ///
391 /// `SeqCst` ensures this increment is visible to all threads before they
392 /// try to acquire a mutable borrow (which checks this counter).
393 pub fn increase_ref(&self) {
394 self.downcast()
395 .num_refs
396 .fetch_add(1, AtomicOrdering::SeqCst);
397 }
398
399 /// Decrements the shared borrow counter.
400 ///
401 /// Called when a `Ref<T>` is dropped, indicating the borrow is released.
402 ///
403 /// # Underflow guard
404 ///
405 /// Saturates at 0: an unmatched decrement — e.g. a C caller running
406 /// `FooRef_delete` after a FAILED downcast with a pre-0.2.1 copy of
407 /// `azul.h` (whose macro did not skip the decrease), or a plain
408 /// double-delete — must not wrap `num_refs` to `usize::MAX`, which
409 /// would make `can_be_shared_mut()` return `false` for the rest of
410 /// the process (callbacks silently stop mutating state).
411 ///
412 /// # Memory Ordering
413 ///
414 /// `SeqCst` ensures this decrement is immediately visible to other threads
415 /// waiting to acquire a mutable borrow.
416 pub fn decrease_ref(&self) {
417 let _ = self.downcast().num_refs.fetch_update(
418 AtomicOrdering::SeqCst,
419 AtomicOrdering::SeqCst,
420 |n| n.checked_sub(1),
421 );
422 }
423
424 /// Increments the mutable borrow counter.
425 ///
426 /// Called when a `RefMut<T>` is created. Should only succeed when this
427 /// counter and `num_refs` are both 0.
428 ///
429 /// # Memory Ordering
430 ///
431 /// `SeqCst` ensures this increment is visible to all other threads,
432 /// blocking them from acquiring any borrow (shared or mutable).
433 pub fn increase_refmut(&self) {
434 self.downcast()
435 .num_mutable_refs
436 .fetch_add(1, AtomicOrdering::SeqCst);
437 }
438
439 /// Decrements the mutable borrow counter.
440 ///
441 /// Called when a `RefMut<T>` is dropped, releasing exclusive access.
442 ///
443 /// # Underflow guard
444 ///
445 /// Saturates at 0 (see [`Self::decrease_ref`]): a double
446 /// `FooRefMut_delete` from C must not wrap `num_mutable_refs`, which
447 /// would corrupt the runtime borrow checker and let a second thread
448 /// or timer callback obtain an aliasing mutable borrow.
449 ///
450 /// # Memory Ordering
451 ///
452 /// `SeqCst` ensures this decrement is immediately visible, allowing
453 /// other threads to acquire borrows.
454 pub fn decrease_refmut(&self) {
455 let _ = self.downcast().num_mutable_refs.fetch_update(
456 AtomicOrdering::SeqCst,
457 AtomicOrdering::SeqCst,
458 |n| n.checked_sub(1),
459 );
460 }
461}
462
463/// RAII guard for a shared borrow of type `T` from a `RefAny`.
464///
465/// Similar to `std::cell::Ref`, this automatically decrements the borrow
466/// counter when dropped, ensuring borrows are properly released.
467///
468/// # Deref
469///
470/// Implements `Deref<Target = T>` so you can use it like `&T`.
471#[derive(Debug)]
472#[repr(C)]
473pub struct Ref<'a, T> {
474 ptr: &'a T,
475 sharing_info: RefCount,
476}
477
478impl<T> Drop for Ref<'_, T> {
479 /// Automatically releases the shared borrow when the guard goes out of scope.
480 ///
481 /// # Safety
482 ///
483 /// Safe because `decrease_ref` uses atomic operations and is designed to be
484 /// called exactly once per `Ref` instance.
485 fn drop(&mut self) {
486 self.sharing_info.decrease_ref();
487 }
488}
489
490impl<T> core::ops::Deref for Ref<'_, T> {
491 type Target = T;
492
493 fn deref(&self) -> &Self::Target {
494 self.ptr
495 }
496}
497
498/// RAII guard for a mutable borrow of type `T` from a `RefAny`.
499///
500/// Similar to `std::cell::RefMut`, this automatically decrements the mutable
501/// borrow counter when dropped, releasing exclusive access.
502///
503/// # Deref / `DerefMut`
504///
505/// Implements both `Deref` and `DerefMut` so you can use it like `&mut T`.
506#[derive(Debug)]
507#[repr(C)]
508pub struct RefMut<'a, T> {
509 ptr: &'a mut T,
510 sharing_info: RefCount,
511}
512
513impl<T> Drop for RefMut<'_, T> {
514 /// Automatically releases the mutable borrow when the guard goes out of scope.
515 ///
516 /// # Safety
517 ///
518 /// Safe because `decrease_refmut` uses atomic operations and is designed to be
519 /// called exactly once per `RefMut` instance.
520 fn drop(&mut self) {
521 self.sharing_info.decrease_refmut();
522 }
523}
524
525impl<T> core::ops::Deref for RefMut<'_, T> {
526 type Target = T;
527
528 fn deref(&self) -> &Self::Target {
529 &*self.ptr
530 }
531}
532
533impl<T> core::ops::DerefMut for RefMut<'_, T> {
534 fn deref_mut(&mut self) -> &mut Self::Target {
535 self.ptr
536 }
537}
538
539/// Type-erased, reference-counted smart pointer with runtime borrow checking.
540///
541/// `RefAny` is similar to `Arc<RefCell<dyn Any>>`, providing:
542/// - Type erasure (stores any `'static` type)
543/// - Reference counting (clones share the same data)
544/// - Runtime borrow checking (enforces Rust's borrowing rules at runtime)
545/// - FFI compatibility (`#[repr(C)]` and C-compatible API)
546///
547/// # Thread Safety
548///
549/// - `Send`: Can be moved between threads (heap-allocated data, atomic counters)
550/// - `Sync`: Can be shared between threads (`downcast_ref/mut` require `&mut self`)
551///
552/// # Memory Safety
553///
554/// Fixed critical UB bugs in alignment, copy count, and pointer provenance.
555/// All operations are verified with Miri to ensure absence of undefined behavior.
556///
557/// # Usage
558///
559/// ```rust
560/// # use azul_core::refany::RefAny;
561/// let data = RefAny::new(42i32);
562/// let mut data_clone = data.clone(); // shares the same heap allocation
563///
564/// // Runtime-checked downcasting with type safety
565/// if let Some(value_ref) = data_clone.downcast_ref::<i32>() {
566/// assert_eq!(*value_ref, 42);
567/// };
568///
569/// // Runtime-checked mutable borrowing
570/// if let Some(mut value_mut) = data_clone.downcast_mut::<i32>() {
571/// *value_mut = 100;
572/// };
573/// ```
574#[derive(Debug)]
575#[repr(C)]
576pub struct RefAny {
577 /// Shared metadata: reference counts, type info, destructor, AND data pointer.
578 ///
579 /// All `RefAny` clones point to the same `RefCountInner` via this field.
580 /// The data pointer is stored in `RefCountInner` so all clones see the same
581 /// pointer, even after `replace_contents()` is called.
582 ///
583 /// The `run_destructor` flag on `RefCount` controls whether dropping this
584 /// `RefAny` should decrement the reference count and potentially free memory.
585 pub sharing_info: RefCount,
586
587 /// Unique ID for this specific clone (root = 0, subsequent clones increment).
588 ///
589 /// Used to distinguish between the original and clones for debugging.
590 pub instance_id: u64,
591}
592
593// The comparison traits below are hand-written, NOT derived, and key on
594// `sharing_info` ALONE. `instance_id` is deliberately omitted:
595//
596// // self.instance_id == other.instance_id <-- NEVER compare this
597//
598// `instance_id` is a debug-only counter that `clone()` increments (original = 0,
599// first clone = 1, ...). Deriving equality folded it in, so a `RefAny` never
600// equaled its own clone even though both point at the same `RefCountInner` — the
601// same heap data, same refcount. Equality here means "same data", not "same
602// handle"; `sharing_info` (a pointer + flag) already distinguishes unrelated
603// instances.
604//
605// Hash/Ord must key on exactly the same fields as PartialEq or they break their
606// own contracts (equal values must hash equally; `cmp() == Equal` must imply
607// `==`), so all five delegate to `sharing_info`.
608impl PartialEq for RefAny {
609 fn eq(&self, other: &Self) -> bool {
610 self.sharing_info == other.sharing_info
611 }
612}
613
614impl Eq for RefAny {}
615
616impl core::hash::Hash for RefAny {
617 fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
618 core::hash::Hash::hash(&self.sharing_info, state);
619 }
620}
621
622impl PartialOrd for RefAny {
623 fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
624 Some(self.cmp(other))
625 }
626}
627
628impl Ord for RefAny {
629 fn cmp(&self, other: &Self) -> core::cmp::Ordering {
630 self.sharing_info.cmp(&other.sharing_info)
631 }
632}
633
634impl_option!(
635 RefAny,
636 OptionRefAny,
637 copy = false,
638 [Debug, Hash, Clone, PartialEq, PartialOrd, Ord, Eq]
639);
640
641// AUDIT: unsound-but-required. These `Send`/`Sync` impls are unconditional in
642// `T`: a `!Send`/`!Sync` payload moved or shared cross-thread races its own
643// internals. This is an INTENTIONAL FFI design constraint — `RefAny` is a
644// type-erased C-ABI handle with no way to carry `T: Send + Sync` bounds across
645// the boundary, and the framework's threading model keeps a given payload on
646// one thread in practice. Left as-is per the audit; do not "fix" by adding
647// bounds (it would break the erased FFI type).
648//
649// SAFETY: RefAny is Send because:
650// - The data pointer points to heap memory (can be sent between threads)
651// - All shared state (RefCountInner) uses atomic operations
652// - No thread-local storage is used
653#[allow(clippy::non_send_fields_in_send_ty)] // see SAFETY note above: atomic refcount, no TLS, no cross-thread deref
654unsafe impl Send for RefAny {}
655
656// SAFETY: RefAny is Sync because:
657// - Methods on `&RefAny` (like `clone`, `get_type_id`) only use atomic operations or
658// read immutable data, which is inherently thread-safe
659// - The runtime borrow checker (via `can_be_shared/shared_mut`) uses SeqCst atomics
660//
661// AUDIT: unsound-but-required (same intentional FFI constraint as `Send` above).
662//
663// The check-then-increment race that this note described in `downcast_ref/mut`
664// is now FIXED (both use atomic `fetch_add`+validate / `compare_exchange`
665// acquisition — see those methods). The remaining unsoundness is only the
666// unconditional-in-`T` `Sync`, which is required by the erased C-ABI type.
667unsafe impl Sync for RefAny {}
668
669impl RefAny {
670 /// Creates a new type-erased `RefAny` containing the given value.
671 ///
672 /// This is the primary way to construct a `RefAny` from Rust code.
673 ///
674 /// # Type Safety
675 ///
676 /// Stores the `TypeId` of `T` for runtime type checking during downcasts.
677 ///
678 /// # Memory Layout
679 ///
680 /// - Allocates memory on the heap with correct size (`size_of::<T>()`) and alignment
681 /// (`align_of::<T>()`)
682 /// - Copies the value into the heap allocation
683 /// - Forgets the original value to prevent double-drop
684 ///
685 /// # Custom Destructor
686 ///
687 /// Creates a type-specific destructor that:
688 /// 1. Copies the data from heap back to stack
689 /// 2. Calls `mem::drop` to run `T`'s destructor
690 /// 3. The heap memory is freed separately in `RefAny::drop`
691 ///
692 /// This two-phase destruction ensures proper cleanup even for complex types.
693 ///
694 /// # Safety
695 ///
696 /// Safe because:
697 /// - `mem::forget` prevents double-drop of the original value
698 /// - Type `T` and destructor `<U>` are matched at compile time
699 /// - `ptr::copy_nonoverlapping` with count=1 copies exactly one `T`
700 ///
701 /// # Example
702 ///
703 /// ```rust
704 /// # use azul_core::refany::RefAny;
705 /// let mut data = RefAny::new(42i32);
706 /// let value = data.downcast_ref::<i32>().unwrap();
707 /// assert_eq!(*value, 42);
708 /// ```
709 pub fn new<T: 'static>(value: T) -> Self {
710 /// Type-specific destructor that properly drops the inner value.
711 ///
712 /// # Safety
713 ///
714 /// Safe to call ONLY with a pointer that was created by `RefAny::new<U>`.
715 /// The type `U` must match the original type `T`.
716 ///
717 /// # Why Copy to Stack?
718 ///
719 /// Rust's drop glue expects a value, not a pointer. We copy the data
720 /// to the stack so `mem::drop` can run the destructor properly.
721 ///
722 /// # Critical Fix
723 ///
724 /// The third argument to `copy_nonoverlapping` is the COUNT (1 element),
725 /// not the SIZE in bytes. Using `size_of::<U>()` here would copy
726 /// `size_of::<U>()` elements, causing buffer overflow.
727 extern "C" fn default_custom_destructor<U: 'static>(ptr: *mut c_void) {
728 use core::{mem, ptr};
729
730 // The actual drop glue. `U::drop` is arbitrary user code and this
731 // function is `extern "C"` (called across the FFI boundary from the
732 // C ABI teardown), so a panic escaping here would unwind across that
733 // boundary = UB.
734 // SAFETY: this fn is only installed by `RefAny::new::<U>`, so `ptr`
735 // points to an initialized, properly aligned `U` that no other code
736 // still references (we are in the final drop). We move it out exactly
737 // once (`count = 1`) and run its drop glue.
738 let run = || unsafe {
739 // A ZST has no bytes to move, and `ptr` is not a real pointer to one:
740 // `RefAny::new` never allocates for a ZST, and `RefCount::drop`
741 // substitutes a 1-byte-aligned dummy. Feeding that to
742 // `copy_nonoverlapping` violates its "aligned and non-null"
743 // precondition (`[u64; 0]` demands align 8) — UB, and Rust's debug
744 // check turns it into a NON-UNWINDING abort that kills the process.
745 //
746 // A ZST has exactly one value, so conjure it directly and run its drop
747 // glue without touching `ptr` at all.
748 if size_of::<U>() == 0 {
749 // Sound for a ZST (exactly one value, touches no memory); the
750 // size_of == 0 guard is what makes assume_init well-defined here.
751 #[allow(clippy::uninit_assumed_init)]
752 drop(mem::MaybeUninit::<U>::uninit().assume_init());
753 return;
754 }
755
756 // Allocate uninitialized stack space for one `U`
757 let mut stack_mem = mem::MaybeUninit::<U>::uninit();
758
759 // Copy 1 element of type U from heap to stack
760 ptr::copy_nonoverlapping(
761 ptr as *const U,
762 stack_mem.as_mut_ptr(),
763 1, // CRITICAL: This is element count, not byte count!
764 );
765
766 // Take ownership and run the destructor
767 let stack_mem = stack_mem.assume_init();
768 drop(stack_mem); // Runs U's Drop implementation
769 };
770
771 // AUDIT: contain any panic from `U::drop` so it can't unwind across
772 // the `extern "C"` boundary. `catch_unwind` needs `std`; `no_std`
773 // builds use `panic = "abort"`, where unwinding cannot occur.
774 #[cfg(feature = "std")]
775 {
776 drop(std::panic::catch_unwind(std::panic::AssertUnwindSafe(run)));
777 }
778 #[cfg(not(feature = "std"))]
779 {
780 run();
781 }
782 }
783
784 let type_name = ::core::any::type_name::<T>();
785 let type_id = Self::get_type_id_static::<T>();
786
787 let st = AzString::from_const_str(type_name);
788 let s = Self::new_c(
789 (&raw const value) as *const c_void,
790 ::core::mem::size_of::<T>(),
791 ::core::mem::align_of::<T>(), // CRITICAL: Pass alignment to prevent UB
792 type_id,
793 st,
794 default_custom_destructor::<T>,
795 0, // serialize_fn: not set for Rust types by default
796 0, // deserialize_fn: not set for Rust types by default
797 );
798 ::core::mem::forget(value); // Prevent double-drop
799 s
800 }
801
802 /// C-ABI compatible function to create a `RefAny` from raw components.
803 ///
804 /// This is the low-level constructor used by FFI bindings (C, Python, etc.).
805 ///
806 /// # Parameters
807 ///
808 /// - `ptr`: Pointer to the value to store (will be copied)
809 /// - `len`: Size of the value in bytes (`size_of::<T>()`)
810 /// - `align`: Required alignment in bytes (`align_of::<T>()`)
811 /// - `type_id`: Unique identifier for the type (for downcast safety)
812 /// - `type_name`: Human-readable type name (for debugging)
813 /// - `custom_destructor`: Function to call when the last reference is dropped
814 /// - `serialize_fn`: Function pointer for JSON serialization (0 = not set)
815 /// - `deserialize_fn`: Function pointer for JSON deserialization (0 = not set)
816 ///
817 /// # Safety
818 ///
819 /// Caller must ensure:
820 /// - `ptr` points to valid data of size `len` with alignment `align`
821 /// - `type_id` uniquely identifies the type
822 /// - `custom_destructor` correctly drops the type at `ptr`
823 /// - `len` and `align` match the actual type's layout
824 /// - If `serialize_fn != 0`, it must be a valid function pointer of type
825 /// `extern "C" fn(RefAny) -> Json`
826 /// - If `deserialize_fn != 0`, it must be a valid function pointer of type
827 /// `extern "C" fn(Json) -> ResultRefAnyString`
828 ///
829 /// # Zero-Sized Types
830 ///
831 /// Special case: ZSTs use a null pointer but still track the type info
832 /// and call the destructor (which may have side effects even for ZSTs).
833 ///
834 /// # Panics
835 ///
836 /// Panics if `ptr` is null while `len > 0` (a non-empty value must have a
837 /// valid backing pointer).
838 #[allow(clippy::used_underscore_binding)] // `_`-prefixed fields are an intentional FFI/api.json naming convention; internal access is required
839 pub fn new_c(
840 // *const T
841 ptr: *const c_void,
842 // sizeof(T)
843 len: usize,
844 // alignof(T)
845 align: usize,
846 // unique ID of the type (used for type comparison when downcasting)
847 type_id: u64,
848 // name of the class such as "app::MyData", usually compiler- or macro-generated
849 type_name: AzString,
850 custom_destructor: extern "C" fn(*mut c_void),
851 // function pointer for JSON serialization (0 = not set)
852 serialize_fn: usize,
853 // function pointer for JSON deserialization (0 = not set)
854 deserialize_fn: usize,
855 ) -> Self {
856 use core::ptr;
857
858 // CRITICAL: Validate input pointer for non-ZST types
859 // A NULL pointer for a non-zero-sized type would cause UB when copying
860 assert!(!(len > 0 && ptr.is_null()),
861 "RefAny::new_c: NULL pointer passed for non-ZST type (size={}). \
862 This would cause undefined behavior. Type: {:?}",
863 len,
864 type_name.as_str()
865 );
866
867 // Special case: Zero-sized types
868 //
869 // Calling `alloc(Layout { size: 0, .. })` is UB, so we use a null pointer.
870 // The destructor is still called (it may have side effects even for ZSTs).
871 let (_internal_ptr, layout) = if len == 0 {
872 let _dummy: [u8; 0] = [];
873 (ptr::null_mut(), Layout::for_value(&_dummy))
874 } else {
875 // CRITICAL FIX: Use the caller-provided alignment, not alignment of [u8]
876 //
877 // Previous bug: `Layout::for_value(&[u8])` created align=1
878 // This caused unaligned references when downcasting to types like i32 (align=4)
879 //
880 // Fixed: `Layout::from_size_align(len, align)` respects the type's alignment
881 let layout = Layout::from_size_align(len, align).expect("Failed to create layout");
882
883 // Allocate heap memory with correct alignment
884 // SAFETY: `layout` has non-zero size (this branch is `len != 0`), the
885 // required precondition for `alloc`; null return is handled below.
886 let heap_struct_as_bytes = unsafe { alloc::alloc::alloc(layout) };
887
888 // Handle allocation failure (aborts the program)
889 if heap_struct_as_bytes.is_null() {
890 alloc::alloc::handle_alloc_error(layout);
891 }
892
893 // Copy the data byte-by-byte to the heap
894 // SAFETY: Both pointers are valid, non-overlapping, and properly aligned
895 unsafe { ptr::copy_nonoverlapping(ptr as *const u8, heap_struct_as_bytes, len) };
896
897 (heap_struct_as_bytes, layout)
898 };
899
900 let ref_count_inner = RefCountInner {
901 _internal_ptr: _internal_ptr as *const c_void,
902 num_copies: AtomicUsize::new(1), // This is the first instance
903 num_refs: AtomicUsize::new(0), // No borrows yet
904 num_mutable_refs: AtomicUsize::new(0), // No mutable borrows yet
905 _internal_len: len,
906 _internal_layout_size: layout.size(),
907 _internal_layout_align: layout.align(),
908 type_id,
909 type_name,
910 custom_destructor,
911 serialize_fn,
912 deserialize_fn,
913 update_fn: 0, // on-update observer not set by default; see set_update_fn
914 };
915
916 let sharing_info = RefCount::new(ref_count_inner);
917
918 Self {
919 sharing_info,
920 instance_id: 0, // Root instance
921 }
922 }
923
924 /// Returns the raw data pointer for FFI downcasting.
925 ///
926 /// This is used by the `AZ_REFLECT` macros in C/C++ to access the
927 /// type-erased data pointer for downcasting operations.
928 ///
929 /// # Safety
930 ///
931 /// The returned pointer must only be dereferenced after verifying
932 /// the type ID matches the expected type. Callers are responsible
933 /// for proper type safety checks.
934 #[allow(clippy::used_underscore_binding)] // `_`-prefixed fields are an intentional FFI/api.json naming convention; internal access is required
935 #[must_use] pub fn get_data_ptr(&self) -> *const c_void {
936 self.sharing_info.downcast()._internal_ptr
937 }
938
939 /// Returns the byte length of the type-erased payload behind
940 /// [`Self::get_data_ptr`] (`size_of::<T>()` of the stored type;
941 /// `0` for ZSTs).
942 #[allow(clippy::used_underscore_binding)] // `_`-prefixed fields are an intentional FFI/api.json naming convention; internal access is required
943 #[must_use] pub fn get_data_len(&self) -> usize {
944 self.sharing_info.downcast()._internal_len
945 }
946
947 /// Checks if this is the only `RefAny` instance with no active borrows.
948 ///
949 /// Returns `true` only if:
950 /// - `num_copies == 1` (no clones exist)
951 /// - `num_refs == 0` (no shared borrows active)
952 /// - `num_mutable_refs == 0` (no mutable borrows active)
953 ///
954 /// Useful for checking if you have exclusive ownership.
955 ///
956 /// # Memory Ordering
957 ///
958 /// Uses `SeqCst` to ensure a consistent view across all three counters.
959 pub(crate) fn has_no_copies(&self) -> bool {
960 self.sharing_info
961 .downcast()
962 .num_copies
963 .load(AtomicOrdering::SeqCst)
964 == 1
965 && self
966 .sharing_info
967 .downcast()
968 .num_refs
969 .load(AtomicOrdering::SeqCst)
970 == 0
971 && self
972 .sharing_info
973 .downcast()
974 .num_mutable_refs
975 .load(AtomicOrdering::SeqCst)
976 == 0
977 }
978
979 /// Attempts to downcast to a shared reference of type `U`.
980 ///
981 /// Returns `None` if:
982 /// - The stored type doesn't match `U` (type safety)
983 /// - A mutable borrow is already active (borrow checking)
984 /// - The pointer is null AND `U` is not zero-sized (uninitialized). A
985 /// stored ZST has a null pointer *by design* (nothing is allocated) and
986 /// downcasts successfully, via a dangling-but-aligned reference.
987 ///
988 /// # Type Safety
989 ///
990 /// Compares `type_id` at runtime before casting. This prevents casting
991 /// `*const c_void` to the wrong type, which would be immediate UB.
992 ///
993 /// # Borrow Checking
994 ///
995 /// Checks `can_be_shared()` to enforce Rust's borrowing rules:
996 /// - Multiple shared borrows are allowed
997 /// - Shared and mutable borrows cannot coexist
998 ///
999 /// # Safety
1000 ///
1001 /// The `unsafe` cast is safe because:
1002 /// - Type ID check ensures `U` matches the stored type
1003 /// - Memory was allocated with correct alignment for `U`
1004 /// - Lifetime `'a` is tied to `&'a mut self`, preventing use-after-free
1005 /// - Reference count is incremented atomically before returning
1006 ///
1007 /// # Why `&mut self`?
1008 ///
1009 /// Requires `&mut self` to prevent multiple threads from calling this
1010 /// simultaneously on the same `RefAny`. The borrow checker enforces this.
1011 /// Clones of the `RefAny` can call this independently (they share data
1012 /// but have separate runtime borrow tracking).
1013 #[allow(clippy::used_underscore_binding)] // `_`-prefixed fields are an intentional FFI/api.json naming convention; internal access is required
1014 #[inline]
1015 pub fn downcast_ref<U: 'static>(&mut self) -> Option<Ref<'_, U>> {
1016 // Runtime type check: prevent downcasting to wrong type
1017 let stored_type_id = self.get_type_id();
1018 let target_type_id = Self::get_type_id_static::<U>();
1019 let is_same_type = stored_type_id == target_type_id;
1020
1021 if !is_same_type {
1022 return None;
1023 }
1024
1025 // AUDIT: ATOMIC shared-borrow acquisition.
1026 //
1027 // `RefAny` is `Sync` and clones share one `RefCountInner`, so the old
1028 // check-then-increment (`can_be_shared()` then `increase_ref()`) raced a
1029 // concurrent `downcast_mut` on another clone: both could pass their
1030 // pre-checks and hand out aliasing `&`/`&mut` to the same memory (UB).
1031 //
1032 // Fix (mirrors the `compare_exchange` discipline in `replace_contents`):
1033 // increment `num_refs` FIRST, then validate that no mutable borrow is
1034 // live. `SeqCst` imposes a single total order, so a writer (which CASes
1035 // `num_mutable_refs` 0->1 then reads `num_refs`) and this reader (which
1036 // adds to `num_refs` then reads `num_mutable_refs`) can never both
1037 // succeed — at least one observes the other's write. Back the increment
1038 // out on any failure path.
1039 self.sharing_info.increase_ref();
1040
1041 if !self.sharing_info.can_be_shared() {
1042 // A mutable borrow is (being) acquired — release and fail.
1043 self.sharing_info.decrease_ref();
1044 return None;
1045 }
1046
1047 // Get data pointer from shared RefCountInner (stable while we hold the
1048 // shared borrow: `replace_contents` needs `num_refs == 0` to proceed).
1049 let data_ptr = self.sharing_info.downcast()._internal_ptr;
1050
1051 // A null `_internal_ptr` means either an uninitialized `RefAny` or a ZST:
1052 // `RefAny::new_c` stores ZSTs with a null pointer (they need no backing
1053 // allocation). A ZST is a *valid* stored value, so the type check above is
1054 // authoritative and a `&U` to a ZST dereferences no bytes — only a
1055 // *non-ZST* null pointer is a genuine failure.
1056 if data_ptr.is_null() && size_of::<U>() != 0 {
1057 self.sharing_info.decrease_ref();
1058 return None;
1059 }
1060
1061 Some(Ref {
1062 // SAFETY: type check passed. For a real value `data_ptr` is non-null
1063 // and correctly aligned; for a ZST (null pointer) we hand out a
1064 // dangling-but-aligned `NonNull::dangling` reference, valid precisely
1065 // because it is never dereferenced for bytes.
1066 ptr: unsafe {
1067 if data_ptr.is_null() {
1068 &*core::ptr::NonNull::<U>::dangling().as_ptr()
1069 } else {
1070 &*(data_ptr as *const U)
1071 }
1072 },
1073 sharing_info: self.sharing_info.clone(),
1074 })
1075 }
1076
1077 /// Attempts to downcast to a mutable reference of type `U`.
1078 ///
1079 /// Returns `None` if:
1080 /// - The stored type doesn't match `U` (type safety)
1081 /// - Any borrow is already active (borrow checking)
1082 /// - The pointer is null AND `U` is not zero-sized (uninitialized). A
1083 /// stored ZST has a null pointer *by design* and downcasts successfully,
1084 /// via a dangling-but-aligned reference; note that the on-update observer
1085 /// is NOT fired for a ZST (there are no bytes for it to snapshot).
1086 ///
1087 /// # Type Safety
1088 ///
1089 /// Compares `type_id` at runtime before casting, preventing UB.
1090 ///
1091 /// # Borrow Checking
1092 ///
1093 /// Checks `can_be_shared_mut()` to enforce exclusive mutability:
1094 /// - No other borrows (shared or mutable) can be active
1095 /// - This is Rust's `&mut T` rule, enforced at runtime
1096 ///
1097 /// # Safety
1098 ///
1099 /// The `unsafe` cast is safe because:
1100 ///
1101 /// - Type ID check ensures `U` matches the stored type
1102 /// - Memory was allocated with correct alignment for `U`
1103 /// - Borrow check ensures no other references exist
1104 /// - Lifetime `'a` is tied to `&'a mut self`, preventing aliasing
1105 /// - Mutable reference count is incremented atomically
1106 ///
1107 /// # Memory Ordering
1108 ///
1109 /// The `increase_refmut()` uses `SeqCst`, ensuring other threads see
1110 /// this mutable borrow before they try to acquire any borrow.
1111 #[allow(clippy::used_underscore_binding)] // `_`-prefixed fields are an intentional FFI/api.json naming convention; internal access is required
1112 #[inline]
1113 pub fn downcast_mut<U: 'static>(&mut self) -> Option<RefMut<'_, U>> {
1114 // Runtime type check
1115 let is_same_type = self.get_type_id() == Self::get_type_id_static::<U>();
1116 if !is_same_type {
1117 return None;
1118 }
1119
1120 // AUDIT: ATOMIC exclusive-borrow acquisition (mirror `replace_contents`).
1121 //
1122 // The old check-then-increment (`can_be_shared_mut()` then
1123 // `increase_refmut()`) raced concurrent borrows on sibling clones and
1124 // could hand out an aliasing `&mut` (UB). Instead, `compare_exchange`
1125 // `num_mutable_refs` 0->1 to atomically take the exclusive slot, THEN
1126 // verify no shared borrow is live; release + fail otherwise. The CAS
1127 // both acquires and rejects a second mutable borrow in one step.
1128 let inner = self.sharing_info.downcast();
1129 if inner
1130 .num_mutable_refs
1131 .compare_exchange(0, 1, AtomicOrdering::SeqCst, AtomicOrdering::SeqCst)
1132 .is_err()
1133 {
1134 return None;
1135 }
1136 if inner.num_refs.load(AtomicOrdering::SeqCst) != 0 {
1137 // A shared borrow is live — release the exclusive slot and fail.
1138 inner.num_mutable_refs.store(0, AtomicOrdering::SeqCst);
1139 return None;
1140 }
1141
1142 // Get data pointer from shared RefCountInner
1143 let data_ptr = inner._internal_ptr;
1144
1145 // A null `_internal_ptr` is either an uninitialized `RefAny` or a ZST
1146 // (stored with a null pointer; see `downcast_ref`). A non-ZST null is a
1147 // real failure — release the exclusive slot and bail. For a ZST there are
1148 // no bytes to observe or mutate, so skip the update observer below and
1149 // hand out a dangling-but-aligned `&mut`, keeping the exclusive borrow.
1150 if data_ptr.is_null() {
1151 if size_of::<U>() != 0 {
1152 inner.num_mutable_refs.store(0, AtomicOrdering::SeqCst);
1153 return None;
1154 }
1155 return Some(RefMut {
1156 // SAFETY: type check passed; `U` is a ZST, so a dangling-but-
1157 // aligned pointer is a valid `&mut` never dereferenced for bytes.
1158 ptr: unsafe { &mut *core::ptr::NonNull::<U>::dangling().as_ptr() },
1159 sharing_info: self.sharing_info.clone(),
1160 });
1161 }
1162
1163 // Fire the on-update observer (if registered) BEFORE handing out the
1164 // mutable borrow: the callback sees the pre-mutation data + its byte
1165 // length, enabling undo/redo snapshots and client/server state sync.
1166 let update_fn = inner.update_fn;
1167 if update_fn != 0 {
1168 // SAFETY: `update_fn` is non-zero (checked) and, per `set_update_fn`'s
1169 // contract, is a valid `extern "C" fn(*const c_void, usize)`. The
1170 // round-trip goes through an int-to-pointer CAST (not a direct
1171 // usize->fn transmute): a transmuted integer carries no provenance,
1172 // which is UB to call (Miri rejects it); the cast re-acquires it.
1173 let cb: extern "C" fn(*const c_void, usize) =
1174 unsafe { core::mem::transmute(update_fn as *const ()) };
1175 let len = inner._internal_len;
1176 // AUDIT: the observer is a host-provided `extern "C"` fn. A Rust
1177 // panic escaping it would unwind across the FFI boundary (UB), so
1178 // contain it. `catch_unwind` needs `std`; `no_std` builds use
1179 // `panic = "abort"` where no unwinding can occur.
1180 #[cfg(feature = "std")]
1181 {
1182 drop(std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1183 cb(data_ptr, len);
1184 })));
1185 }
1186 #[cfg(not(feature = "std"))]
1187 {
1188 cb(data_ptr, len);
1189 }
1190 }
1191
1192 Some(RefMut {
1193 // SAFETY: Type and borrow checks passed, exclusive access guaranteed
1194 ptr: unsafe { &mut *(data_ptr as *mut U) },
1195 sharing_info: self.sharing_info.clone(),
1196 })
1197 }
1198
1199 /// Computes a runtime type ID from Rust's `TypeId`.
1200 ///
1201 /// Rust's `TypeId` is not `#[repr(C)]` and can't cross FFI boundaries.
1202 /// This function converts it to a `u64` by treating it as a byte array.
1203 ///
1204 /// # Safety
1205 ///
1206 /// Safe because:
1207 /// - `TypeId` is a valid type with a stable layout
1208 /// - We only read from it, never write
1209 /// - The slice lifetime is bounded by the function scope
1210 ///
1211 /// # Implementation
1212 ///
1213 /// Treats the `TypeId` as bytes and sums them with bit shifts to create
1214 /// a unique (but not cryptographically secure) hash.
1215 #[inline]
1216 fn get_type_id_static<T: 'static>() -> u64 {
1217 use core::{any::TypeId, mem};
1218
1219 let t_id = TypeId::of::<T>();
1220
1221 // SAFETY: TypeId is a valid type, we're only reading it
1222 let struct_as_bytes = unsafe {
1223 core::slice::from_raw_parts(
1224 (&raw const t_id) as *const u8,
1225 size_of::<TypeId>(),
1226 )
1227 };
1228
1229 // AUDIT: fold ALL bytes of the `TypeId` (16 on current toolchains),
1230 // not just the first 8. This u64 is the ONLY runtime type guard used by
1231 // `downcast_*`; dropping the high 8 bytes let two distinct types whose
1232 // `TypeId`s differ only in their upper half collide, permitting a
1233 // wrong-type downcast (UB). An FxHash-style rotate+multiply mixes every
1234 // byte into the result and is deterministic within a process run (which
1235 // is all `TypeId` itself guarantees).
1236 struct_as_bytes.iter().fold(0u64, |hash, &b| {
1237 (hash.rotate_left(5) ^ u64::from(b)).wrapping_mul(0x51_7c_c1_b7_27_22_0a_95)
1238 })
1239 }
1240
1241 /// Checks if the stored type matches the given type ID.
1242 #[must_use] pub fn is_type(&self, type_id: u64) -> bool {
1243 self.sharing_info.downcast().type_id == type_id
1244 }
1245
1246 /// Returns the stored type ID.
1247 #[must_use] pub fn get_type_id(&self) -> u64 {
1248 self.sharing_info.downcast().type_id
1249 }
1250
1251 /// Returns the human-readable type name for debugging.
1252 #[must_use] pub fn get_type_name(&self) -> AzString {
1253 self.sharing_info.downcast().type_name.clone()
1254 }
1255
1256 /// Returns the current reference count (number of `RefAny` clones sharing this data).
1257 ///
1258 /// This is useful for debugging and metadata purposes.
1259 #[must_use] pub fn get_ref_count(&self) -> usize {
1260 self.sharing_info
1261 .downcast()
1262 .num_copies
1263 .load(AtomicOrdering::SeqCst)
1264 }
1265
1266 /// Returns the serialize function pointer (0 = not set).
1267 ///
1268 /// This is used for JSON serialization of `RefAny` contents.
1269 #[must_use] pub fn get_serialize_fn(&self) -> usize {
1270 self.sharing_info.downcast().serialize_fn
1271 }
1272
1273 /// Returns the deserialize function pointer (0 = not set).
1274 ///
1275 /// This is used for JSON deserialization to create a new `RefAny`.
1276 #[must_use] pub fn get_deserialize_fn(&self) -> usize {
1277 self.sharing_info.downcast().deserialize_fn
1278 }
1279
1280 /// Sets the serialize function pointer.
1281 ///
1282 /// # Safety
1283 ///
1284 /// The caller must ensure the function pointer is valid and has the correct
1285 /// signature: `extern "C" fn(RefAny) -> Json`
1286 ///
1287 /// **Known issue:** `&mut self` is exclusive to this clone, not to the shared
1288 /// `RefCountInner`. Concurrent calls via different clones are a data race
1289 /// because `serialize_fn` is a plain `usize`, not atomic.
1290 pub fn set_serialize_fn(&mut self, serialize_fn: usize) {
1291 // FIXME: &mut self is exclusive to this clone only, not to the shared
1292 // RefCountInner — concurrent calls via different clones are a data race.
1293 let inner = self.sharing_info.ptr.cast_mut();
1294 // SAFETY: `inner` came from `Box::into_raw` and is live (we hold `self`).
1295 unsafe {
1296 (*inner).serialize_fn = serialize_fn;
1297 }
1298 }
1299
1300 /// Sets the deserialize function pointer.
1301 ///
1302 /// # Safety
1303 ///
1304 /// The caller must ensure the function pointer is valid and has the correct
1305 /// signature: `extern "C" fn(Json) -> ResultRefAnyString`
1306 ///
1307 /// **Known issue:** `&mut self` is exclusive to this clone, not to the shared
1308 /// `RefCountInner`. Concurrent calls via different clones are a data race
1309 /// because `deserialize_fn` is a plain `usize`, not atomic.
1310 pub fn set_deserialize_fn(&mut self, deserialize_fn: usize) {
1311 // FIXME: &mut self is exclusive to this clone only, not to the shared
1312 // RefCountInner — concurrent calls via different clones are a data race.
1313 let inner = self.sharing_info.ptr.cast_mut();
1314 // SAFETY: `inner` came from `Box::into_raw` and is live (we hold `self`).
1315 unsafe {
1316 (*inner).deserialize_fn = deserialize_fn;
1317 }
1318 }
1319
1320 /// Registers an on-update observer (`0` = unset). It is fired from
1321 /// [`Self::downcast_mut`] with the (data ptr, byte len) of the *pre-mutation*
1322 /// data, just before the mutable borrow is handed out — the foundation for
1323 /// undo/redo snapshots and client/server state sync.
1324 ///
1325 /// # Safety
1326 ///
1327 /// If `update_fn != 0` it must be a valid `extern "C" fn(*const c_void, usize)`.
1328 /// Same shared-`RefCountInner` caveat as [`Self::set_serialize_fn`]: `&mut self`
1329 /// is exclusive to this clone, not to the shared inner.
1330 pub fn set_update_fn(&mut self, update_fn: usize) {
1331 let inner = self.sharing_info.ptr.cast_mut();
1332 // SAFETY: `inner` came from `Box::into_raw` and is live (we hold `self`).
1333 unsafe {
1334 (*inner).update_fn = update_fn;
1335 }
1336 }
1337
1338 /// Returns the registered on-update observer fn pointer (`0` = unset).
1339 #[must_use] pub fn get_update_fn(&self) -> usize {
1340 self.sharing_info.downcast().update_fn
1341 }
1342
1343 /// Returns true if this `RefAny` supports JSON serialization.
1344 #[must_use] pub fn can_serialize(&self) -> bool {
1345 self.get_serialize_fn() != 0
1346 }
1347
1348 /// Returns true if this `RefAny` type supports JSON deserialization.
1349 #[must_use] pub fn can_deserialize(&self) -> bool {
1350 self.get_deserialize_fn() != 0
1351 }
1352
1353 /// Replaces the contents of this `RefAny` with a new value from another `RefAny`.
1354 ///
1355 /// This method:
1356 /// 1. Atomically acquires a mutable "lock" via `compare_exchange`
1357 /// 2. Calls the destructor on the old value
1358 /// 3. Deallocates the old memory
1359 /// 4. Copies the new value's memory
1360 /// 5. Updates metadata (`type_id`, `type_name`, destructor, serialize/deserialize fns)
1361 /// 6. Updates the shared _`internal_ptr` so ALL clones see the new data
1362 /// 7. Releases the lock
1363 ///
1364 /// Since all clones of a `RefAny` share the same `RefCountInner`, this change
1365 /// will be visible to ALL clones of this `RefAny`.
1366 ///
1367 /// # Returns
1368 ///
1369 /// - `true` if the replacement was successful
1370 /// - `false` if there are active borrows (would cause UB)
1371 ///
1372 /// # Thread Safety
1373 ///
1374 /// Uses `compare_exchange` to atomically acquire exclusive access, preventing
1375 /// any race condition between checking for borrows and modifying the data.
1376 ///
1377 /// # Safety
1378 ///
1379 /// Safe because:
1380 /// - We atomically acquire exclusive access before modifying
1381 /// - The old destructor is called before deallocation
1382 /// - Memory is properly allocated with correct alignment
1383 /// - All metadata is updated while holding the lock
1384 ///
1385 /// # Panics
1386 ///
1387 /// Panics if a memory `Layout` for the replacement value cannot be
1388 /// constructed (its size overflows `isize::MAX`).
1389 #[allow(clippy::used_underscore_binding)] // `_`-prefixed fields are an intentional FFI/api.json naming convention; internal access is required
1390 pub fn replace_contents(&mut self, new_value: Self) -> bool {
1391 use core::ptr;
1392
1393 let inner = self.sharing_info.ptr.cast_mut();
1394
1395 // Atomically acquire exclusive access by setting num_mutable_refs to 1.
1396 // This uses compare_exchange to ensure no race condition:
1397 // - If num_mutable_refs is 0, set it to 1 (success)
1398 // - If num_mutable_refs is not 0, someone else has it (fail)
1399 // We also need to check num_refs == 0 atomically.
1400 let inner_ref = self.sharing_info.downcast();
1401
1402 // First, try to acquire the mutable lock
1403 let mutable_lock_result = inner_ref.num_mutable_refs.compare_exchange(
1404 0, // expected: no mutable refs
1405 1, // desired: we take the mutable ref
1406 AtomicOrdering::SeqCst,
1407 AtomicOrdering::SeqCst,
1408 );
1409
1410 if mutable_lock_result.is_err() {
1411 // Someone else has a mutable reference
1412 return false;
1413 }
1414
1415 // Now check that there are no shared references
1416 // Note: We hold the mutable lock, so no new shared refs can be acquired
1417 if inner_ref.num_refs.load(AtomicOrdering::SeqCst) != 0 {
1418 // Release the lock and fail
1419 inner_ref.num_mutable_refs.store(0, AtomicOrdering::SeqCst);
1420 return false;
1421 }
1422
1423 // We now have exclusive access - perform the replacement
1424 // SAFETY: we hold the exclusive lock (num_mutable_refs==1, num_refs==0),
1425 // so no live `Ref`/`RefMut` aliases the data; `inner` is the live
1426 // `RefCountInner` from `Box::into_raw`. Old data is destructed+freed with
1427 // its own stored layout before the pointer is overwritten, and the new
1428 // data is freshly allocated and byte-copied.
1429 unsafe {
1430 // Get old layout info before we overwrite it
1431 let old_ptr = (*inner)._internal_ptr;
1432 let old_len = (*inner)._internal_len;
1433 let old_layout_size = (*inner)._internal_layout_size;
1434 let old_layout_align = (*inner)._internal_layout_align;
1435 let old_destructor = (*inner).custom_destructor;
1436
1437 // Step 1: Call destructor on old value (if non-ZST)
1438 if old_len > 0 && !old_ptr.is_null() {
1439 old_destructor(old_ptr.cast_mut());
1440 }
1441
1442 // Step 2: Deallocate old memory (if non-ZST). Use the *checked*
1443 // `Layout::from_size_align` (not `_unchecked`): the stored
1444 // size/align came from a valid `Layout`, so it always succeeds, and
1445 // this shrinks the unchecked surface inside this unsafe block.
1446 if old_layout_size > 0 && !old_ptr.is_null() {
1447 let old_layout = Layout::from_size_align(old_layout_size, old_layout_align)
1448 .expect("replace_contents: stored old layout was invalid");
1449 alloc::alloc::dealloc(old_ptr as *mut u8, old_layout);
1450 }
1451
1452 // Get new value's metadata
1453 let new_inner = new_value.sharing_info.downcast();
1454 let new_ptr = new_inner._internal_ptr;
1455 let new_len = new_inner._internal_len;
1456 let new_layout_size = new_inner._internal_layout_size;
1457 let new_layout_align = new_inner._internal_layout_align;
1458
1459 // Step 3: Allocate new memory and copy data
1460 let allocated_ptr = if new_len == 0 {
1461 ptr::null_mut()
1462 } else {
1463 let new_layout = Layout::from_size_align(new_len, new_layout_align)
1464 .expect("Failed to create layout");
1465 let heap_ptr = alloc::alloc::alloc(new_layout);
1466 if heap_ptr.is_null() {
1467 alloc::alloc::handle_alloc_error(new_layout);
1468 }
1469 // Copy data from new_value
1470 ptr::copy_nonoverlapping(
1471 new_ptr as *const u8,
1472 heap_ptr,
1473 new_len,
1474 );
1475 heap_ptr
1476 };
1477
1478 // Step 4: Update the shared internal pointer in RefCountInner
1479 // All clones will see this new pointer!
1480 (*inner)._internal_ptr = allocated_ptr as *const c_void;
1481
1482 // Step 5: Update metadata in RefCountInner
1483 (*inner)._internal_len = new_len;
1484 (*inner)._internal_layout_size = new_layout_size;
1485 (*inner)._internal_layout_align = new_layout_align;
1486 (*inner).type_id = new_inner.type_id;
1487 (*inner).type_name = new_inner.type_name.clone();
1488 (*inner).custom_destructor = new_inner.custom_destructor;
1489 (*inner).serialize_fn = new_inner.serialize_fn;
1490 (*inner).deserialize_fn = new_inner.deserialize_fn;
1491 (*inner).update_fn = new_inner.update_fn;
1492 }
1493
1494 // Release the mutable lock
1495 self.sharing_info.downcast().num_mutable_refs.store(0, AtomicOrdering::SeqCst);
1496
1497 // AUDIT: reclaim `new_value` instead of leaking it.
1498 //
1499 // The old code `mem::forget(new_value)` to stop `RefAny::drop` from
1500 // running the T-destructor a SECOND time on the bytes we just copied
1501 // into our own allocation — but that leaked `new_value`'s entire
1502 // `RefCountInner` box AND its heap data block on every single call.
1503 //
1504 // Instead, neutralize `new_value`'s destructor to a no-op and let the
1505 // normal refcount teardown run: it frees BOTH allocations (data block +
1506 // inner box) when this was the last reference, without re-running the
1507 // real T-destructor (which now lives on OUR inner, to run exactly once
1508 // when `self` is finally dropped). If `new_value` still had clones, the
1509 // no-op keeps them from double-dropping the shared T while their own
1510 // last drop still reclaims the shared block — no double free, no leak.
1511 #[allow(clippy::items_after_statements)]
1512 const extern "C" fn noop_destructor(_: *mut c_void) {}
1513 let new_inner = new_value.sharing_info.ptr.cast_mut();
1514 if !new_inner.is_null() {
1515 // SAFETY: `new_inner` came from `Box::into_raw` in `RefCount::new`
1516 // and is still alive (we hold `new_value`).
1517 unsafe {
1518 (*new_inner).custom_destructor = noop_destructor;
1519 }
1520 }
1521 drop(new_value);
1522
1523 true
1524 }
1525}
1526
1527impl Clone for RefAny {
1528 /// Creates a new `RefAny` sharing the same heap-allocated data.
1529 ///
1530 /// This is cheap (just increments a counter) and is how multiple parts
1531 /// of the code can hold references to the same data.
1532 ///
1533 /// # Reference Counting
1534 ///
1535 /// Atomically increments `num_copies` with `SeqCst` ordering before
1536 /// creating the clone. This ensures all threads see the updated count
1537 /// before the clone can be used.
1538 ///
1539 /// # Instance ID
1540 ///
1541 /// Each clone gets a unique `instance_id` based on the current copy count.
1542 /// The original has `instance_id=0`, the first clone gets `1`, etc.
1543 ///
1544 /// # Memory Ordering
1545 ///
1546 /// The `fetch_add` followed by `load` both use `SeqCst`:
1547 /// - `fetch_add`: Ensures the increment is visible to all threads
1548 /// - `load`: Gets the updated value for the `instance_id`
1549 ///
1550 /// This prevents race conditions where two threads clone simultaneously
1551 /// and both see the same `instance_id`.
1552 ///
1553 /// # Safety
1554 ///
1555 /// Safe because:
1556 ///
1557 /// - Atomic operations prevent data races
1558 /// - The heap allocation remains valid (only freed when count reaches 0)
1559 /// - `run_destructor` is set to `true` for all clones
1560 fn clone(&self) -> Self {
1561 // Atomically increment the reference count
1562 let inner = self.sharing_info.downcast();
1563 let prev = inner.num_copies.fetch_add(1, AtomicOrdering::SeqCst);
1564
1565 let new_instance_id = (prev + 1) as u64;
1566
1567 Self {
1568 // Data pointer is now in RefCountInner, shared automatically
1569 sharing_info: RefCount {
1570 ptr: self.sharing_info.ptr, // Share the same metadata (and data pointer)
1571 run_destructor: true, // This clone should decrement num_copies on drop
1572 },
1573 // Give this clone a unique ID based on the updated count
1574 instance_id: new_instance_id,
1575 }
1576 }
1577}
1578
1579impl Drop for RefAny {
1580 /// Empty drop implementation - all cleanup is handled by `RefCount::drop`.
1581 ///
1582 /// When a `RefAny` is dropped, its `sharing_info: RefCount` field is automatically
1583 /// dropped by Rust. The `RefCount::drop` implementation handles all cleanup:
1584 ///
1585 /// 1. Atomically decrements `num_copies` with `fetch_sub`
1586 /// 2. If the previous value was 1 (we're the last reference):
1587 /// - Reclaims the `RefCountInner` via `Box::from_raw`
1588 /// - Calls the custom destructor to run `T::drop()`
1589 /// - Deallocates the heap memory with the stored layout
1590 ///
1591 /// # Why No Code Here?
1592 ///
1593 /// Previously, `RefAny::drop` handled cleanup, but this caused issues with the
1594 /// C API where `Ref<T>` and `RefMut<T>` guards (which clone the `RefCount`) need
1595 /// to keep the data alive even after the original `RefAny` is dropped.
1596 ///
1597 /// By moving all cleanup to `RefCount::drop`, we ensure that:
1598 /// - `RefAny::clone()` creates a `RefCount` with `run_destructor = true`
1599 /// - `AZ_REFLECT` macros create `Ref`/`RefMut` guards that clone `RefCount`
1600 /// - Each `RefCount` drop decrements the counter
1601 /// - Only the LAST drop (when `num_copies` was 1) cleans up memory
1602 ///
1603 /// See `RefCount::drop` for the full algorithm and safety documentation.
1604 fn drop(&mut self) {
1605 // RefCount::drop handles everything automatically.
1606 // The sharing_info field is dropped by Rust, triggering RefCount::drop.
1607 }
1608}
1609
1610#[cfg(test)]
1611#[allow(clippy::items_after_statements, clippy::redundant_clone, clippy::cast_possible_truncation, clippy::cast_sign_loss, trivial_casts, clippy::borrow_as_ptr, clippy::cast_ptr_alignment, clippy::unused_self, unused_qualifications, unreachable_pub, private_interfaces)] // pedantic lints are noise in unsafe-exercising test code
1612mod audit_tests {
1613 use super::*;
1614 use core::sync::atomic::{AtomicUsize, Ordering};
1615
1616 static DROP_COUNT: AtomicUsize = AtomicUsize::new(0);
1617
1618 // The tests below share the single `DROP_COUNT` static: each resets it to 0
1619 // and then asserts an exact drop count. Under the default multi-threaded
1620 // test runner they would otherwise interleave and corrupt each other's
1621 // counts (a real, if test-only, isolation bug). Every `DROP_COUNT`-using
1622 // test takes this lock first to serialize; it is poison-tolerant so one
1623 // failing test does not cascade `.unwrap()` panics into the rest.
1624 static DROP_COUNT_SERIAL: std::sync::Mutex<()> = std::sync::Mutex::new(());
1625 fn serialize_drop_count() -> std::sync::MutexGuard<'static, ()> {
1626 DROP_COUNT_SERIAL
1627 .lock()
1628 .unwrap_or_else(std::sync::PoisonError::into_inner)
1629 }
1630
1631 struct DropCounter(#[allow(dead_code)] u32);
1632 impl Drop for DropCounter {
1633 fn drop(&mut self) {
1634 DROP_COUNT.fetch_add(1, Ordering::SeqCst);
1635 }
1636 }
1637
1638 // AUDIT: exclusive borrow must be denied while a shared borrow is live and
1639 // vice-versa (runtime borrow checker), and must be recoverable after the
1640 // guard drops. Exercises the atomic acquire/release added to downcast_*.
1641 #[test]
1642 fn borrow_exclusion_and_recovery() {
1643 // The runtime borrow guard lives in the *shared* refcount inner, so it
1644 // is only observable across two clones (a single `RefAny` can't hold two
1645 // guards at once — the methods take `&mut self`). `b` shares `a`'s inner.
1646 let mut a = RefAny::new(7i32);
1647 let mut b = a.clone();
1648
1649 {
1650 let r = a.downcast_ref::<i32>().unwrap();
1651 assert_eq!(*r, 7);
1652 // shared borrow live -> no mutable borrow via the shared inner
1653 assert!(b.downcast_mut::<i32>().is_none());
1654 // another shared borrow is fine
1655 assert!(b.downcast_ref::<i32>().is_some());
1656 }
1657
1658 {
1659 let mut m = a.downcast_mut::<i32>().unwrap();
1660 *m = 42;
1661 // mutable borrow live -> no shared borrow via the shared inner
1662 assert!(b.downcast_ref::<i32>().is_none());
1663 }
1664
1665 assert_eq!(*a.downcast_ref::<i32>().unwrap(), 42);
1666 }
1667
1668 // AUDIT: wrong-type downcast must be rejected. Same type -> same id.
1669 #[test]
1670 fn type_id_guard() {
1671 let mut a = RefAny::new(1u64);
1672 assert!(a.downcast_ref::<i32>().is_none());
1673 assert!(a.downcast_ref::<u64>().is_some());
1674
1675 assert_eq!(
1676 RefAny::get_type_id_static::<u64>(),
1677 RefAny::get_type_id_static::<u64>()
1678 );
1679 assert_ne!(
1680 RefAny::get_type_id_static::<u64>(),
1681 RefAny::get_type_id_static::<i64>()
1682 );
1683 }
1684
1685 // AUDIT: replace_contents must run each stored value's destructor exactly
1686 // once (old value on replace, new value on final drop) and must not leak.
1687 #[test]
1688 fn replace_contents_drops_exactly_once() {
1689 let _serial = serialize_drop_count();
1690 DROP_COUNT.store(0, Ordering::SeqCst);
1691 {
1692 let mut a = RefAny::new(DropCounter(1));
1693 let b = RefAny::new(DropCounter(2));
1694 assert!(a.replace_contents(b));
1695 // The original `a` value was dropped during replacement.
1696 assert_eq!(DROP_COUNT.load(Ordering::SeqCst), 1);
1697 // `a` now holds the (copied) `b` value; dropped at end of scope.
1698 }
1699 // Two DropCounter values were constructed; both must be dropped once.
1700 assert_eq!(DROP_COUNT.load(Ordering::SeqCst), 2);
1701 }
1702
1703 // AUDIT: replace_contents must fail (return false) while a borrow is live.
1704 #[test]
1705 fn replace_contents_denied_while_borrowed() {
1706 let mut a = RefAny::new(1i32);
1707 // Clone first: `r` will exclusively borrow `a`, so the sibling clone
1708 // must exist beforehand. Both share the same inner RefCountInner.
1709 let mut a2 = a.clone();
1710 let r = a.downcast_ref::<i32>().unwrap();
1711 // A live shared borrow (num_refs != 0) on the shared inner must block
1712 // replace_contents via the sibling clone.
1713 assert!(!a2.replace_contents(RefAny::new(2i32)));
1714 drop(r);
1715 assert!(a2.replace_contents(RefAny::new(2i32)));
1716 }
1717
1718 // ---- Miri-focused unit tests -------------------------------------------
1719 // These exercise the pure-Rust memory behavior of each unsafe path so Miri
1720 // can detect UB (bad provenance, misalignment, use-after-free, leaks,
1721 // refcount corruption). No FFI, no threads, no OS calls; tiny allocations.
1722
1723 // MIRI: covers RefAny::new + new_c alloc/copy_nonoverlapping + downcast_ref
1724 // (&*(ptr as *const U)) + the final Drop path (Box::from_raw + dealloc +
1725 // custom destructor). A non-Copy heap type checks the destructor runs.
1726 #[test]
1727 fn miri_new_downcast_drop_roundtrip() {
1728 let _serial = serialize_drop_count();
1729 DROP_COUNT.store(0, Ordering::SeqCst);
1730 {
1731 let mut a = RefAny::new(DropCounter(9));
1732 // downcast_ref exercises the type-id guard + aligned pointer cast.
1733 assert!(a.downcast_ref::<DropCounter>().is_some());
1734 assert!(a.downcast_ref::<u8>().is_none());
1735 }
1736 assert_eq!(DROP_COUNT.load(Ordering::SeqCst), 1);
1737 }
1738
1739 // MIRI: alignment correctness of new_c's Layout::from_size_align path. An
1740 // over-aligned payload downcast to a misaligned pointer would be UB.
1741 #[test]
1742 fn miri_alignment_preserved() {
1743 #[repr(align(16))]
1744 #[derive(Debug)]
1745 struct Over(u64);
1746 let mut a = RefAny::new(Over(0xABCD));
1747 let r = a.downcast_ref::<Over>().unwrap();
1748 assert_eq!(r.0, 0xABCD);
1749 assert_eq!((&raw const *r) as usize % 16, 0);
1750 }
1751
1752 // MIRI: clone shares one RefCountInner; num_copies increments on clone and
1753 // decrements on drop (RefCount::clone / RefCount::drop fetch paths). Data
1754 // must survive while any clone lives and be freed exactly once at the end.
1755 #[test]
1756 fn miri_clone_refcount_increment_decrement() {
1757 let _serial = serialize_drop_count();
1758 DROP_COUNT.store(0, Ordering::SeqCst);
1759 {
1760 let a = RefAny::new(DropCounter(1));
1761 assert_eq!(a.get_ref_count(), 1);
1762 let b = a.clone();
1763 assert_eq!(a.get_ref_count(), 2);
1764 assert_eq!(b.get_ref_count(), 2);
1765 {
1766 let c = b.clone();
1767 assert_eq!(c.get_ref_count(), 3);
1768 }
1769 // c dropped -> back to 2, nothing freed yet.
1770 assert_eq!(a.get_ref_count(), 2);
1771 assert_eq!(DROP_COUNT.load(Ordering::SeqCst), 0);
1772 }
1773 // all clones dropped -> data destructed exactly once.
1774 assert_eq!(DROP_COUNT.load(Ordering::SeqCst), 1);
1775 }
1776
1777 // MIRI: downcast_mut hands out &mut *(ptr as *mut U); mutation must be
1778 // visible through a shared clone (shared RefCountInner data pointer).
1779 #[test]
1780 fn miri_downcast_mut_mutation_visible_across_clones() {
1781 let mut a = RefAny::new(10u32);
1782 let mut b = a.clone();
1783 {
1784 let mut m = a.downcast_mut::<u32>().unwrap();
1785 *m += 5;
1786 }
1787 assert_eq!(*b.downcast_ref::<u32>().unwrap(), 15);
1788 }
1789
1790 // MIRI: the runtime borrow refcount on the shared inner. Exercises
1791 // increase_ref/decrease_ref/increase_refmut/decrease_refmut and the
1792 // can_be_shared / can_be_shared_mut predicates directly, plus the
1793 // checked_sub underflow guard (decrement at zero must saturate, not wrap).
1794 #[test]
1795 fn miri_borrow_counter_transitions_and_underflow_guard() {
1796 let a = RefAny::new(0i32);
1797 let rc = &a.sharing_info;
1798
1799 assert!(rc.can_be_shared());
1800 assert!(rc.can_be_shared_mut());
1801
1802 rc.increase_ref();
1803 assert!(rc.can_be_shared()); // shared borrows coexist
1804 assert!(!rc.can_be_shared_mut()); // but block a mutable borrow
1805 rc.decrease_ref();
1806 assert!(rc.can_be_shared_mut());
1807
1808 rc.increase_refmut();
1809 assert!(!rc.can_be_shared()); // mutable borrow blocks shared
1810 assert!(!rc.can_be_shared_mut());
1811 rc.decrease_refmut();
1812 assert!(rc.can_be_shared_mut());
1813
1814 // Underflow guard: extra decrements must saturate at 0, never wrap to
1815 // usize::MAX (which would permanently break the borrow checker).
1816 rc.decrease_ref();
1817 rc.decrease_refmut();
1818 assert!(rc.can_be_shared());
1819 assert!(rc.can_be_shared_mut());
1820 }
1821
1822 // MIRI: get_type_id_static reads TypeId via from_raw_parts and folds ALL
1823 // bytes. Same type -> same id (stable within a run); distinct types differ.
1824 #[test]
1825 fn miri_type_id_static_stable_and_distinct() {
1826 assert_eq!(
1827 RefAny::get_type_id_static::<(u8, u64)>(),
1828 RefAny::get_type_id_static::<(u8, u64)>()
1829 );
1830 assert_ne!(
1831 RefAny::get_type_id_static::<u32>(),
1832 RefAny::get_type_id_static::<[u32; 2]>()
1833 );
1834 }
1835
1836 // MIRI: ZST payload uses a null data pointer but must still construct, clone,
1837 // run its destructor exactly once, and downcast (a ZST reference reads no
1838 // bytes, so a dangling-but-aligned pointer is a valid reference).
1839 #[test]
1840 fn miri_zst_roundtrip_and_destructor() {
1841 let _serial = serialize_drop_count();
1842 DROP_COUNT.store(0, Ordering::SeqCst);
1843 struct ZstDrop;
1844 impl Drop for ZstDrop {
1845 fn drop(&mut self) {
1846 DROP_COUNT.fetch_add(1, Ordering::SeqCst);
1847 }
1848 }
1849 {
1850 let mut a = RefAny::new(ZstDrop);
1851 assert_eq!(a.get_data_len(), 0);
1852 // downcast_ref succeeds for a ZST (dangling ref, no bytes read); the
1853 // returned guard drops here without running the value's destructor.
1854 assert!(a.downcast_ref::<ZstDrop>().is_some());
1855 let _b = a.clone();
1856 }
1857 assert_eq!(DROP_COUNT.load(Ordering::SeqCst), 1);
1858 }
1859
1860 // MIRI: replace_contents alloc/dealloc/copy path plus the neutralized
1861 // new_value destructor. Old value destructed once, new value destructed
1862 // once at final drop, with no leak/double-free of either heap block.
1863 #[test]
1864 fn miri_replace_contents_alloc_paths() {
1865 let _serial = serialize_drop_count();
1866 DROP_COUNT.store(0, Ordering::SeqCst);
1867 {
1868 let mut a = RefAny::new(DropCounter(1));
1869 assert!(a.replace_contents(RefAny::new(DropCounter(2))));
1870 assert_eq!(DROP_COUNT.load(Ordering::SeqCst), 1); // old value gone
1871 assert_eq!(a.downcast_ref::<DropCounter>().unwrap().0, 2u32);
1872 }
1873 assert_eq!(DROP_COUNT.load(Ordering::SeqCst), 2);
1874 }
1875
1876 // MIRI: replacing across differing sizes/alignments (u8 -> u64) reallocates
1877 // correctly and keeps the shared pointer aligned for the new type.
1878 #[test]
1879 fn miri_replace_contents_changes_layout() {
1880 let mut a = RefAny::new(7u8);
1881 assert!(a.replace_contents(RefAny::new(0x1122_3344_5566_7788u64)));
1882 {
1883 // downcast_ref takes &mut self, so scope the guard before the next call.
1884 let r = a.downcast_ref::<u64>().unwrap();
1885 assert_eq!(*r, 0x1122_3344_5566_7788u64);
1886 assert_eq!((&raw const *r) as usize % core::mem::align_of::<u64>(), 0);
1887 }
1888 // old u8 type must no longer downcast.
1889 assert!(a.downcast_ref::<u8>().is_none());
1890 }
1891
1892 // MIRI: RefCount clone/drop in isolation keeps the inner alive until the
1893 // last handle drops (Box::into_raw / Box::from_raw balance).
1894 #[test]
1895 fn miri_refcount_clone_keeps_inner_alive() {
1896 let a = RefAny::new(5usize);
1897 let rc0 = a.sharing_info.clone(); // +1 copy
1898 let rc1 = rc0.clone(); // +1 copy
1899 assert_eq!(a.get_ref_count(), 3);
1900 drop(rc1);
1901 drop(rc0);
1902 assert_eq!(a.get_ref_count(), 1);
1903 // `a` still usable -> inner not freed.
1904 assert_eq!(*a.clone().downcast_ref::<usize>().unwrap(), 5);
1905 }
1906}
1907
1908#[cfg(test)]
1909#[allow(
1910 clippy::items_after_statements,
1911 clippy::redundant_clone,
1912 clippy::needless_pass_by_value,
1913 clippy::needless_range_loop,
1914 clippy::cast_possible_truncation,
1915 clippy::cast_sign_loss,
1916 clippy::cast_lossless,
1917 clippy::float_cmp,
1918 clippy::unreadable_literal,
1919 clippy::unusual_byte_groupings,
1920 clippy::many_single_char_names,
1921 clippy::used_underscore_binding,
1922 clippy::borrow_as_ptr,
1923 clippy::cast_ptr_alignment,
1924 clippy::fn_to_numeric_cast_any,
1925 trivial_casts,
1926 unused_qualifications,
1927 unreachable_pub,
1928 private_interfaces,
1929 missing_debug_implementations,
1930 missing_copy_implementations
1931)] // pedantic lints are noise in unsafe-exercising test code
1932mod autotest_generated {
1933 use alloc::{string::String, vec::Vec};
1934 use core::{
1935 ffi::c_void,
1936 sync::atomic::{AtomicUsize, Ordering},
1937 };
1938
1939 use super::*;
1940
1941 /// Destructor for payloads that need no drop glue (`Copy` types built via
1942 /// the raw C-ABI `new_c` path).
1943 extern "C" fn noop_destructor(_: *mut c_void) {}
1944
1945 /// Store `value` in a `RefAny` and read it back out: the byte-copy into the
1946 /// heap allocation and the type-checked pointer cast must be lossless.
1947 fn round_trip<T: 'static + Clone + PartialEq + core::fmt::Debug>(value: T) {
1948 let mut a = RefAny::new(value.clone());
1949 let r = a
1950 .downcast_ref::<T>()
1951 .expect("downcast to the stored type must succeed");
1952 assert_eq!(*r, value);
1953 }
1954
1955 // ---- RefAny::new_c — raw C-ABI constructor, malformed/boundary inputs ----
1956
1957 // A NULL pointer with a non-zero length is the classic FFI mistake: copying
1958 // from it would be UB, so `new_c` must panic instead of reading it.
1959 #[test]
1960 #[should_panic(expected = "NULL pointer passed for non-ZST type")]
1961 fn new_c_null_ptr_with_nonzero_len_panics() {
1962 drop(RefAny::new_c(
1963 core::ptr::null(),
1964 4,
1965 4,
1966 RefAny::get_type_id_static::<u32>(),
1967 AzString::from_const_str("autotest::NullPtr"),
1968 noop_destructor,
1969 0,
1970 0,
1971 ));
1972 }
1973
1974 // A non-power-of-two alignment cannot form a valid `Layout`; it must panic
1975 // before allocating rather than allocate with a bogus layout (which would
1976 // make the matching `dealloc` in `drop` UB).
1977 #[test]
1978 #[should_panic(expected = "Failed to create layout")]
1979 fn new_c_non_power_of_two_align_panics() {
1980 let value: u32 = 7;
1981 drop(RefAny::new_c(
1982 (&raw const value).cast::<c_void>(),
1983 4,
1984 3, // not a power of two
1985 RefAny::get_type_id_static::<u32>(),
1986 AzString::from_const_str("autotest::BadAlign"),
1987 noop_destructor,
1988 0,
1989 0,
1990 ));
1991 }
1992
1993 // `usize::MAX` bytes overflows `isize::MAX` and cannot be a `Layout`: the
1994 // checked constructor must reject it (no silent overflow into a tiny alloc).
1995 #[test]
1996 #[should_panic(expected = "Failed to create layout")]
1997 fn new_c_huge_len_panics_instead_of_overflowing() {
1998 let value: u8 = 1;
1999 drop(RefAny::new_c(
2000 (&raw const value).cast::<c_void>(),
2001 usize::MAX,
2002 1,
2003 RefAny::get_type_id_static::<u8>(),
2004 AzString::from_const_str("autotest::HugeLen"),
2005 noop_destructor,
2006 0,
2007 0,
2008 ));
2009 }
2010
2011 // len == 0 is the ZST path: NULL data pointer is legal, `align` is ignored
2012 // (even a nonsensical 0), nothing is allocated, and a ZST still downcasts
2013 // (via a dangling-but-aligned reference — there are no bytes to read).
2014 #[test]
2015 fn new_c_zero_len_null_ptr_is_a_clean_zst() {
2016 let mut a = RefAny::new_c(
2017 core::ptr::null(),
2018 0,
2019 0, // invalid alignment, but unused on the ZST path
2020 RefAny::get_type_id_static::<()>(),
2021 AzString::from_const_str("autotest::Zst"),
2022 noop_destructor,
2023 0,
2024 0,
2025 );
2026 assert_eq!(a.get_data_len(), 0);
2027 assert!(a.get_data_ptr().is_null());
2028 assert!(a.is_type(RefAny::get_type_id_static::<()>()));
2029 // Type matches and a `&()`/`&mut ()` needs no backing bytes, so the
2030 // downcast succeeds; each temporary guard releases its borrow slot when it
2031 // drops at the end of its statement.
2032 assert!(a.downcast_ref::<()>().is_some());
2033 assert!(a.downcast_mut::<()>().is_some());
2034 assert!(a.sharing_info.can_be_shared_mut());
2035 }
2036
2037 // Round-trip through the raw C-ABI constructor: what `new_c` encodes,
2038 // `downcast_ref` must decode bit-for-bit.
2039 #[test]
2040 fn new_c_round_trip_matches_rust_constructor() {
2041 let value: u64 = 0xDEAD_BEEF_CAFE_BABE;
2042 let mut a = RefAny::new_c(
2043 (&raw const value).cast::<c_void>(),
2044 core::mem::size_of::<u64>(),
2045 core::mem::align_of::<u64>(),
2046 RefAny::get_type_id_static::<u64>(),
2047 AzString::from_const_str("u64"),
2048 noop_destructor,
2049 7,
2050 9,
2051 );
2052 assert_eq!(a.get_data_len(), core::mem::size_of::<u64>());
2053 assert_eq!(a.get_ref_count(), 1);
2054 assert_eq!(a.get_serialize_fn(), 7);
2055 assert_eq!(a.get_deserialize_fn(), 9);
2056 assert!(a.can_serialize());
2057 assert!(a.can_deserialize());
2058 assert_eq!(*a.downcast_ref::<u64>().unwrap(), value);
2059 }
2060
2061 // The runtime guard is the type ID, nothing else: a matching size, name and
2062 // destructor must NOT be enough to downcast if the ID differs by one bit.
2063 #[test]
2064 fn new_c_wrong_type_id_rejects_downcast() {
2065 let value: u64 = 0x0102_0304_0506_0708;
2066 let real_id = RefAny::get_type_id_static::<u64>();
2067 let mut a = RefAny::new_c(
2068 (&raw const value).cast::<c_void>(),
2069 core::mem::size_of::<u64>(),
2070 core::mem::align_of::<u64>(),
2071 real_id ^ 1, // one bit off
2072 AzString::from_const_str("u64"),
2073 noop_destructor,
2074 0,
2075 0,
2076 );
2077 assert!(!a.is_type(real_id));
2078 assert!(a.downcast_ref::<u64>().is_none());
2079 assert!(a.downcast_mut::<u64>().is_none());
2080 // The rejected downcasts must not have left a borrow behind.
2081 assert!(a.sharing_info.can_be_shared_mut());
2082 }
2083
2084 // Over-alignment (align > len) is a valid `Layout`; the payload must land on
2085 // an address that satisfies the requested alignment.
2086 #[test]
2087 fn new_c_over_aligned_small_payload() {
2088 let value: u8 = 0x5A;
2089 let mut a = RefAny::new_c(
2090 (&raw const value).cast::<c_void>(),
2091 1,
2092 16,
2093 RefAny::get_type_id_static::<u8>(),
2094 AzString::from_const_str("u8"),
2095 noop_destructor,
2096 0,
2097 0,
2098 );
2099 assert_eq!(a.get_data_ptr() as usize % 16, 0);
2100 assert_eq!(*a.downcast_ref::<u8>().unwrap(), 0x5A);
2101 }
2102
2103 // The type name is arbitrary caller-supplied UTF-8 (generated by foreign
2104 // codegen): empty, unicode, RTL overrides and embedded NULs must survive.
2105 #[test]
2106 fn new_c_preserves_unicode_and_empty_type_names() {
2107 let value: u32 = 0;
2108 let weird = "app::💥Ünïcødé<T>\u{202E}rtl\u{0}nul";
2109 let a = RefAny::new_c(
2110 (&raw const value).cast::<c_void>(),
2111 4,
2112 4,
2113 1,
2114 AzString::from(String::from(weird)),
2115 noop_destructor,
2116 0,
2117 0,
2118 );
2119 assert_eq!(a.get_type_name().as_str(), weird);
2120
2121 let b = RefAny::new_c(
2122 (&raw const value).cast::<c_void>(),
2123 4,
2124 4,
2125 2,
2126 AzString::from_const_str(""),
2127 noop_destructor,
2128 0,
2129 0,
2130 );
2131 assert_eq!(b.get_type_name().as_str(), "");
2132 }
2133
2134 // ---- RefAny::new — post-construction invariants ----
2135
2136 #[test]
2137 fn new_invariants_hold() {
2138 let mut a = RefAny::new(0x1122_3344u32);
2139 assert_eq!(a.get_data_len(), core::mem::size_of::<u32>());
2140 assert!(!a.get_data_ptr().is_null());
2141 assert_eq!(a.get_data_ptr() as usize % core::mem::align_of::<u32>(), 0);
2142 assert_eq!(a.get_type_id(), RefAny::get_type_id_static::<u32>());
2143 assert!(a.is_type(RefAny::get_type_id_static::<u32>()));
2144 assert_eq!(a.get_type_name().as_str(), "u32");
2145 assert_eq!(a.get_ref_count(), 1);
2146 assert!(a.has_no_copies());
2147 assert_eq!(a.get_serialize_fn(), 0);
2148 assert_eq!(a.get_deserialize_fn(), 0);
2149 assert_eq!(a.get_update_fn(), 0);
2150 assert!(!a.can_serialize());
2151 assert!(!a.can_deserialize());
2152 assert!(a.sharing_info.can_be_shared());
2153 assert!(a.sharing_info.can_be_shared_mut());
2154 assert_eq!(a.instance_id, 0);
2155 assert_eq!(*a.downcast_ref::<u32>().unwrap(), 0x1122_3344);
2156 }
2157
2158 // A zero-length array of an 8-aligned element is still a ZST: `new` must take
2159 // the null-pointer path (no zero-size allocation, which would be UB).
2160 #[test]
2161 fn new_zero_sized_array_of_aligned_type_is_a_zst() {
2162 let mut a = RefAny::new([0u64; 0]);
2163 assert_eq!(a.get_data_len(), 0);
2164 assert!(a.get_data_ptr().is_null());
2165 assert_eq!(
2166 a.sharing_info.debug_get_refcount_copied()._internal_layout_size,
2167 0
2168 );
2169 assert!(a.downcast_ref::<[u64; 0]>().is_some());
2170 assert_eq!(a.get_ref_count(), 1);
2171 }
2172
2173 // Large + heavily over-aligned payload: the alignment recorded at
2174 // construction must be honoured by the allocation, or every downcast would
2175 // hand out a misaligned reference.
2176 #[test]
2177 fn new_large_over_aligned_payload_round_trips() {
2178 #[repr(align(64))]
2179 #[derive(Clone)]
2180 struct Big([u8; 4096]);
2181
2182 let mut a = RefAny::new(Big([0xAB; 4096]));
2183 assert_eq!(a.get_data_len(), 4096);
2184 assert_eq!(a.get_data_ptr() as usize % 64, 0);
2185 let r = a.downcast_ref::<Big>().unwrap();
2186 assert_eq!((&raw const *r) as usize % 64, 0);
2187 assert!(r.0.iter().all(|&b| b == 0xAB));
2188 }
2189
2190 // ---- numeric limits / round-trip ----
2191
2192 #[test]
2193 fn integer_limits_round_trip() {
2194 round_trip(u8::MIN);
2195 round_trip(u8::MAX);
2196 round_trip(i8::MIN);
2197 round_trip(i8::MAX);
2198 round_trip(u16::MAX);
2199 round_trip(i16::MIN);
2200 round_trip(u32::MAX);
2201 round_trip(i32::MIN);
2202 round_trip(u64::MAX);
2203 round_trip(i64::MIN);
2204 // u128/i128 are 16-aligned on most targets -> exercises the align path
2205 round_trip(u128::MAX);
2206 round_trip(i128::MIN);
2207 round_trip(i128::MAX);
2208 round_trip(usize::MAX);
2209 round_trip(isize::MIN);
2210 round_trip(0usize);
2211 }
2212
2213 // Floats are copied as raw bytes, so every bit pattern (NaN payloads, signed
2214 // zero, infinities) must survive unchanged — no normalization, no rounding.
2215 #[test]
2216 fn float_extremes_round_trip_bit_exact() {
2217 let mut nan = RefAny::new(f64::NAN);
2218 assert!(nan.downcast_ref::<f64>().unwrap().is_nan());
2219
2220 // A NaN with a non-canonical payload must come back bit-identical.
2221 let bits = 0x7FF0_0000_0000_0001u64;
2222 let mut payload_nan = RefAny::new(f64::from_bits(bits));
2223 assert_eq!(payload_nan.downcast_ref::<f64>().unwrap().to_bits(), bits);
2224
2225 let mut neg_zero = RefAny::new(-0.0f64);
2226 let nz = neg_zero.downcast_ref::<f64>().unwrap();
2227 assert!(*nz == 0.0 && nz.is_sign_negative());
2228 drop(nz);
2229
2230 let mut inf = RefAny::new(f32::NEG_INFINITY);
2231 assert_eq!(*inf.downcast_ref::<f32>().unwrap(), f32::NEG_INFINITY);
2232 // f32 and f64 are distinct types even though both are "floats".
2233 assert!(inf.downcast_ref::<f64>().is_none());
2234
2235 round_trip(f64::MIN);
2236 round_trip(f64::MAX);
2237 round_trip(f64::MIN_POSITIVE);
2238 round_trip(f32::EPSILON);
2239 round_trip(f32::MAX);
2240 }
2241
2242 // Owned heap payloads: the value is moved in (`mem::forget` on the original)
2243 // and dropped exactly once at the end — a double-drop here would be a
2244 // double-free of the String/Vec buffers.
2245 #[test]
2246 fn owned_unicode_payloads_round_trip() {
2247 round_trip(String::new());
2248 round_trip(String::from("héllo 🌍 \u{202E}rtl\u{0}nul"));
2249 round_trip('🌍');
2250
2251 let v: Vec<String> = vec![String::from("a"), String::from("🎉"), String::new()];
2252 round_trip(v);
2253 }
2254
2255 // A struct with interior padding is byte-copied, padding included: the copy
2256 // must not disturb the initialized fields.
2257 #[test]
2258 fn padded_struct_round_trips() {
2259 #[derive(Clone, PartialEq, Debug)]
2260 #[repr(C)]
2261 struct Padded {
2262 a: u8,
2263 b: u64,
2264 c: u8,
2265 }
2266 round_trip(Padded {
2267 a: 0xFF,
2268 b: u64::MAX,
2269 c: 0x01,
2270 });
2271 }
2272
2273 // ---- setters: 0 / 1 / usize::MAX (never dereferenced by azul-core) ----
2274
2275 #[test]
2276 fn set_serialize_fn_zero_and_extremes() {
2277 let mut a = RefAny::new(1u32);
2278 assert_eq!(a.get_serialize_fn(), 0);
2279 assert!(!a.can_serialize());
2280
2281 a.set_serialize_fn(usize::MAX);
2282 assert_eq!(a.get_serialize_fn(), usize::MAX);
2283 assert!(a.can_serialize());
2284
2285 a.set_serialize_fn(1);
2286 assert_eq!(a.get_serialize_fn(), 1);
2287 assert!(a.can_serialize());
2288
2289 a.set_serialize_fn(0);
2290 assert_eq!(a.get_serialize_fn(), 0);
2291 assert!(!a.can_serialize());
2292
2293 // The fn pointer lives in the SHARED inner, so a clone's setter is
2294 // visible through the original.
2295 let mut b = a.clone();
2296 b.set_serialize_fn(42);
2297 assert_eq!(a.get_serialize_fn(), 42);
2298 assert!(a.can_serialize());
2299 b.set_serialize_fn(0);
2300 assert!(!a.can_serialize());
2301 }
2302
2303 #[test]
2304 fn set_deserialize_fn_zero_and_extremes() {
2305 let mut a = RefAny::new(1u32);
2306 assert_eq!(a.get_deserialize_fn(), 0);
2307 assert!(!a.can_deserialize());
2308
2309 a.set_deserialize_fn(usize::MAX);
2310 assert_eq!(a.get_deserialize_fn(), usize::MAX);
2311 assert!(a.can_deserialize());
2312
2313 a.set_deserialize_fn(1);
2314 assert_eq!(a.get_deserialize_fn(), 1);
2315
2316 a.set_deserialize_fn(0);
2317 assert_eq!(a.get_deserialize_fn(), 0);
2318 assert!(!a.can_deserialize());
2319
2320 let mut b = a.clone();
2321 b.set_deserialize_fn(42);
2322 assert_eq!(a.get_deserialize_fn(), 42);
2323 b.set_deserialize_fn(0);
2324 assert!(!a.can_deserialize());
2325 }
2326
2327 // `set_update_fn` only *stores* the address; a bogus value must round-trip
2328 // and must be resettable to 0. (Deliberately no `downcast_mut` while the
2329 // observer is bogus — `downcast_mut` transmutes and CALLS it.)
2330 #[test]
2331 fn set_update_fn_zero_and_extremes() {
2332 let mut a = RefAny::new(1u32);
2333 assert_eq!(a.get_update_fn(), 0);
2334
2335 a.set_update_fn(usize::MAX);
2336 assert_eq!(a.get_update_fn(), usize::MAX);
2337
2338 a.set_update_fn(0);
2339 assert_eq!(a.get_update_fn(), 0);
2340 // With the observer unset again, mutable borrows work as normal.
2341 assert!(a.downcast_mut::<u32>().is_some());
2342 }
2343
2344 // The registered observer must fire exactly once per *successful*
2345 // `downcast_mut`, and must see the PRE-mutation bytes + the payload length.
2346 static UPDATE_CALLS: AtomicUsize = AtomicUsize::new(0);
2347 static UPDATE_LEN: AtomicUsize = AtomicUsize::new(0);
2348 static UPDATE_PRE_VALUE: AtomicUsize = AtomicUsize::new(0);
2349
2350 extern "C" fn record_update(ptr: *const c_void, len: usize) {
2351 UPDATE_CALLS.fetch_add(1, Ordering::SeqCst);
2352 UPDATE_LEN.store(len, Ordering::SeqCst);
2353 if !ptr.is_null() && len == core::mem::size_of::<u32>() {
2354 // SAFETY: only installed on a `RefAny` holding a `u32`, and
2355 // `downcast_mut` fires it with that live payload pointer.
2356 let pre = unsafe { core::ptr::read_unaligned(ptr.cast::<u32>()) };
2357 UPDATE_PRE_VALUE.store(pre as usize, Ordering::SeqCst);
2358 }
2359 }
2360
2361 #[test]
2362 fn update_fn_fires_once_with_pre_mutation_data() {
2363 UPDATE_CALLS.store(0, Ordering::SeqCst);
2364
2365 let mut a = RefAny::new(7u32);
2366 let cb: extern "C" fn(*const c_void, usize) = record_update;
2367 a.set_update_fn(cb as usize);
2368 assert_eq!(a.get_update_fn(), cb as usize);
2369
2370 {
2371 let mut m = a.downcast_mut::<u32>().unwrap();
2372 *m = 9;
2373 }
2374 assert_eq!(UPDATE_CALLS.load(Ordering::SeqCst), 1);
2375 assert_eq!(UPDATE_LEN.load(Ordering::SeqCst), 4);
2376 // The observer saw 7, not 9: it runs BEFORE the borrow is handed out.
2377 assert_eq!(UPDATE_PRE_VALUE.load(Ordering::SeqCst), 7);
2378
2379 // A wrong-type downcast must not fire it.
2380 assert!(a.downcast_mut::<u64>().is_none());
2381 assert_eq!(UPDATE_CALLS.load(Ordering::SeqCst), 1);
2382
2383 // A shared borrow is not a mutation -> must not fire it.
2384 assert_eq!(*a.downcast_ref::<u32>().unwrap(), 9);
2385 assert_eq!(UPDATE_CALLS.load(Ordering::SeqCst), 1);
2386
2387 // A *denied* mutable borrow (shared borrow live on a sibling clone)
2388 // must not fire it either.
2389 let mut b = a.clone();
2390 let r = a.downcast_ref::<u32>().unwrap();
2391 assert!(b.downcast_mut::<u32>().is_none());
2392 assert_eq!(UPDATE_CALLS.load(Ordering::SeqCst), 1);
2393 drop(r);
2394
2395 // Unregistering stops the observer.
2396 b.set_update_fn(0);
2397 assert!(b.downcast_mut::<u32>().is_some());
2398 assert_eq!(UPDATE_CALLS.load(Ordering::SeqCst), 1);
2399 }
2400
2401 // ---- predicates ----
2402
2403 #[test]
2404 fn is_type_true_false_and_extremes() {
2405 let a = RefAny::new(0u32);
2406 let id = a.get_type_id();
2407
2408 assert!(a.is_type(id));
2409 assert!(!a.is_type(!id)); // every bit flipped -> always a different id
2410 assert!(!a.is_type(id.wrapping_add(1)));
2411 assert!(!a.is_type(RefAny::get_type_id_static::<i32>()));
2412 if id != 0 {
2413 assert!(!a.is_type(0));
2414 }
2415 if id != u64::MAX {
2416 assert!(!a.is_type(u64::MAX));
2417 }
2418 }
2419
2420 #[test]
2421 fn has_no_copies_transitions() {
2422 let mut a = RefAny::new(1u32);
2423 assert!(a.has_no_copies());
2424
2425 {
2426 let b = a.clone();
2427 assert!(!a.has_no_copies()); // num_copies == 2
2428 assert!(!b.has_no_copies());
2429 }
2430 assert!(a.has_no_copies()); // clone dropped -> exclusive again
2431
2432 {
2433 // A live shared borrow (taken via a sibling clone) also disqualifies.
2434 let mut c = a.clone();
2435 let r = c.downcast_ref::<u32>().unwrap();
2436 assert_eq!(*r, 1);
2437 assert!(!a.has_no_copies());
2438 }
2439 assert!(a.has_no_copies());
2440
2441 {
2442 let mut c = a.clone();
2443 let m = c.downcast_mut::<u32>().unwrap();
2444 assert_eq!(*m, 1);
2445 assert!(!a.has_no_copies());
2446 }
2447 assert!(a.has_no_copies());
2448 }
2449
2450 #[test]
2451 fn can_serialize_and_can_deserialize_track_the_fn_pointers() {
2452 let mut a = RefAny::new(1u32);
2453 assert!(!a.can_serialize());
2454 assert!(!a.can_deserialize());
2455
2456 a.set_serialize_fn(1);
2457 assert!(a.can_serialize());
2458 assert!(!a.can_deserialize());
2459
2460 a.set_deserialize_fn(usize::MAX);
2461 assert!(a.can_serialize());
2462 assert!(a.can_deserialize());
2463
2464 a.set_serialize_fn(0);
2465 a.set_deserialize_fn(0);
2466 assert!(!a.can_serialize());
2467 assert!(!a.can_deserialize());
2468 }
2469
2470 // ---- getters ----
2471
2472 #[test]
2473 fn get_ref_count_tracks_clones_and_borrow_guards() {
2474 let mut a = RefAny::new(5u8);
2475 assert_eq!(a.get_ref_count(), 1);
2476
2477 let mut b = a.clone();
2478 assert_eq!(a.get_ref_count(), 2);
2479 assert_eq!(b.get_ref_count(), 2);
2480
2481 {
2482 // The guard clones the RefCount, so it keeps the data alive.
2483 let r = b.downcast_ref::<u8>().unwrap();
2484 assert_eq!(*r, 5);
2485 assert_eq!(a.get_ref_count(), 3);
2486 }
2487 assert_eq!(a.get_ref_count(), 2);
2488
2489 {
2490 let m = b.downcast_mut::<u8>().unwrap();
2491 assert_eq!(*m, 5);
2492 assert_eq!(a.get_ref_count(), 3);
2493 }
2494 assert_eq!(a.get_ref_count(), 2);
2495
2496 drop(b);
2497 assert_eq!(a.get_ref_count(), 1);
2498 assert_eq!(*a.downcast_ref::<u8>().unwrap(), 5);
2499 }
2500
2501 #[test]
2502 fn debug_snapshot_matches_the_live_counters() {
2503 let a = RefAny::new(0x1122_3344u32);
2504 let d = a.sharing_info.debug_get_refcount_copied();
2505 assert_eq!(d.num_copies, 1);
2506 assert_eq!(d.num_refs, 0);
2507 assert_eq!(d.num_mutable_refs, 0);
2508 assert_eq!(d._internal_len, 4);
2509 assert_eq!(d._internal_layout_size, 4);
2510 assert_eq!(d._internal_layout_align, core::mem::align_of::<u32>());
2511 assert_eq!(d.type_id, RefAny::get_type_id_static::<u32>());
2512 assert_eq!(d.type_name.as_str(), "u32");
2513 assert_ne!(d.custom_destructor, 0);
2514 assert_eq!(d.serialize_fn, 0);
2515 assert_eq!(d.deserialize_fn, 0);
2516
2517 a.sharing_info.increase_ref();
2518 a.sharing_info.increase_refmut();
2519 let d2 = a.sharing_info.debug_get_refcount_copied();
2520 assert_eq!(d2.num_refs, 1);
2521 assert_eq!(d2.num_mutable_refs, 1);
2522 // The first snapshot is a copy, not a view: it must not have changed.
2523 assert_eq!(d.num_refs, 0);
2524
2525 a.sharing_info.decrease_ref();
2526 a.sharing_info.decrease_refmut();
2527 let d3 = a.sharing_info.debug_get_refcount_copied();
2528 assert_eq!((d3.num_refs, d3.num_mutable_refs), (0, 0));
2529
2530 // The Debug impl goes through `downcast()` — it must not panic.
2531 assert!(!alloc::format!("{:?}", a.sharing_info).is_empty());
2532 }
2533
2534 #[test]
2535 fn get_type_name_reports_the_rust_type() {
2536 #[derive(Clone)]
2537 struct AutotestNamed(#[allow(dead_code)] u8);
2538
2539 let a = RefAny::new(AutotestNamed(1));
2540 let name = a.get_type_name();
2541 assert!(
2542 name.as_str().contains("AutotestNamed"),
2543 "unexpected type name: {}",
2544 name.as_str()
2545 );
2546
2547 let generic = RefAny::new(Vec::<String>::new());
2548 assert!(generic.get_type_name().as_str().contains("Vec"));
2549
2550 assert_eq!(RefAny::new(1u32).get_type_name().as_str(), "u32");
2551 }
2552
2553 // ---- RefCount: construction, downcast, clone/drop balance ----
2554
2555 #[test]
2556 fn refcount_new_downcast_and_clone_lifecycle() {
2557 let rc = RefCount::new(RefCountInner {
2558 _internal_ptr: core::ptr::null(),
2559 num_copies: AtomicUsize::new(1),
2560 num_refs: AtomicUsize::new(0),
2561 num_mutable_refs: AtomicUsize::new(0),
2562 _internal_len: 0,
2563 _internal_layout_size: 0,
2564 _internal_layout_align: 1,
2565 type_id: 0xDEAD_BEEF,
2566 type_name: AzString::from_const_str("autotest::Synthetic"),
2567 custom_destructor: noop_destructor,
2568 serialize_fn: 0,
2569 deserialize_fn: 0,
2570 update_fn: 0,
2571 });
2572 assert!(!rc.ptr.is_null());
2573 assert!(rc.run_destructor);
2574
2575 let inner = rc.downcast();
2576 assert_eq!(inner.type_id, 0xDEAD_BEEF);
2577 assert_eq!(inner.type_name.as_str(), "autotest::Synthetic");
2578 assert_eq!(inner._internal_len, 0);
2579 assert!(rc.can_be_shared());
2580 assert!(rc.can_be_shared_mut());
2581
2582 // Clones must keep the boxed inner alive; the counters must return to 1
2583 // so the final drop frees it exactly once.
2584 let c1 = rc.clone();
2585 assert_eq!(rc.debug_get_refcount_copied().num_copies, 2);
2586 let c2 = c1.clone();
2587 assert_eq!(rc.debug_get_refcount_copied().num_copies, 3);
2588 drop(c2);
2589 drop(c1);
2590 assert_eq!(rc.debug_get_refcount_copied().num_copies, 1);
2591 }
2592
2593 // The borrow counters must saturate at 0 instead of wrapping to usize::MAX
2594 // (an unmatched `FooRef_delete` from C would otherwise permanently wedge the
2595 // runtime borrow checker), and stay usable afterwards.
2596 #[test]
2597 fn borrow_counters_saturate_at_zero_and_stay_usable() {
2598 let mut a = RefAny::new(3i64);
2599 {
2600 let rc = &a.sharing_info;
2601
2602 // 64 unmatched decrements on both counters.
2603 for _ in 0..64 {
2604 rc.decrease_ref();
2605 rc.decrease_refmut();
2606 }
2607 let d = rc.debug_get_refcount_copied();
2608 assert_eq!(d.num_refs, 0);
2609 assert_eq!(d.num_mutable_refs, 0);
2610 assert!(rc.can_be_shared());
2611 assert!(rc.can_be_shared_mut());
2612
2613 // Many shared borrows coexist, but block a mutable one.
2614 for _ in 0..256 {
2615 rc.increase_ref();
2616 }
2617 assert_eq!(rc.debug_get_refcount_copied().num_refs, 256);
2618 assert!(rc.can_be_shared());
2619 assert!(!rc.can_be_shared_mut());
2620 for _ in 0..256 {
2621 rc.decrease_ref();
2622 }
2623 assert_eq!(rc.debug_get_refcount_copied().num_refs, 0);
2624 assert!(rc.can_be_shared_mut());
2625
2626 // Same for the mutable counter, plus one extra decrement.
2627 rc.increase_refmut();
2628 rc.increase_refmut();
2629 assert!(!rc.can_be_shared());
2630 rc.decrease_refmut();
2631 rc.decrease_refmut();
2632 rc.decrease_refmut();
2633 assert_eq!(rc.debug_get_refcount_copied().num_mutable_refs, 0);
2634 }
2635
2636 // The borrow checker still works after all those underflow attempts.
2637 assert_eq!(*a.downcast_ref::<i64>().unwrap(), 3);
2638 assert!(a.downcast_mut::<i64>().is_some());
2639 }
2640
2641 // ---- get_type_id_static ----
2642
2643 // The u64 type ID is the ONLY runtime guard against a wrong-type downcast,
2644 // so distinct types must not collide (this is what folding ALL TypeId bytes
2645 // buys us) and it must be stable within a process run.
2646 #[test]
2647 fn type_id_static_is_stable_and_collision_free() {
2648 let ids = [
2649 RefAny::get_type_id_static::<u8>(),
2650 RefAny::get_type_id_static::<u16>(),
2651 RefAny::get_type_id_static::<u32>(),
2652 RefAny::get_type_id_static::<u64>(),
2653 RefAny::get_type_id_static::<u128>(),
2654 RefAny::get_type_id_static::<usize>(),
2655 RefAny::get_type_id_static::<i8>(),
2656 RefAny::get_type_id_static::<i16>(),
2657 RefAny::get_type_id_static::<i32>(),
2658 RefAny::get_type_id_static::<i64>(),
2659 RefAny::get_type_id_static::<i128>(),
2660 RefAny::get_type_id_static::<isize>(),
2661 RefAny::get_type_id_static::<f32>(),
2662 RefAny::get_type_id_static::<f64>(),
2663 RefAny::get_type_id_static::<bool>(),
2664 RefAny::get_type_id_static::<char>(),
2665 RefAny::get_type_id_static::<()>(),
2666 RefAny::get_type_id_static::<String>(),
2667 RefAny::get_type_id_static::<Vec<u8>>(),
2668 RefAny::get_type_id_static::<Vec<u16>>(),
2669 RefAny::get_type_id_static::<[u8; 1]>(),
2670 RefAny::get_type_id_static::<[u8; 2]>(),
2671 RefAny::get_type_id_static::<(u8, u8)>(),
2672 RefAny::get_type_id_static::<(u8, u16)>(),
2673 RefAny::get_type_id_static::<Option<u8>>(),
2674 RefAny::get_type_id_static::<Option<u16>>(),
2675 ];
2676
2677 for i in 0..ids.len() {
2678 for j in (i + 1)..ids.len() {
2679 assert_ne!(ids[i], ids[j], "type id collision between {i} and {j}");
2680 }
2681 }
2682
2683 // Deterministic within a run.
2684 assert_eq!(RefAny::get_type_id_static::<Vec<u8>>(), ids[18]);
2685 assert_eq!(RefAny::get_type_id_static::<u8>(), ids[0]);
2686 }
2687
2688 // ---- clone / instance ids ----
2689
2690 #[test]
2691 fn root_instance_id_is_zero_and_clones_are_distinct() {
2692 let a = RefAny::new(0u8);
2693 assert_eq!(a.instance_id, 0);
2694
2695 let b = a.clone();
2696 let c = b.clone();
2697 assert_ne!(b.instance_id, 0);
2698 assert_ne!(c.instance_id, 0);
2699 assert_ne!(b.instance_id, c.instance_id);
2700 assert_eq!(a.get_ref_count(), 3);
2701 }
2702
2703 // ---- replace_contents ----
2704
2705 #[test]
2706 fn replace_contents_zst_and_value_transitions() {
2707 #[derive(Clone)]
2708 struct Zst;
2709
2710 let mut a = RefAny::new(Zst);
2711 assert_eq!(a.get_data_len(), 0);
2712 assert!(a.get_data_ptr().is_null());
2713
2714 // ZST -> sized: a real allocation must appear.
2715 assert!(a.replace_contents(RefAny::new(0x4142_4344u32)));
2716 assert_eq!(a.get_data_len(), 4);
2717 assert!(!a.get_data_ptr().is_null());
2718 assert!(a.is_type(RefAny::get_type_id_static::<u32>()));
2719 assert_eq!(*a.downcast_ref::<u32>().unwrap(), 0x4142_4344);
2720
2721 // sized -> ZST: the pointer goes back to null, but the ZST still downcasts
2722 // via a dangling reference; each temporary guard releases its borrow slot
2723 // on drop, so the exclusive slot is free again afterwards.
2724 assert!(a.replace_contents(RefAny::new(Zst)));
2725 assert_eq!(a.get_data_len(), 0);
2726 assert!(a.get_data_ptr().is_null());
2727 assert!(a.downcast_ref::<Zst>().is_some());
2728 assert!(a.downcast_mut::<Zst>().is_some());
2729 assert!(a.sharing_info.can_be_shared_mut());
2730 }
2731
2732 #[test]
2733 fn replace_contents_is_visible_to_all_clones() {
2734 let mut a = RefAny::new(1u32);
2735 let mut b = a.clone();
2736
2737 assert!(a.replace_contents(RefAny::new(2u32)));
2738 assert_eq!(*b.downcast_ref::<u32>().unwrap(), 2);
2739
2740 // The type may change too — every clone sees the new type.
2741 assert!(a.replace_contents(RefAny::new(String::from("swapped"))));
2742 assert!(b.downcast_ref::<u32>().is_none());
2743 assert_eq!(b.downcast_ref::<String>().unwrap().as_str(), "swapped");
2744 assert!(b.get_type_name().as_str().contains("String"));
2745 assert_eq!(b.get_type_id(), RefAny::get_type_id_static::<String>());
2746 }
2747
2748 #[test]
2749 fn replace_contents_denied_while_mutably_borrowed() {
2750 let mut a = RefAny::new(1u32);
2751 let mut b = a.clone();
2752
2753 let m = a.downcast_mut::<u32>().unwrap();
2754 // A live mutable borrow on the shared inner must block the replacement
2755 // (performing it would free memory the `RefMut` still points at).
2756 assert!(!b.replace_contents(RefAny::new(2u32)));
2757 drop(m);
2758
2759 assert!(b.replace_contents(RefAny::new(2u32)));
2760 assert_eq!(*b.downcast_ref::<u32>().unwrap(), 2);
2761 }
2762
2763 // The serialize/deserialize/update hooks are part of the replaced metadata:
2764 // after a replacement they describe the NEW value, not the old one.
2765 #[test]
2766 fn replace_contents_resets_the_fn_pointers_to_the_new_value() {
2767 let mut a = RefAny::new(1u32);
2768 a.set_serialize_fn(3);
2769 a.set_deserialize_fn(4);
2770 assert!(a.can_serialize());
2771 assert!(a.can_deserialize());
2772
2773 assert!(a.replace_contents(RefAny::new(2u32)));
2774 assert_eq!(a.get_serialize_fn(), 0);
2775 assert_eq!(a.get_deserialize_fn(), 0);
2776 assert_eq!(a.get_update_fn(), 0);
2777 assert!(!a.can_serialize());
2778 assert!(!a.can_deserialize());
2779 }
2780
2781 // Repeated replacement across changing sizes/alignments must neither leak nor
2782 // corrupt the payload (Miri checks the alloc/dealloc balance here).
2783 #[test]
2784 fn repeated_replace_contents_stays_consistent() {
2785 let mut a = RefAny::new(String::from("start"));
2786 for i in 0..16u32 {
2787 assert!(a.replace_contents(RefAny::new(i)));
2788 assert_eq!(*a.downcast_ref::<u32>().unwrap(), i);
2789 assert!(a.replace_contents(RefAny::new(u128::from(i) | (1 << 100))));
2790 assert_eq!(
2791 *a.downcast_ref::<u128>().unwrap(),
2792 u128::from(i) | (1 << 100)
2793 );
2794 assert!(a.replace_contents(RefAny::new(String::from("s"))));
2795 }
2796 assert_eq!(a.downcast_ref::<String>().unwrap().as_str(), "s");
2797 }
2798
2799 // ---- destructor robustness / concurrency ----
2800
2801 // `default_custom_destructor` is `extern "C"`: a panic from the payload's
2802 // `Drop` must be caught there, not unwound across the FFI boundary (UB).
2803 #[cfg(feature = "std")]
2804 #[test]
2805 fn panicking_payload_drop_is_contained() {
2806 struct PanicOnDrop(#[allow(dead_code)] u64);
2807 impl Drop for PanicOnDrop {
2808 fn drop(&mut self) {
2809 panic!("autotest: payload Drop panicked (expected, must be contained)");
2810 }
2811 }
2812
2813 let a = RefAny::new(PanicOnDrop(1));
2814 drop(a); // must not propagate the panic out of the extern "C" destructor
2815 }
2816
2817 // RefAny is Send + Sync: concurrent clone/borrow/drop from several threads
2818 // must leave the reference count exactly where it started.
2819 #[cfg(feature = "std")]
2820 #[test]
2821 fn concurrent_clone_and_borrow_keeps_the_refcount_balanced() {
2822 use std::{sync::Arc, thread};
2823
2824 let shared = Arc::new(RefAny::new(11u32));
2825 let mut handles = Vec::new();
2826
2827 for _ in 0..4 {
2828 let s = Arc::clone(&shared);
2829 handles.push(thread::spawn(move || {
2830 for _ in 0..16 {
2831 let mut local = (*s).clone();
2832 // No thread takes a mutable borrow, so a shared borrow can
2833 // never be denied.
2834 let r = local
2835 .downcast_ref::<u32>()
2836 .expect("shared borrow must always succeed here");
2837 assert_eq!(*r, 11);
2838 }
2839 }));
2840 }
2841 for h in handles {
2842 h.join().expect("worker thread panicked");
2843 }
2844
2845 assert_eq!(shared.get_ref_count(), 1);
2846 }
2847}