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
303/// Say ONCE, on stderr, that something borrowed a released `RefAny`.
304///
305/// Returning `None` keeps the process alive, but a callback whose data has been
306/// released silently does nothing, which is its own kind of bug — so the first
307/// occurrence is announced rather than swallowed. Once per process: this can
308/// fire from a hot path (every callback dispatch downcasts), and a per-frame
309/// warning would be its own denial of service. `RUST_BACKTRACE=1` on the run
310/// that prints it names the caller.
311#[cfg(feature = "std")]
312fn report_released_downcast() {
313 use std::sync::atomic::{AtomicBool, Ordering};
314 static SAID: AtomicBool = AtomicBool::new(false);
315 if !SAID.swap(true, Ordering::Relaxed) {
316 eprintln!(
317 "[azul][refany] a RELEASED RefAny was downcast: its RefCount is already freed, so \
318 the borrow returned None and whatever wanted the data did nothing. This is a \
319 use-after-release in the CALLER, not here. Re-run with RUST_BACKTRACE=1 to name \
320 it. (said once per process)"
321 );
322 if std::env::var("RUST_BACKTRACE").is_ok() {
323 eprintln!("{}", std::backtrace::Backtrace::force_capture());
324 }
325 }
326}
327
328#[cfg(not(feature = "std"))]
329const fn report_released_downcast() {}
330
331impl RefCount {
332 /// Creates a new `RefCount` by boxing the metadata on the heap.
333 ///
334 /// # Safety
335 ///
336 /// Safe because we're creating a new allocation with `Box::new`,
337 /// then immediately leaking it with `into_raw` to get a stable pointer.
338 fn new(ref_count: RefCountInner) -> Self {
339 Self {
340 ptr: Box::into_raw(Box::new(ref_count)),
341 run_destructor: true,
342 }
343 }
344
345 /// Has this `RefCount`'s metadata already been released?
346 ///
347 /// A null `ptr` is a REAL state, not corruption: [`Drop`] nulls the field
348 /// after freeing the `RefCountInner`, deliberately mirroring the `ptr = 0`
349 /// convention the `AZ_REFLECT` C macros use, so that a second
350 /// `AzRefCount_delete` of the same C-owned struct is a no-op instead of a
351 /// double free. Anything reachable from SAFE Rust must therefore treat a
352 /// released `RefCount` as "no data", not as a reason to abort — see
353 /// [`RefAny::get_type_id`].
354 pub(crate) const fn is_released(&self) -> bool {
355 self.ptr.is_null()
356 }
357
358 /// Dereferences the raw pointer to access the metadata.
359 ///
360 /// # Safety
361 ///
362 /// Safe because:
363 /// - The pointer is created from `Box::into_raw`, so it's valid and properly aligned
364 /// - The lifetime is tied to `&self`, ensuring the pointer is still alive
365 /// - Reference counting ensures the data isn't freed while references exist
366 fn downcast(&self) -> &RefCountInner {
367 assert!(
368 !self.ptr.is_null(),
369 "[RefCount::downcast] FATAL: self.ptr is null!"
370 );
371 // SAFETY: `ptr` is non-null (asserted) and came from `Box::into_raw`; the
372 // returned reference is bounded by `&self`, and refcounting keeps the
373 // `RefCountInner` alive for at least that long.
374 unsafe { &*self.ptr }
375 }
376
377 /// Creates a debug snapshot of the current reference counts.
378 ///
379 /// Loads all atomic values with `SeqCst` ordering to get a consistent view.
380 #[allow(clippy::used_underscore_binding)] // `_`-prefixed fields are an intentional FFI/api.json naming convention; internal access is required
381 pub(crate) fn debug_get_refcount_copied(&self) -> RefCountInnerDebug {
382 let dc = self.downcast();
383 RefCountInnerDebug {
384 num_copies: dc.num_copies.load(AtomicOrdering::SeqCst),
385 num_refs: dc.num_refs.load(AtomicOrdering::SeqCst),
386 num_mutable_refs: dc.num_mutable_refs.load(AtomicOrdering::SeqCst),
387 _internal_len: dc._internal_len,
388 _internal_layout_size: dc._internal_layout_size,
389 _internal_layout_align: dc._internal_layout_align,
390 type_id: dc.type_id,
391 type_name: dc.type_name.clone(),
392 custom_destructor: dc.custom_destructor as usize,
393 serialize_fn: dc.serialize_fn,
394 deserialize_fn: dc.deserialize_fn,
395 }
396 }
397
398 /// Runtime check: can we create a shared borrow?
399 ///
400 /// Returns `true` if there are no active mutable borrows.
401 /// Multiple shared borrows can coexist (like `&T` in Rust).
402 ///
403 /// # Memory Ordering
404 ///
405 /// Uses `SeqCst` to ensure we see the most recent state from all threads.
406 /// If another thread just released a mutable borrow, we'll see it.
407 #[must_use]
408 pub fn can_be_shared(&self) -> bool {
409 self.downcast()
410 .num_mutable_refs
411 .load(AtomicOrdering::SeqCst)
412 == 0
413 }
414
415 /// Runtime check: can we create a mutable borrow?
416 ///
417 /// Returns `true` only if there are ZERO active borrows of any kind.
418 /// This enforces Rust's exclusive mutability rule (like `&mut T`).
419 ///
420 /// # Memory Ordering
421 ///
422 /// Uses `SeqCst` to ensure we see all recent borrows from all threads.
423 /// Both counters must be checked atomically to prevent races.
424 #[must_use]
425 pub fn can_be_shared_mut(&self) -> bool {
426 let info = self.downcast();
427 info.num_mutable_refs.load(AtomicOrdering::SeqCst) == 0
428 && info.num_refs.load(AtomicOrdering::SeqCst) == 0
429 }
430
431 /// Increments the shared borrow counter.
432 ///
433 /// Called when a `Ref<T>` is created. The `Ref::drop` will decrement it.
434 ///
435 /// # Memory Ordering
436 ///
437 /// `SeqCst` ensures this increment is visible to all threads before they
438 /// try to acquire a mutable borrow (which checks this counter).
439 pub fn increase_ref(&self) {
440 self.downcast()
441 .num_refs
442 .fetch_add(1, AtomicOrdering::SeqCst);
443 }
444
445 /// Decrements the shared borrow counter.
446 ///
447 /// Called when a `Ref<T>` is dropped, indicating the borrow is released.
448 ///
449 /// # Underflow guard
450 ///
451 /// Saturates at 0: an unmatched decrement — e.g. a C caller running
452 /// `FooRef_delete` after a FAILED downcast with a pre-0.2.1 copy of
453 /// `azul.h` (whose macro did not skip the decrease), or a plain
454 /// double-delete — must not wrap `num_refs` to `usize::MAX`, which
455 /// would make `can_be_shared_mut()` return `false` for the rest of
456 /// the process (callbacks silently stop mutating state).
457 ///
458 /// # Memory Ordering
459 ///
460 /// `SeqCst` ensures this decrement is immediately visible to other threads
461 /// waiting to acquire a mutable borrow.
462 pub fn decrease_ref(&self) {
463 let _ = self.downcast().num_refs.fetch_update(
464 AtomicOrdering::SeqCst,
465 AtomicOrdering::SeqCst,
466 |n| n.checked_sub(1),
467 );
468 }
469
470 /// Increments the mutable borrow counter.
471 ///
472 /// Called when a `RefMut<T>` is created. Should only succeed when this
473 /// counter and `num_refs` are both 0.
474 ///
475 /// # Memory Ordering
476 ///
477 /// `SeqCst` ensures this increment is visible to all other threads,
478 /// blocking them from acquiring any borrow (shared or mutable).
479 pub fn increase_refmut(&self) {
480 self.downcast()
481 .num_mutable_refs
482 .fetch_add(1, AtomicOrdering::SeqCst);
483 }
484
485 /// Decrements the mutable borrow counter.
486 ///
487 /// Called when a `RefMut<T>` is dropped, releasing exclusive access.
488 ///
489 /// # Underflow guard
490 ///
491 /// Saturates at 0 (see [`Self::decrease_ref`]): a double
492 /// `FooRefMut_delete` from C must not wrap `num_mutable_refs`, which
493 /// would corrupt the runtime borrow checker and let a second thread
494 /// or timer callback obtain an aliasing mutable borrow.
495 ///
496 /// # Memory Ordering
497 ///
498 /// `SeqCst` ensures this decrement is immediately visible, allowing
499 /// other threads to acquire borrows.
500 pub fn decrease_refmut(&self) {
501 let _ = self.downcast().num_mutable_refs.fetch_update(
502 AtomicOrdering::SeqCst,
503 AtomicOrdering::SeqCst,
504 |n| n.checked_sub(1),
505 );
506 }
507}
508
509/// RAII guard for a shared borrow of type `T` from a `RefAny`.
510///
511/// Similar to `std::cell::Ref`, this automatically decrements the borrow
512/// counter when dropped, ensuring borrows are properly released.
513///
514/// # Deref
515///
516/// Implements `Deref<Target = T>` so you can use it like `&T`.
517#[derive(Debug)]
518#[repr(C)]
519pub struct Ref<'a, T> {
520 ptr: &'a T,
521 sharing_info: RefCount,
522}
523
524impl<T> Drop for Ref<'_, T> {
525 /// Automatically releases the shared borrow when the guard goes out of scope.
526 ///
527 /// # Safety
528 ///
529 /// Safe because `decrease_ref` uses atomic operations and is designed to be
530 /// called exactly once per `Ref` instance.
531 fn drop(&mut self) {
532 self.sharing_info.decrease_ref();
533 }
534}
535
536impl<T> core::ops::Deref for Ref<'_, T> {
537 type Target = T;
538
539 fn deref(&self) -> &Self::Target {
540 self.ptr
541 }
542}
543
544/// RAII guard for a mutable borrow of type `T` from a `RefAny`.
545///
546/// Similar to `std::cell::RefMut`, this automatically decrements the mutable
547/// borrow counter when dropped, releasing exclusive access.
548///
549/// # Deref / `DerefMut`
550///
551/// Implements both `Deref` and `DerefMut` so you can use it like `&mut T`.
552#[derive(Debug)]
553#[repr(C)]
554pub struct RefMut<'a, T> {
555 ptr: &'a mut T,
556 sharing_info: RefCount,
557}
558
559impl<T> Drop for RefMut<'_, T> {
560 /// Automatically releases the mutable borrow when the guard goes out of scope.
561 ///
562 /// # Safety
563 ///
564 /// Safe because `decrease_refmut` uses atomic operations and is designed to be
565 /// called exactly once per `RefMut` instance.
566 fn drop(&mut self) {
567 self.sharing_info.decrease_refmut();
568 }
569}
570
571impl<T> core::ops::Deref for RefMut<'_, T> {
572 type Target = T;
573
574 fn deref(&self) -> &Self::Target {
575 &*self.ptr
576 }
577}
578
579impl<T> core::ops::DerefMut for RefMut<'_, T> {
580 fn deref_mut(&mut self) -> &mut Self::Target {
581 self.ptr
582 }
583}
584
585/// Type-erased, reference-counted smart pointer with runtime borrow checking.
586///
587/// `RefAny` is similar to `Arc<RefCell<dyn Any>>`, providing:
588/// - Type erasure (stores any `'static` type)
589/// - Reference counting (clones share the same data)
590/// - Runtime borrow checking (enforces Rust's borrowing rules at runtime)
591/// - FFI compatibility (`#[repr(C)]` and C-compatible API)
592///
593/// # Thread Safety
594///
595/// - `Send`: Can be moved between threads (heap-allocated data, atomic counters)
596/// - `Sync`: Can be shared between threads (`downcast_ref/mut` require `&mut self`)
597///
598/// # Memory Safety
599///
600/// Fixed critical UB bugs in alignment, copy count, and pointer provenance.
601/// All operations are verified with Miri to ensure absence of undefined behavior.
602///
603/// # Usage
604///
605/// ```rust
606/// # use azul_core::refany::RefAny;
607/// let data = RefAny::new(42i32);
608/// let mut data_clone = data.clone(); // shares the same heap allocation
609///
610/// // Runtime-checked downcasting with type safety
611/// if let Some(value_ref) = data_clone.downcast_ref::<i32>() {
612/// assert_eq!(*value_ref, 42);
613/// };
614///
615/// // Runtime-checked mutable borrowing
616/// if let Some(mut value_mut) = data_clone.downcast_mut::<i32>() {
617/// *value_mut = 100;
618/// };
619/// ```
620#[derive(Debug)]
621#[repr(C)]
622pub struct RefAny {
623 /// Shared metadata: reference counts, type info, destructor, AND data pointer.
624 ///
625 /// All `RefAny` clones point to the same `RefCountInner` via this field.
626 /// The data pointer is stored in `RefCountInner` so all clones see the same
627 /// pointer, even after `replace_contents()` is called.
628 ///
629 /// The `run_destructor` flag on `RefCount` controls whether dropping this
630 /// `RefAny` should decrement the reference count and potentially free memory.
631 pub sharing_info: RefCount,
632
633 /// Unique ID for this specific clone (root = 0, subsequent clones increment).
634 ///
635 /// Used to distinguish between the original and clones for debugging.
636 pub instance_id: u64,
637}
638
639// The comparison traits below are hand-written, NOT derived, and key on
640// `sharing_info` ALONE. `instance_id` is deliberately omitted:
641//
642// // self.instance_id == other.instance_id <-- NEVER compare this
643//
644// `instance_id` is a debug-only counter that `clone()` increments (original = 0,
645// first clone = 1, ...). Deriving equality folded it in, so a `RefAny` never
646// equaled its own clone even though both point at the same `RefCountInner` — the
647// same heap data, same refcount. Equality here means "same data", not "same
648// handle"; `sharing_info` (a pointer + flag) already distinguishes unrelated
649// instances.
650//
651// Hash/Ord must key on exactly the same fields as PartialEq or they break their
652// own contracts (equal values must hash equally; `cmp() == Equal` must imply
653// `==`), so all five delegate to `sharing_info`.
654impl PartialEq for RefAny {
655 fn eq(&self, other: &Self) -> bool {
656 self.sharing_info == other.sharing_info
657 }
658}
659
660impl Eq for RefAny {}
661
662impl core::hash::Hash for RefAny {
663 fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
664 core::hash::Hash::hash(&self.sharing_info, state);
665 }
666}
667
668impl PartialOrd for RefAny {
669 fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
670 Some(self.cmp(other))
671 }
672}
673
674impl Ord for RefAny {
675 fn cmp(&self, other: &Self) -> core::cmp::Ordering {
676 self.sharing_info.cmp(&other.sharing_info)
677 }
678}
679
680impl_option!(
681 RefAny,
682 OptionRefAny,
683 copy = false,
684 [Debug, Hash, Clone, PartialEq, PartialOrd, Ord, Eq]
685);
686
687// AUDIT: unsound-but-required. These `Send`/`Sync` impls are unconditional in
688// `T`: a `!Send`/`!Sync` payload moved or shared cross-thread races its own
689// internals. This is an INTENTIONAL FFI design constraint — `RefAny` is a
690// type-erased C-ABI handle with no way to carry `T: Send + Sync` bounds across
691// the boundary, and the framework's threading model keeps a given payload on
692// one thread in practice. Left as-is per the audit; do not "fix" by adding
693// bounds (it would break the erased FFI type).
694//
695// SAFETY: RefAny is Send because:
696// - The data pointer points to heap memory (can be sent between threads)
697// - All shared state (RefCountInner) uses atomic operations
698// - No thread-local storage is used
699#[allow(clippy::non_send_fields_in_send_ty)] // see SAFETY note above: atomic refcount, no TLS, no cross-thread deref
700unsafe impl Send for RefAny {}
701
702// SAFETY: RefAny is Sync because:
703// - Methods on `&RefAny` (like `clone`, `get_type_id`) only use atomic operations or
704// read immutable data, which is inherently thread-safe
705// - The runtime borrow checker (via `can_be_shared/shared_mut`) uses SeqCst atomics
706//
707// AUDIT: unsound-but-required (same intentional FFI constraint as `Send` above).
708//
709// The check-then-increment race that this note described in `downcast_ref/mut`
710// is now FIXED (both use atomic `fetch_add`+validate / `compare_exchange`
711// acquisition — see those methods). The remaining unsoundness is only the
712// unconditional-in-`T` `Sync`, which is required by the erased C-ABI type.
713unsafe impl Sync for RefAny {}
714
715impl RefAny {
716 /// Creates a new type-erased `RefAny` containing the given value.
717 ///
718 /// This is the primary way to construct a `RefAny` from Rust code.
719 ///
720 /// # Type Safety
721 ///
722 /// Stores the `TypeId` of `T` for runtime type checking during downcasts.
723 ///
724 /// # Memory Layout
725 ///
726 /// - Allocates memory on the heap with correct size (`size_of::<T>()`) and alignment
727 /// (`align_of::<T>()`)
728 /// - Copies the value into the heap allocation
729 /// - Forgets the original value to prevent double-drop
730 ///
731 /// # Custom Destructor
732 ///
733 /// Creates a type-specific destructor that:
734 /// 1. Copies the data from heap back to stack
735 /// 2. Calls `mem::drop` to run `T`'s destructor
736 /// 3. The heap memory is freed separately in `RefAny::drop`
737 ///
738 /// This two-phase destruction ensures proper cleanup even for complex types.
739 ///
740 /// # Safety
741 ///
742 /// Safe because:
743 /// - `mem::forget` prevents double-drop of the original value
744 /// - Type `T` and destructor `<U>` are matched at compile time
745 /// - `ptr::copy_nonoverlapping` with count=1 copies exactly one `T`
746 ///
747 /// # Example
748 ///
749 /// ```rust
750 /// # use azul_core::refany::RefAny;
751 /// let mut data = RefAny::new(42i32);
752 /// let value = data.downcast_ref::<i32>().unwrap();
753 /// assert_eq!(*value, 42);
754 /// ```
755 pub fn new<T: 'static>(value: T) -> Self {
756 /// Type-specific destructor that properly drops the inner value.
757 ///
758 /// # Safety
759 ///
760 /// Safe to call ONLY with a pointer that was created by `RefAny::new<U>`.
761 /// The type `U` must match the original type `T`.
762 ///
763 /// # Why Copy to Stack?
764 ///
765 /// Rust's drop glue expects a value, not a pointer. We copy the data
766 /// to the stack so `mem::drop` can run the destructor properly.
767 ///
768 /// # Critical Fix
769 ///
770 /// The third argument to `copy_nonoverlapping` is the COUNT (1 element),
771 /// not the SIZE in bytes. Using `size_of::<U>()` here would copy
772 /// `size_of::<U>()` elements, causing buffer overflow.
773 extern "C" fn default_custom_destructor<U: 'static>(ptr: *mut c_void) {
774 use core::{mem, ptr};
775
776 // The actual drop glue. `U::drop` is arbitrary user code and this
777 // function is `extern "C"` (called across the FFI boundary from the
778 // C ABI teardown), so a panic escaping here would unwind across that
779 // boundary = UB.
780 // SAFETY: this fn is only installed by `RefAny::new::<U>`, so `ptr`
781 // points to an initialized, properly aligned `U` that no other code
782 // still references (we are in the final drop). We move it out exactly
783 // once (`count = 1`) and run its drop glue.
784 let run = || unsafe {
785 // A ZST has no bytes to move, and `ptr` is not a real pointer to one:
786 // `RefAny::new` never allocates for a ZST, and `RefCount::drop`
787 // substitutes a 1-byte-aligned dummy. Feeding that to
788 // `copy_nonoverlapping` violates its "aligned and non-null"
789 // precondition (`[u64; 0]` demands align 8) — UB, and Rust's debug
790 // check turns it into a NON-UNWINDING abort that kills the process.
791 //
792 // A ZST has exactly one value, so conjure it directly and run its drop
793 // glue without touching `ptr` at all.
794 if size_of::<U>() == 0 {
795 // Sound for a ZST (exactly one value, touches no memory); the
796 // size_of == 0 guard is what makes assume_init well-defined here.
797 #[allow(clippy::uninit_assumed_init)]
798 drop(mem::MaybeUninit::<U>::uninit().assume_init());
799 return;
800 }
801
802 // Allocate uninitialized stack space for one `U`
803 let mut stack_mem = mem::MaybeUninit::<U>::uninit();
804
805 // Copy 1 element of type U from heap to stack
806 ptr::copy_nonoverlapping(
807 ptr as *const U,
808 stack_mem.as_mut_ptr(),
809 1, // CRITICAL: This is element count, not byte count!
810 );
811
812 // Take ownership and run the destructor
813 let stack_mem = stack_mem.assume_init();
814 drop(stack_mem); // Runs U's Drop implementation
815 };
816
817 // AUDIT: contain any panic from `U::drop` so it can't unwind across
818 // the `extern "C"` boundary. `catch_unwind` needs `std`; `no_std`
819 // builds use `panic = "abort"`, where unwinding cannot occur.
820 #[cfg(feature = "std")]
821 {
822 drop(std::panic::catch_unwind(std::panic::AssertUnwindSafe(run)));
823 }
824 #[cfg(not(feature = "std"))]
825 {
826 run();
827 }
828 }
829
830 let type_name = ::core::any::type_name::<T>();
831 let type_id = Self::get_type_id_static::<T>();
832
833 let st = AzString::from_const_str(type_name);
834 let s = Self::new_c(
835 (&raw const value) as *const c_void,
836 ::core::mem::size_of::<T>(),
837 ::core::mem::align_of::<T>(), // CRITICAL: Pass alignment to prevent UB
838 type_id,
839 st,
840 default_custom_destructor::<T>,
841 0, // serialize_fn: not set for Rust types by default
842 0, // deserialize_fn: not set for Rust types by default
843 );
844 ::core::mem::forget(value); // Prevent double-drop
845 s
846 }
847
848 /// C-ABI compatible function to create a `RefAny` from raw components.
849 ///
850 /// This is the low-level constructor used by FFI bindings (C, Python, etc.).
851 ///
852 /// # Parameters
853 ///
854 /// - `ptr`: Pointer to the value to store (will be copied)
855 /// - `len`: Size of the value in bytes (`size_of::<T>()`)
856 /// - `align`: Required alignment in bytes (`align_of::<T>()`)
857 /// - `type_id`: Unique identifier for the type (for downcast safety)
858 /// - `type_name`: Human-readable type name (for debugging)
859 /// - `custom_destructor`: Function to call when the last reference is dropped
860 /// - `serialize_fn`: Function pointer for JSON serialization (0 = not set)
861 /// - `deserialize_fn`: Function pointer for JSON deserialization (0 = not set)
862 ///
863 /// # Safety
864 ///
865 /// Caller must ensure:
866 /// - `ptr` points to valid data of size `len` with alignment `align`
867 /// - `type_id` uniquely identifies the type
868 /// - `custom_destructor` correctly drops the type at `ptr`
869 /// - `len` and `align` match the actual type's layout
870 /// - If `serialize_fn != 0`, it must be a valid function pointer of type
871 /// `extern "C" fn(RefAny) -> Json`
872 /// - If `deserialize_fn != 0`, it must be a valid function pointer of type
873 /// `extern "C" fn(Json) -> ResultRefAnyString`
874 ///
875 /// # Zero-Sized Types
876 ///
877 /// Special case: ZSTs use a null pointer but still track the type info
878 /// and call the destructor (which may have side effects even for ZSTs).
879 ///
880 /// # Panics
881 ///
882 /// Panics if `ptr` is null while `len > 0` (a non-empty value must have a
883 /// valid backing pointer).
884 #[allow(clippy::used_underscore_binding)] // `_`-prefixed fields are an intentional FFI/api.json naming convention; internal access is required
885 pub fn new_c(
886 // *const T
887 ptr: *const c_void,
888 // sizeof(T)
889 len: usize,
890 // alignof(T)
891 align: usize,
892 // unique ID of the type (used for type comparison when downcasting)
893 type_id: u64,
894 // name of the class such as "app::MyData", usually compiler- or macro-generated
895 type_name: AzString,
896 custom_destructor: extern "C" fn(*mut c_void),
897 // function pointer for JSON serialization (0 = not set)
898 serialize_fn: usize,
899 // function pointer for JSON deserialization (0 = not set)
900 deserialize_fn: usize,
901 ) -> Self {
902 use core::ptr;
903
904 // CRITICAL: Validate input pointer for non-ZST types
905 // A NULL pointer for a non-zero-sized type would cause UB when copying
906 assert!(
907 !(len > 0 && ptr.is_null()),
908 "RefAny::new_c: NULL pointer passed for non-ZST type (size={}). \
909 This would cause undefined behavior. Type: {:?}",
910 len,
911 type_name.as_str()
912 );
913
914 // Special case: Zero-sized types
915 //
916 // Calling `alloc(Layout { size: 0, .. })` is UB, so we use a null pointer.
917 // The destructor is still called (it may have side effects even for ZSTs).
918 let (_internal_ptr, layout) = if len == 0 {
919 let _dummy: [u8; 0] = [];
920 (ptr::null_mut(), Layout::for_value(&_dummy))
921 } else {
922 // CRITICAL FIX: Use the caller-provided alignment, not alignment of [u8]
923 //
924 // Previous bug: `Layout::for_value(&[u8])` created align=1
925 // This caused unaligned references when downcasting to types like i32 (align=4)
926 //
927 // Fixed: `Layout::from_size_align(len, align)` respects the type's alignment
928 let layout = Layout::from_size_align(len, align).expect("Failed to create layout");
929
930 // Allocate heap memory with correct alignment
931 // SAFETY: `layout` has non-zero size (this branch is `len != 0`), the
932 // required precondition for `alloc`; null return is handled below.
933 let heap_struct_as_bytes = unsafe { alloc::alloc::alloc(layout) };
934
935 // Handle allocation failure (aborts the program)
936 if heap_struct_as_bytes.is_null() {
937 alloc::alloc::handle_alloc_error(layout);
938 }
939
940 // Copy the data byte-by-byte to the heap
941 // SAFETY: Both pointers are valid, non-overlapping, and properly aligned
942 unsafe { ptr::copy_nonoverlapping(ptr as *const u8, heap_struct_as_bytes, len) };
943
944 (heap_struct_as_bytes, layout)
945 };
946
947 let ref_count_inner = RefCountInner {
948 _internal_ptr: _internal_ptr as *const c_void,
949 num_copies: AtomicUsize::new(1), // This is the first instance
950 num_refs: AtomicUsize::new(0), // No borrows yet
951 num_mutable_refs: AtomicUsize::new(0), // No mutable borrows yet
952 _internal_len: len,
953 _internal_layout_size: layout.size(),
954 _internal_layout_align: layout.align(),
955 type_id,
956 type_name,
957 custom_destructor,
958 serialize_fn,
959 deserialize_fn,
960 update_fn: 0, // on-update observer not set by default; see set_update_fn
961 };
962
963 let sharing_info = RefCount::new(ref_count_inner);
964
965 Self {
966 sharing_info,
967 instance_id: 0, // Root instance
968 }
969 }
970
971 /// Returns the raw data pointer for FFI downcasting.
972 ///
973 /// This is used by the `AZ_REFLECT` macros in C/C++ to access the
974 /// type-erased data pointer for downcasting operations.
975 ///
976 /// # Safety
977 ///
978 /// The returned pointer must only be dereferenced after verifying
979 /// the type ID matches the expected type. Callers are responsible
980 /// for proper type safety checks.
981 #[allow(clippy::used_underscore_binding)]
982 // `_`-prefixed fields are an intentional FFI/api.json naming convention; internal access is required
983 #[must_use]
984 pub fn get_data_ptr(&self) -> *const c_void {
985 self.sharing_info.downcast()._internal_ptr
986 }
987
988 /// Returns the byte length of the type-erased payload behind
989 /// [`Self::get_data_ptr`] (`size_of::<T>()` of the stored type;
990 /// `0` for ZSTs).
991 #[allow(clippy::used_underscore_binding)]
992 // `_`-prefixed fields are an intentional FFI/api.json naming convention; internal access is required
993 #[must_use]
994 pub fn get_data_len(&self) -> usize {
995 self.sharing_info.downcast()._internal_len
996 }
997
998 /// Checks if this is the only `RefAny` instance with no active borrows.
999 ///
1000 /// Returns `true` only if:
1001 /// - `num_copies == 1` (no clones exist)
1002 /// - `num_refs == 0` (no shared borrows active)
1003 /// - `num_mutable_refs == 0` (no mutable borrows active)
1004 ///
1005 /// Useful for checking if you have exclusive ownership.
1006 ///
1007 /// # Memory Ordering
1008 ///
1009 /// Uses `SeqCst` to ensure a consistent view across all three counters.
1010 pub(crate) fn has_no_copies(&self) -> bool {
1011 self.sharing_info
1012 .downcast()
1013 .num_copies
1014 .load(AtomicOrdering::SeqCst)
1015 == 1
1016 && self
1017 .sharing_info
1018 .downcast()
1019 .num_refs
1020 .load(AtomicOrdering::SeqCst)
1021 == 0
1022 && self
1023 .sharing_info
1024 .downcast()
1025 .num_mutable_refs
1026 .load(AtomicOrdering::SeqCst)
1027 == 0
1028 }
1029
1030 /// Attempts to downcast to a shared reference of type `U`.
1031 ///
1032 /// Returns `None` if:
1033 /// - The stored type doesn't match `U` (type safety)
1034 /// - A mutable borrow is already active (borrow checking)
1035 /// - The pointer is null AND `U` is not zero-sized (uninitialized). A
1036 /// stored ZST has a null pointer *by design* (nothing is allocated) and
1037 /// downcasts successfully, via a dangling-but-aligned reference.
1038 ///
1039 /// # Type Safety
1040 ///
1041 /// Compares `type_id` at runtime before casting. This prevents casting
1042 /// `*const c_void` to the wrong type, which would be immediate UB.
1043 ///
1044 /// # Borrow Checking
1045 ///
1046 /// Checks `can_be_shared()` to enforce Rust's borrowing rules:
1047 /// - Multiple shared borrows are allowed
1048 /// - Shared and mutable borrows cannot coexist
1049 ///
1050 /// # Safety
1051 ///
1052 /// The `unsafe` cast is safe because:
1053 /// - Type ID check ensures `U` matches the stored type
1054 /// - Memory was allocated with correct alignment for `U`
1055 /// - Lifetime `'a` is tied to `&'a mut self`, preventing use-after-free
1056 /// - Reference count is incremented atomically before returning
1057 ///
1058 /// # Why `&mut self`?
1059 ///
1060 /// Requires `&mut self` to prevent multiple threads from calling this
1061 /// simultaneously on the same `RefAny`. The borrow checker enforces this.
1062 /// Clones of the `RefAny` can call this independently (they share data
1063 /// but have separate runtime borrow tracking).
1064 #[allow(clippy::used_underscore_binding)]
1065 // `_`-prefixed fields are an intentional FFI/api.json naming convention; internal access is required
1066 #[inline]
1067 pub fn downcast_ref<U: 'static>(&mut self) -> Option<Ref<'_, U>> {
1068 // A RELEASED `RefAny` holds nothing to borrow. Checked explicitly
1069 // rather than relying on `get_type_id`'s `0` never colliding with a
1070 // real `TypeId` hash.
1071 if self.sharing_info.is_released() {
1072 report_released_downcast();
1073 return None;
1074 }
1075 // Runtime type check: prevent downcasting to wrong type
1076 let stored_type_id = self.get_type_id();
1077 let target_type_id = Self::get_type_id_static::<U>();
1078 let is_same_type = stored_type_id == target_type_id;
1079
1080 if !is_same_type {
1081 return None;
1082 }
1083
1084 // AUDIT: ATOMIC shared-borrow acquisition.
1085 //
1086 // `RefAny` is `Sync` and clones share one `RefCountInner`, so the old
1087 // check-then-increment (`can_be_shared()` then `increase_ref()`) raced a
1088 // concurrent `downcast_mut` on another clone: both could pass their
1089 // pre-checks and hand out aliasing `&`/`&mut` to the same memory (UB).
1090 //
1091 // Fix (mirrors the `compare_exchange` discipline in `replace_contents`):
1092 // increment `num_refs` FIRST, then validate that no mutable borrow is
1093 // live. `SeqCst` imposes a single total order, so a writer (which CASes
1094 // `num_mutable_refs` 0->1 then reads `num_refs`) and this reader (which
1095 // adds to `num_refs` then reads `num_mutable_refs`) can never both
1096 // succeed — at least one observes the other's write. Back the increment
1097 // out on any failure path.
1098 self.sharing_info.increase_ref();
1099
1100 if !self.sharing_info.can_be_shared() {
1101 // A mutable borrow is (being) acquired — release and fail.
1102 self.sharing_info.decrease_ref();
1103 return None;
1104 }
1105
1106 // Get data pointer from shared RefCountInner (stable while we hold the
1107 // shared borrow: `replace_contents` needs `num_refs == 0` to proceed).
1108 let data_ptr = self.sharing_info.downcast()._internal_ptr;
1109
1110 // A null `_internal_ptr` means either an uninitialized `RefAny` or a ZST:
1111 // `RefAny::new_c` stores ZSTs with a null pointer (they need no backing
1112 // allocation). A ZST is a *valid* stored value, so the type check above is
1113 // authoritative and a `&U` to a ZST dereferences no bytes — only a
1114 // *non-ZST* null pointer is a genuine failure.
1115 if data_ptr.is_null() && size_of::<U>() != 0 {
1116 self.sharing_info.decrease_ref();
1117 return None;
1118 }
1119
1120 Some(Ref {
1121 // SAFETY: type check passed. For a real value `data_ptr` is non-null
1122 // and correctly aligned; for a ZST (null pointer) we hand out a
1123 // dangling-but-aligned `NonNull::dangling` reference, valid precisely
1124 // because it is never dereferenced for bytes.
1125 ptr: unsafe {
1126 if data_ptr.is_null() {
1127 &*core::ptr::NonNull::<U>::dangling().as_ptr()
1128 } else {
1129 &*(data_ptr as *const U)
1130 }
1131 },
1132 sharing_info: self.sharing_info.clone(),
1133 })
1134 }
1135
1136 /// Attempts to downcast to a mutable reference of type `U`.
1137 ///
1138 /// Returns `None` if:
1139 /// - The stored type doesn't match `U` (type safety)
1140 /// - Any borrow is already active (borrow checking)
1141 /// - The pointer is null AND `U` is not zero-sized (uninitialized). A
1142 /// stored ZST has a null pointer *by design* and downcasts successfully,
1143 /// via a dangling-but-aligned reference; note that the on-update observer
1144 /// is NOT fired for a ZST (there are no bytes for it to snapshot).
1145 ///
1146 /// # Type Safety
1147 ///
1148 /// Compares `type_id` at runtime before casting, preventing UB.
1149 ///
1150 /// # Borrow Checking
1151 ///
1152 /// Checks `can_be_shared_mut()` to enforce exclusive mutability:
1153 /// - No other borrows (shared or mutable) can be active
1154 /// - This is Rust's `&mut T` rule, enforced at runtime
1155 ///
1156 /// # Safety
1157 ///
1158 /// The `unsafe` cast is safe because:
1159 ///
1160 /// - Type ID check ensures `U` matches the stored type
1161 /// - Memory was allocated with correct alignment for `U`
1162 /// - Borrow check ensures no other references exist
1163 /// - Lifetime `'a` is tied to `&'a mut self`, preventing aliasing
1164 /// - Mutable reference count is incremented atomically
1165 ///
1166 /// # Memory Ordering
1167 ///
1168 /// The `increase_refmut()` uses `SeqCst`, ensuring other threads see
1169 /// this mutable borrow before they try to acquire any borrow.
1170 #[allow(clippy::used_underscore_binding)]
1171 // `_`-prefixed fields are an intentional FFI/api.json naming convention; internal access is required
1172 #[inline]
1173 pub fn downcast_mut<U: 'static>(&mut self) -> Option<RefMut<'_, U>> {
1174 if self.sharing_info.is_released() {
1175 report_released_downcast();
1176 return None;
1177 }
1178 // Runtime type check
1179 let is_same_type = self.get_type_id() == Self::get_type_id_static::<U>();
1180 if !is_same_type {
1181 return None;
1182 }
1183
1184 // AUDIT: ATOMIC exclusive-borrow acquisition (mirror `replace_contents`).
1185 //
1186 // The old check-then-increment (`can_be_shared_mut()` then
1187 // `increase_refmut()`) raced concurrent borrows on sibling clones and
1188 // could hand out an aliasing `&mut` (UB). Instead, `compare_exchange`
1189 // `num_mutable_refs` 0->1 to atomically take the exclusive slot, THEN
1190 // verify no shared borrow is live; release + fail otherwise. The CAS
1191 // both acquires and rejects a second mutable borrow in one step.
1192 let inner = self.sharing_info.downcast();
1193 if inner
1194 .num_mutable_refs
1195 .compare_exchange(0, 1, AtomicOrdering::SeqCst, AtomicOrdering::SeqCst)
1196 .is_err()
1197 {
1198 return None;
1199 }
1200 if inner.num_refs.load(AtomicOrdering::SeqCst) != 0 {
1201 // A shared borrow is live — release the exclusive slot and fail.
1202 inner.num_mutable_refs.store(0, AtomicOrdering::SeqCst);
1203 return None;
1204 }
1205
1206 // Get data pointer from shared RefCountInner
1207 let data_ptr = inner._internal_ptr;
1208
1209 // A null `_internal_ptr` is either an uninitialized `RefAny` or a ZST
1210 // (stored with a null pointer; see `downcast_ref`). A non-ZST null is a
1211 // real failure — release the exclusive slot and bail. For a ZST there are
1212 // no bytes to observe or mutate, so skip the update observer below and
1213 // hand out a dangling-but-aligned `&mut`, keeping the exclusive borrow.
1214 if data_ptr.is_null() {
1215 if size_of::<U>() != 0 {
1216 inner.num_mutable_refs.store(0, AtomicOrdering::SeqCst);
1217 return None;
1218 }
1219 return Some(RefMut {
1220 // SAFETY: type check passed; `U` is a ZST, so a dangling-but-
1221 // aligned pointer is a valid `&mut` never dereferenced for bytes.
1222 ptr: unsafe { &mut *core::ptr::NonNull::<U>::dangling().as_ptr() },
1223 sharing_info: self.sharing_info.clone(),
1224 });
1225 }
1226
1227 // Fire the on-update observer (if registered) BEFORE handing out the
1228 // mutable borrow: the callback sees the pre-mutation data + its byte
1229 // length, enabling undo/redo snapshots and client/server state sync.
1230 let update_fn = inner.update_fn;
1231 if update_fn != 0 {
1232 // SAFETY: `update_fn` is non-zero (checked) and, per `set_update_fn`'s
1233 // contract, is a valid `extern "C" fn(*const c_void, usize)`. The
1234 // round-trip goes through an int-to-pointer CAST (not a direct
1235 // usize->fn transmute): a transmuted integer carries no provenance,
1236 // which is UB to call (Miri rejects it); the cast re-acquires it.
1237 let cb: extern "C" fn(*const c_void, usize) =
1238 unsafe { core::mem::transmute(update_fn as *const ()) };
1239 let len = inner._internal_len;
1240 // AUDIT: the observer is a host-provided `extern "C"` fn. A Rust
1241 // panic escaping it would unwind across the FFI boundary (UB), so
1242 // contain it. `catch_unwind` needs `std`; `no_std` builds use
1243 // `panic = "abort"` where no unwinding can occur.
1244 #[cfg(feature = "std")]
1245 {
1246 drop(std::panic::catch_unwind(std::panic::AssertUnwindSafe(
1247 || {
1248 cb(data_ptr, len);
1249 },
1250 )));
1251 }
1252 #[cfg(not(feature = "std"))]
1253 {
1254 cb(data_ptr, len);
1255 }
1256 }
1257
1258 Some(RefMut {
1259 // SAFETY: Type and borrow checks passed, exclusive access guaranteed
1260 ptr: unsafe { &mut *(data_ptr as *mut U) },
1261 sharing_info: self.sharing_info.clone(),
1262 })
1263 }
1264
1265 /// Computes a runtime type ID from Rust's `TypeId`.
1266 ///
1267 /// Rust's `TypeId` is not `#[repr(C)]` and can't cross FFI boundaries.
1268 /// This function converts it to a `u64` by treating it as a byte array.
1269 ///
1270 /// # Safety
1271 ///
1272 /// Safe because:
1273 /// - `TypeId` is a valid type with a stable layout
1274 /// - We only read from it, never write
1275 /// - The slice lifetime is bounded by the function scope
1276 ///
1277 /// # Implementation
1278 ///
1279 /// Treats the `TypeId` as bytes and sums them with bit shifts to create
1280 /// a unique (but not cryptographically secure) hash.
1281 #[inline]
1282 fn get_type_id_static<T: 'static>() -> u64 {
1283 use core::{any::TypeId, mem};
1284
1285 let t_id = TypeId::of::<T>();
1286
1287 // SAFETY: TypeId is a valid type, we're only reading it
1288 let struct_as_bytes = unsafe {
1289 core::slice::from_raw_parts((&raw const t_id) as *const u8, size_of::<TypeId>())
1290 };
1291
1292 // AUDIT: fold ALL bytes of the `TypeId` (16 on current toolchains),
1293 // not just the first 8. This u64 is the ONLY runtime type guard used by
1294 // `downcast_*`; dropping the high 8 bytes let two distinct types whose
1295 // `TypeId`s differ only in their upper half collide, permitting a
1296 // wrong-type downcast (UB). An FxHash-style rotate+multiply mixes every
1297 // byte into the result and is deterministic within a process run (which
1298 // is all `TypeId` itself guarantees).
1299 struct_as_bytes.iter().fold(0u64, |hash, &b| {
1300 (hash.rotate_left(5) ^ u64::from(b)).wrapping_mul(0x51_7c_c1_b7_27_22_0a_95)
1301 })
1302 }
1303
1304 /// Checks if the stored type matches the given type ID.
1305 #[must_use]
1306 pub fn is_type(&self, type_id: u64) -> bool {
1307 self.sharing_info.downcast().type_id == type_id
1308 }
1309
1310 /// Returns the stored type ID.
1311 #[must_use]
1312 pub fn get_type_id(&self) -> u64 {
1313 // A RELEASED `RefAny` has no type, and saying so beats aborting. This
1314 // is the gate every `downcast_ref` / `downcast_mut` passes through
1315 // first, so one check here makes the whole safe surface null-tolerant.
1316 // It used to reach `RefCount::downcast`'s assertion instead, which in a
1317 // `panic = "abort"` release build takes the process down — a
1318 // widget callback whose data had been released killed the app on an
1319 // ordinary focus change (device report, 2026-09-01). `0` is not a
1320 // `TypeId` any real `U` hashes to, so no downcast can match it.
1321 if self.sharing_info.is_released() {
1322 return 0;
1323 }
1324 self.sharing_info.downcast().type_id
1325 }
1326
1327 /// Returns the human-readable type name for debugging.
1328 #[must_use]
1329 pub fn get_type_name(&self) -> AzString {
1330 if self.sharing_info.is_released() {
1331 return AzString::from_const_str("<released>");
1332 }
1333 self.sharing_info.downcast().type_name.clone()
1334 }
1335
1336 /// Returns the current reference count (number of `RefAny` clones sharing this data).
1337 ///
1338 /// This is useful for debugging and metadata purposes.
1339 #[must_use]
1340 pub fn get_ref_count(&self) -> usize {
1341 self.sharing_info
1342 .downcast()
1343 .num_copies
1344 .load(AtomicOrdering::SeqCst)
1345 }
1346
1347 /// Returns the serialize function pointer (0 = not set).
1348 ///
1349 /// This is used for JSON serialization of `RefAny` contents.
1350 #[must_use]
1351 pub fn get_serialize_fn(&self) -> usize {
1352 self.sharing_info.downcast().serialize_fn
1353 }
1354
1355 /// Returns the deserialize function pointer (0 = not set).
1356 ///
1357 /// This is used for JSON deserialization to create a new `RefAny`.
1358 #[must_use]
1359 pub fn get_deserialize_fn(&self) -> usize {
1360 self.sharing_info.downcast().deserialize_fn
1361 }
1362
1363 /// Sets the serialize function pointer.
1364 ///
1365 /// # Safety
1366 ///
1367 /// The caller must ensure the function pointer is valid and has the correct
1368 /// signature: `extern "C" fn(RefAny) -> Json`
1369 ///
1370 /// **Known issue:** `&mut self` is exclusive to this clone, not to the shared
1371 /// `RefCountInner`. Concurrent calls via different clones are a data race
1372 /// because `serialize_fn` is a plain `usize`, not atomic.
1373 pub fn set_serialize_fn(&mut self, serialize_fn: usize) {
1374 // FIXME: &mut self is exclusive to this clone only, not to the shared
1375 // RefCountInner — concurrent calls via different clones are a data race.
1376 let inner = self.sharing_info.ptr.cast_mut();
1377 // SAFETY: `inner` came from `Box::into_raw` and is live (we hold `self`).
1378 unsafe {
1379 (*inner).serialize_fn = serialize_fn;
1380 }
1381 }
1382
1383 /// Sets the deserialize function pointer.
1384 ///
1385 /// # Safety
1386 ///
1387 /// The caller must ensure the function pointer is valid and has the correct
1388 /// signature: `extern "C" fn(Json) -> ResultRefAnyString`
1389 ///
1390 /// **Known issue:** `&mut self` is exclusive to this clone, not to the shared
1391 /// `RefCountInner`. Concurrent calls via different clones are a data race
1392 /// because `deserialize_fn` is a plain `usize`, not atomic.
1393 pub fn set_deserialize_fn(&mut self, deserialize_fn: usize) {
1394 // FIXME: &mut self is exclusive to this clone only, not to the shared
1395 // RefCountInner — concurrent calls via different clones are a data race.
1396 let inner = self.sharing_info.ptr.cast_mut();
1397 // SAFETY: `inner` came from `Box::into_raw` and is live (we hold `self`).
1398 unsafe {
1399 (*inner).deserialize_fn = deserialize_fn;
1400 }
1401 }
1402
1403 /// Registers an on-update observer (`0` = unset). It is fired from
1404 /// [`Self::downcast_mut`] with the (data ptr, byte len) of the *pre-mutation*
1405 /// data, just before the mutable borrow is handed out — the foundation for
1406 /// undo/redo snapshots and client/server state sync.
1407 ///
1408 /// # Safety
1409 ///
1410 /// If `update_fn != 0` it must be a valid `extern "C" fn(*const c_void, usize)`.
1411 /// Same shared-`RefCountInner` caveat as [`Self::set_serialize_fn`]: `&mut self`
1412 /// is exclusive to this clone, not to the shared inner.
1413 pub fn set_update_fn(&mut self, update_fn: usize) {
1414 let inner = self.sharing_info.ptr.cast_mut();
1415 // SAFETY: `inner` came from `Box::into_raw` and is live (we hold `self`).
1416 unsafe {
1417 (*inner).update_fn = update_fn;
1418 }
1419 }
1420
1421 /// Returns the registered on-update observer fn pointer (`0` = unset).
1422 #[must_use]
1423 pub fn get_update_fn(&self) -> usize {
1424 self.sharing_info.downcast().update_fn
1425 }
1426
1427 /// Returns true if this `RefAny` supports JSON serialization.
1428 #[must_use]
1429 pub fn can_serialize(&self) -> bool {
1430 self.get_serialize_fn() != 0
1431 }
1432
1433 /// Returns true if this `RefAny` type supports JSON deserialization.
1434 #[must_use]
1435 pub fn can_deserialize(&self) -> bool {
1436 self.get_deserialize_fn() != 0
1437 }
1438
1439 /// Replaces the contents of this `RefAny` with a new value from another `RefAny`.
1440 ///
1441 /// This method:
1442 /// 1. Atomically acquires a mutable "lock" via `compare_exchange`
1443 /// 2. Calls the destructor on the old value
1444 /// 3. Deallocates the old memory
1445 /// 4. Copies the new value's memory
1446 /// 5. Updates metadata (`type_id`, `type_name`, destructor, serialize/deserialize fns)
1447 /// 6. Updates the shared _`internal_ptr` so ALL clones see the new data
1448 /// 7. Releases the lock
1449 ///
1450 /// Since all clones of a `RefAny` share the same `RefCountInner`, this change
1451 /// will be visible to ALL clones of this `RefAny`.
1452 ///
1453 /// # Returns
1454 ///
1455 /// - `true` if the replacement was successful
1456 /// - `false` if there are active borrows (would cause UB)
1457 ///
1458 /// # Thread Safety
1459 ///
1460 /// Uses `compare_exchange` to atomically acquire exclusive access, preventing
1461 /// any race condition between checking for borrows and modifying the data.
1462 ///
1463 /// # Safety
1464 ///
1465 /// Safe because:
1466 /// - We atomically acquire exclusive access before modifying
1467 /// - The old destructor is called before deallocation
1468 /// - Memory is properly allocated with correct alignment
1469 /// - All metadata is updated while holding the lock
1470 ///
1471 /// # Panics
1472 ///
1473 /// Panics if a memory `Layout` for the replacement value cannot be
1474 /// constructed (its size overflows `isize::MAX`).
1475 #[allow(clippy::used_underscore_binding)] // `_`-prefixed fields are an intentional FFI/api.json naming convention; internal access is required
1476 pub fn replace_contents(&mut self, new_value: Self) -> bool {
1477 use core::ptr;
1478
1479 let inner = self.sharing_info.ptr.cast_mut();
1480
1481 // Atomically acquire exclusive access by setting num_mutable_refs to 1.
1482 // This uses compare_exchange to ensure no race condition:
1483 // - If num_mutable_refs is 0, set it to 1 (success)
1484 // - If num_mutable_refs is not 0, someone else has it (fail)
1485 // We also need to check num_refs == 0 atomically.
1486 let inner_ref = self.sharing_info.downcast();
1487
1488 // First, try to acquire the mutable lock
1489 let mutable_lock_result = inner_ref.num_mutable_refs.compare_exchange(
1490 0, // expected: no mutable refs
1491 1, // desired: we take the mutable ref
1492 AtomicOrdering::SeqCst,
1493 AtomicOrdering::SeqCst,
1494 );
1495
1496 if mutable_lock_result.is_err() {
1497 // Someone else has a mutable reference
1498 return false;
1499 }
1500
1501 // Now check that there are no shared references
1502 // Note: We hold the mutable lock, so no new shared refs can be acquired
1503 if inner_ref.num_refs.load(AtomicOrdering::SeqCst) != 0 {
1504 // Release the lock and fail
1505 inner_ref.num_mutable_refs.store(0, AtomicOrdering::SeqCst);
1506 return false;
1507 }
1508
1509 // We now have exclusive access - perform the replacement
1510 // SAFETY: we hold the exclusive lock (num_mutable_refs==1, num_refs==0),
1511 // so no live `Ref`/`RefMut` aliases the data; `inner` is the live
1512 // `RefCountInner` from `Box::into_raw`. Old data is destructed+freed with
1513 // its own stored layout before the pointer is overwritten, and the new
1514 // data is freshly allocated and byte-copied.
1515 unsafe {
1516 // Get old layout info before we overwrite it
1517 let old_ptr = (*inner)._internal_ptr;
1518 let old_len = (*inner)._internal_len;
1519 let old_layout_size = (*inner)._internal_layout_size;
1520 let old_layout_align = (*inner)._internal_layout_align;
1521 let old_destructor = (*inner).custom_destructor;
1522
1523 // Step 1: Call destructor on old value (if non-ZST)
1524 if old_len > 0 && !old_ptr.is_null() {
1525 old_destructor(old_ptr.cast_mut());
1526 }
1527
1528 // Step 2: Deallocate old memory (if non-ZST). Use the *checked*
1529 // `Layout::from_size_align` (not `_unchecked`): the stored
1530 // size/align came from a valid `Layout`, so it always succeeds, and
1531 // this shrinks the unchecked surface inside this unsafe block.
1532 if old_layout_size > 0 && !old_ptr.is_null() {
1533 let old_layout = Layout::from_size_align(old_layout_size, old_layout_align)
1534 .expect("replace_contents: stored old layout was invalid");
1535 alloc::alloc::dealloc(old_ptr as *mut u8, old_layout);
1536 }
1537
1538 // Get new value's metadata
1539 let new_inner = new_value.sharing_info.downcast();
1540 let new_ptr = new_inner._internal_ptr;
1541 let new_len = new_inner._internal_len;
1542 let new_layout_size = new_inner._internal_layout_size;
1543 let new_layout_align = new_inner._internal_layout_align;
1544
1545 // Step 3: Allocate new memory and copy data
1546 let allocated_ptr = if new_len == 0 {
1547 ptr::null_mut()
1548 } else {
1549 let new_layout = Layout::from_size_align(new_len, new_layout_align)
1550 .expect("Failed to create layout");
1551 let heap_ptr = alloc::alloc::alloc(new_layout);
1552 if heap_ptr.is_null() {
1553 alloc::alloc::handle_alloc_error(new_layout);
1554 }
1555 // Copy data from new_value
1556 ptr::copy_nonoverlapping(new_ptr as *const u8, heap_ptr, new_len);
1557 heap_ptr
1558 };
1559
1560 // Step 4: Update the shared internal pointer in RefCountInner
1561 // All clones will see this new pointer!
1562 (*inner)._internal_ptr = allocated_ptr as *const c_void;
1563
1564 // Step 5: Update metadata in RefCountInner
1565 (*inner)._internal_len = new_len;
1566 (*inner)._internal_layout_size = new_layout_size;
1567 (*inner)._internal_layout_align = new_layout_align;
1568 (*inner).type_id = new_inner.type_id;
1569 (*inner).type_name = new_inner.type_name.clone();
1570 (*inner).custom_destructor = new_inner.custom_destructor;
1571 (*inner).serialize_fn = new_inner.serialize_fn;
1572 (*inner).deserialize_fn = new_inner.deserialize_fn;
1573 (*inner).update_fn = new_inner.update_fn;
1574 }
1575
1576 // Release the mutable lock
1577 self.sharing_info
1578 .downcast()
1579 .num_mutable_refs
1580 .store(0, AtomicOrdering::SeqCst);
1581
1582 // AUDIT: reclaim `new_value` instead of leaking it.
1583 //
1584 // The old code `mem::forget(new_value)` to stop `RefAny::drop` from
1585 // running the T-destructor a SECOND time on the bytes we just copied
1586 // into our own allocation — but that leaked `new_value`'s entire
1587 // `RefCountInner` box AND its heap data block on every single call.
1588 //
1589 // Instead, neutralize `new_value`'s destructor to a no-op and let the
1590 // normal refcount teardown run: it frees BOTH allocations (data block +
1591 // inner box) when this was the last reference, without re-running the
1592 // real T-destructor (which now lives on OUR inner, to run exactly once
1593 // when `self` is finally dropped). If `new_value` still had clones, the
1594 // no-op keeps them from double-dropping the shared T while their own
1595 // last drop still reclaims the shared block — no double free, no leak.
1596 #[allow(clippy::items_after_statements)]
1597 const extern "C" fn noop_destructor(_: *mut c_void) {}
1598 let new_inner = new_value.sharing_info.ptr.cast_mut();
1599 if !new_inner.is_null() {
1600 // SAFETY: `new_inner` came from `Box::into_raw` in `RefCount::new`
1601 // and is still alive (we hold `new_value`).
1602 unsafe {
1603 (*new_inner).custom_destructor = noop_destructor;
1604 }
1605 }
1606 drop(new_value);
1607
1608 true
1609 }
1610}
1611
1612impl Clone for RefAny {
1613 /// Creates a new `RefAny` sharing the same heap-allocated data.
1614 ///
1615 /// This is cheap (just increments a counter) and is how multiple parts
1616 /// of the code can hold references to the same data.
1617 ///
1618 /// # Reference Counting
1619 ///
1620 /// Atomically increments `num_copies` with `SeqCst` ordering before
1621 /// creating the clone. This ensures all threads see the updated count
1622 /// before the clone can be used.
1623 ///
1624 /// # Instance ID
1625 ///
1626 /// Each clone gets a unique `instance_id` based on the current copy count.
1627 /// The original has `instance_id=0`, the first clone gets `1`, etc.
1628 ///
1629 /// # Memory Ordering
1630 ///
1631 /// The `fetch_add` followed by `load` both use `SeqCst`:
1632 /// - `fetch_add`: Ensures the increment is visible to all threads
1633 /// - `load`: Gets the updated value for the `instance_id`
1634 ///
1635 /// This prevents race conditions where two threads clone simultaneously
1636 /// and both see the same `instance_id`.
1637 ///
1638 /// # Safety
1639 ///
1640 /// Safe because:
1641 ///
1642 /// - Atomic operations prevent data races
1643 /// - The heap allocation remains valid (only freed when count reaches 0)
1644 /// - `run_destructor` is set to `true` for all clones
1645 fn clone(&self) -> Self {
1646 // Atomically increment the reference count
1647 let inner = self.sharing_info.downcast();
1648 let prev = inner.num_copies.fetch_add(1, AtomicOrdering::SeqCst);
1649
1650 let new_instance_id = (prev + 1) as u64;
1651
1652 Self {
1653 // Data pointer is now in RefCountInner, shared automatically
1654 sharing_info: RefCount {
1655 ptr: self.sharing_info.ptr, // Share the same metadata (and data pointer)
1656 run_destructor: true, // This clone should decrement num_copies on drop
1657 },
1658 // Give this clone a unique ID based on the updated count
1659 instance_id: new_instance_id,
1660 }
1661 }
1662}
1663
1664impl Drop for RefAny {
1665 /// Empty drop implementation - all cleanup is handled by `RefCount::drop`.
1666 ///
1667 /// When a `RefAny` is dropped, its `sharing_info: RefCount` field is automatically
1668 /// dropped by Rust. The `RefCount::drop` implementation handles all cleanup:
1669 ///
1670 /// 1. Atomically decrements `num_copies` with `fetch_sub`
1671 /// 2. If the previous value was 1 (we're the last reference):
1672 /// - Reclaims the `RefCountInner` via `Box::from_raw`
1673 /// - Calls the custom destructor to run `T::drop()`
1674 /// - Deallocates the heap memory with the stored layout
1675 ///
1676 /// # Why No Code Here?
1677 ///
1678 /// Previously, `RefAny::drop` handled cleanup, but this caused issues with the
1679 /// C API where `Ref<T>` and `RefMut<T>` guards (which clone the `RefCount`) need
1680 /// to keep the data alive even after the original `RefAny` is dropped.
1681 ///
1682 /// By moving all cleanup to `RefCount::drop`, we ensure that:
1683 /// - `RefAny::clone()` creates a `RefCount` with `run_destructor = true`
1684 /// - `AZ_REFLECT` macros create `Ref`/`RefMut` guards that clone `RefCount`
1685 /// - Each `RefCount` drop decrements the counter
1686 /// - Only the LAST drop (when `num_copies` was 1) cleans up memory
1687 ///
1688 /// See `RefCount::drop` for the full algorithm and safety documentation.
1689 fn drop(&mut self) {
1690 // RefCount::drop handles everything automatically.
1691 // The sharing_info field is dropped by Rust, triggering RefCount::drop.
1692 }
1693}
1694
1695#[cfg(test)]
1696#[path = "refany_test.rs"]
1697mod refany_test;