euv_core/reactive/signal/impl.rs
1use super::*;
2
3/// Implementation of reactive signal operations.
4impl<T> Signal<T>
5where
6 T: Clone + PartialEq + 'static,
7{
8 /// Returns a shared reference to the signal inner registry.
9 ///
10 /// # Returns
11 ///
12 /// - `&'static HashSet<usize>` - A shared reference to the global signal address registry.
13 #[allow(static_mut_refs)]
14 fn registry() -> &'static HashSet<usize> {
15 unsafe { &*SIGNAL_INNER_REGISTRY.deref().get_0().get() }
16 }
17
18 /// Returns a mutable reference to the signal inner registry.
19 ///
20 /// # Returns
21 ///
22 /// - `&'static mut HashSet<usize>` - A mutable reference to the global signal address registry.
23 #[allow(static_mut_refs)]
24 fn registry_mut() -> &'static mut HashSet<usize> {
25 unsafe { &mut *SIGNAL_INNER_REGISTRY.deref().get_0().get() }
26 }
27
28 /// Creates a new `Signal` with the given initial value.
29 ///
30 /// Allocates `SignalInner<T>` on the heap via `Box`, stores the raw pointer
31 /// address, and registers it in the global registry for lifecycle tracking.
32 ///
33 /// # Arguments
34 ///
35 /// - `T: Clone + PartialEq + 'static` - The initial value of the signal.
36 ///
37 /// # Returns
38 ///
39 /// - `Self` - A handle to the newly created reactive signal.
40 pub fn create(value: T) -> Self {
41 let mut inner: SignalInner<T> = SignalInner::new(value, Vec::new(), true);
42 inner.set_listeners_replaced(false);
43 let boxed: Box<SignalInner<T>> = Box::new(inner);
44 let ptr: *mut SignalInner<T> = Box::into_raw(boxed);
45 let addr: usize = ptr as usize;
46 Self::registry_mut().insert(addr);
47 let mut signal: Self = Self::new(0, PhantomData);
48 signal.set_inner(addr);
49 signal
50 }
51
52 /// Returns the current value of the signal.
53 ///
54 /// Directly reads the value from the heap-allocated inner state via raw
55 /// pointer dereference. No runtime borrow checking overhead.
56 ///
57 /// If the signal has been marked inactive (`alive == false`), returns the
58 /// last stored value without registering tracking dependencies. This
59 /// ensures that stale async callbacks (e.g., orphaned `setInterval`)
60 /// holding a `Signal` copy can still call `.get()` safely without
61 /// triggering side effects or panics.
62 ///
63 /// If a tracking context is active (i.e., a DynamicNode is being rendered),
64 /// automatically registers the current dynamic node as a dependent of
65 /// this signal for precise reactive updates.
66 ///
67 /// # Returns
68 ///
69 /// - `T: Clone + PartialEq + 'static` - The current value of the signal.
70 pub fn get(&self) -> T {
71 let inner: &mut SignalInner<T> = Self::inner_mut(self.get_inner());
72 if !inner.get_alive() {
73 return inner.get_value().clone();
74 }
75 let tracking_id: usize = CURRENT_TRACKING_DYNAMIC_ID.load(Ordering::Relaxed);
76 if tracking_id != usize::MAX {
77 self.add_dependent(tracking_id);
78 }
79 inner.get_value().clone()
80 }
81
82 /// Subscribes a callback to be invoked when the signal changes.
83 ///
84 /// # Arguments
85 ///
86 /// - `FnMut() + 'static` - The callback to invoke when the signal changes.
87 pub fn subscribe<F>(&self, callback: F)
88 where
89 F: FnMut() + 'static,
90 {
91 Self::inner_mut(self.get_inner())
92 .get_mut_listeners()
93 .push(Box::new(callback));
94 }
95
96 /// Replaces all listeners with a single new callback.
97 ///
98 /// Unlike `subscribe`, which appends a listener, this method clears any
99 /// existing listeners first and then adds the new one.
100 ///
101 /// # Arguments
102 ///
103 /// - `FnMut() + 'static` - The callback to invoke when the signal changes.
104 pub(crate) fn replace_listener<F>(&self, callback: F)
105 where
106 F: FnMut() + 'static,
107 {
108 let inner: &mut SignalInner<T> = Self::inner_mut(self.get_inner());
109 inner.get_mut_listeners().clear();
110 inner.get_mut_listeners().push(Box::new(callback));
111 inner.set_listeners_replaced(true);
112 }
113
114 /// Detaches this signal from the reactive system without freeing memory.
115 ///
116 /// Marks the signal inactive and clears its listeners and dependents, but
117 /// intentionally keeps the heap allocation alive.
118 ///
119 /// This is the only supported teardown path for a signal, and is used by
120 /// both DOM-bound subscribe closures (when their node is removed) and the
121 /// `use_signal` hook cleanup (when a component unmounts or a `match` arm
122 /// switches). Freeing the allocation is deliberately never done at these
123 /// points because `Signal<T>` is `Copy` (just a `usize` address): async
124 /// callbacks (`spawn_local` futures, `setTimeout` / `setInterval`
125 /// closures, Promise continuations) may still hold copies of the signal,
126 /// and freeing would turn their later `.get()` / `.set()` calls into a
127 /// use-after-free. Deactivating instead makes those stale calls safe
128 /// no-ops.
129 ///
130 /// The allocation remains valid until the page unloads. For SPAs this is
131 /// acceptable; a long-lived app could add a periodic sweep that frees
132 /// `alive == false` entries once no async references remain. This mirrors
133 /// the contract documented on `clear_signal_listeners`.
134 pub(crate) fn deactivate(&self) {
135 let inner: &mut SignalInner<T> = Self::inner_mut(self.get_inner());
136 inner.set_alive(false);
137 inner.get_mut_listeners().clear();
138 inner.get_mut_dependents().clear();
139 // Remove this signal as a subscriber from every bridge it currently
140 // depends on. Any bridge whose dependency set becomes empty AND has
141 // already been detached (no longer in `SIGNAL_INNER_REGISTRY`) is
142 // fully reclaimed by freeing its `SignalInner<T>` heap allocation.
143 // Bridges still in the registry are kept alive because their bound
144 // DOM element still references them via `data-euv-signal-addrs`.
145 let self_addr: usize = self.get_inner();
146 let mut ready_to_free: Vec<usize> = Vec::new();
147 for (bridge_addr, sources) in BridgeRefsCell::map_mut().iter_mut() {
148 if sources.remove(&self_addr) && sources.is_empty() {
149 // The bridge has no remaining source subscribers; it can
150 // be freed if it has already been deactivated (i.e. its
151 // element was detached and `clear_listeners` ran).
152 if !Self::registry().contains(bridge_addr) {
153 ready_to_free.push(*bridge_addr);
154 }
155 }
156 }
157 for bridge_addr in ready_to_free {
158 BridgeRefsCell::map_mut().remove(&bridge_addr);
159 unsafe {
160 let _: Box<SignalInner<T>> = Box::from_raw(bridge_addr as *mut SignalInner<T>);
161 }
162 }
163 }
164
165 /// Core implementation of value update and listener notification.
166 ///
167 /// Returns `true` if the value was updated and listeners were notified.
168 /// Returns `false` if the signal is inactive or the value is unchanged.
169 ///
170 /// Uses a swap-out pattern for listeners: moves all listeners into a local
171 /// `Vec`, drops the mutable reference to inner state, then invokes each
172 /// listener. After invocation, listeners are moved back. This prevents
173 /// issues with re-entrant access during listener callbacks.
174 ///
175 /// # Arguments
176 ///
177 /// - `T: Clone + PartialEq + 'static` - A generic type parameter.
178 ///
179 /// # Returns
180 ///
181 /// - `bool` - A boolean.
182 fn update(&self, value: T) -> bool {
183 let inner: &mut SignalInner<T> = Self::inner_mut(self.get_inner());
184 if !inner.get_alive() {
185 return false;
186 }
187 if *inner.get_value() == value {
188 return false;
189 }
190 inner.set_value(value);
191 inner.set_listeners_replaced(false);
192 let mut listeners: Vec<Box<dyn FnMut()>> = Vec::new();
193 swap(inner.get_mut_listeners(), &mut listeners);
194 for listener in listeners.iter_mut() {
195 listener();
196 }
197 if !Self::is_alive(self.get_inner()) {
198 return true;
199 }
200 let inner: &mut SignalInner<T> = Self::inner_mut(self.get_inner());
201 if inner.get_alive() {
202 if inner.get_listeners_replaced() {
203 inner.set_listeners_replaced(false);
204 } else {
205 let new_listeners: &mut Vec<Box<dyn FnMut()>> = inner.get_mut_listeners();
206 if new_listeners.is_empty() {
207 swap(new_listeners, &mut listeners);
208 } else {
209 listeners.append(new_listeners);
210 swap(new_listeners, &mut listeners);
211 }
212 }
213 }
214 true
215 }
216
217 /// Registers a dynamic node ID as a dependent of this signal.
218 ///
219 /// When this signal changes, only its registered dependents will be
220 /// marked dirty for re-rendering, enabling precise updates instead
221 /// of broadcasting to all dynamic nodes.
222 ///
223 /// # Arguments
224 ///
225 /// - `usize` - The dynamic node ID to register as a dependent.
226 ///
227 /// OPT 9: the common rendering case is "this dependent was just added
228 /// (last element of the list)". A `deps.last() == Some(&dynamic_id)`
229 /// check short-circuits the `Vec::contains` linear scan, turning the
230 /// typical append-into-existing-list call from O(N) to O(1). Only the
231 /// rare cases (first add, or `dynamic_id` re-added after a previous
232 /// unsubscription) fall back to the full scan + push.
233 pub(crate) fn add_dependent(&self, dynamic_id: usize) {
234 let deps: &mut Vec<usize> = Self::inner_mut(self.get_inner()).get_mut_dependents();
235 if let Some(last) = deps.last() {
236 if *last == dynamic_id {
237 return;
238 }
239 if !deps.contains(&dynamic_id) {
240 deps.push(dynamic_id);
241 }
242 } else {
243 deps.push(dynamic_id);
244 }
245 }
246
247 /// Returns the list of dependent dynamic node IDs for this signal.
248 ///
249 /// # Returns
250 ///
251 /// - `Vec<usize>` - Clone of the dependents list.
252 pub(crate) fn get_dependents(&self) -> Vec<usize> {
253 Self::inner_mut(self.get_inner()).get_dependents().clone()
254 }
255
256 /// Sets the value of the signal and notifies listeners.
257 ///
258 /// Uses precise dirty marking: only dynamic nodes that depend on
259 /// this signal are marked dirty, avoiding full broadcast.
260 ///
261 /// When called inside `batch`, the dispatch is
262 /// deferred (dirty slots are still marked precisely), and the
263 /// outermost `set()` call outside the suppressed scope will
264 /// trigger the actual dispatch cycle.
265 ///
266 /// # Arguments
267 ///
268 /// - `T: Clone + PartialEq + 'static` - The new value to assign to the signal.
269 pub fn set(&self, value: T) {
270 if self.update(value) {
271 let dependents: Vec<usize> = self.get_dependents();
272 App::schedule_update(&dependents);
273 }
274 }
275
276 /// Retrieves a mutable pointer to `SignalInner<T>` directly from the
277 /// signal's stored address.
278 ///
279 /// SAFETY: The address stored in `Signal::inner` is always a valid pointer
280 /// to a `SignalInner<T>` that is kept alive by the global registry. Since
281 /// WASM is single-threaded, the pointer is always valid as long as the
282 /// signal has not been explicitly freed.
283 ///
284 /// # Arguments
285 ///
286 /// - `usize` - A non-negative integer (`usize`).
287 ///
288 /// # Returns
289 ///
290 /// - `'static mut SignalInner<T>` - A `'static mut SignalInner<T>` value.
291 fn inner_mut(addr: usize) -> &'static mut SignalInner<T> {
292 unsafe { &mut *(addr as *mut SignalInner<T>) }
293 }
294
295 /// Returns whether the signal allocation at `addr` is still present
296 /// in the global registry (i.e. has not been freed).
297 ///
298 /// # Arguments
299 ///
300 /// - `usize` - Raw address to test.
301 ///
302 /// # Returns
303 ///
304 /// - `bool` - `true` when the address still refers to live data.
305 pub(crate) fn is_alive(addr: usize) -> bool {
306 Self::registry().contains(&addr)
307 }
308}
309
310/// Provides a safe default for `Signal<T>` by creating a valid signal
311/// initialized with `T::default()`.
312///
313/// This prevents the creation of invalid signals with `inner = 0` (null
314/// pointer), which would cause a panic when `.get()` is called.
315///
316/// # Returns
317///
318/// - `Self` - A valid signal initialized with `T::default()`.
319impl<T> Default for Signal<T>
320where
321 T: Clone + Default + PartialEq + 'static,
322{
323 /// Constructs a default [`Signal`] value.
324 fn default() -> Self {
325 Self::create(T::default())
326 }
327}
328
329/// Clones the signal, sharing the same inner state.
330///
331/// Since `Signal` is `Copy`, this simply returns `*self`.
332///
333/// # Returns
334///
335/// - `Self` - A copy of the signal handle sharing the same inner state.
336impl<T> Clone for Signal<T>
337where
338 T: Clone + PartialEq + 'static,
339{
340 /// Clones the [`Signal`] by reusing shared, cheap-to-clone state where possible.
341 fn clone(&self) -> Self {
342 *self
343 }
344}
345
346/// Copies the signal, sharing the same inner state.
347///
348/// Safe because only the inner address (a `usize`) is copied;
349/// the actual heap allocation is owned by the global signal registry.
350impl<T> Copy for Signal<T> where T: Clone + PartialEq + 'static {}
351
352/// Marks `SignalCell` as `Sync` for single-threaded WASM contexts.
353///
354/// SAFETY: `SignalCell` is only used in single-threaded WASM contexts.
355/// Concurrent access from multiple threads would be undefined behavior.
356unsafe impl<T> Sync for SignalCell<T> where T: Clone + PartialEq + 'static {}
357
358/// Implementation of SignalCell construction and access.
359impl<T> SignalCell<T>
360where
361 T: Clone + PartialEq + 'static,
362{
363 /// Creates a new `SignalCell` with no signal stored.
364 ///
365 /// # Returns
366 ///
367 /// - `Self` - An empty `SignalCell` with `None` stored in the inner `UnsafeCell`.
368 pub const fn none() -> Self {
369 Self {
370 inner: UnsafeCell::new(None),
371 }
372 }
373
374 /// Stores a signal into the cell.
375 ///
376 /// First write wins: if a signal has already been stored, the new
377 /// signal is dropped and the existing one is kept.
378 ///
379 /// # Arguments
380 ///
381 /// - `Signal<T>` - The signal to store.
382 pub fn set(&self, signal: Signal<T>) {
383 unsafe {
384 let ptr: &mut Option<Signal<T>> = &mut *self.get_inner().get();
385 if ptr.is_none() {
386 *ptr = Some(signal);
387 }
388 }
389 }
390
391 /// Returns the signal stored in the cell, if any.
392 ///
393 /// # Returns
394 ///
395 /// - `Option<Signal<T>>` - The stored signal, or `None` when no signal
396 /// has been stored via `set` yet.
397 pub fn loaded(&self) -> Option<Signal<T>> {
398 unsafe {
399 let ptr: &Option<Signal<T>> = &*self.get_inner().get();
400 *ptr
401 }
402 }
403}
404
405/// Provides a default empty `SignalCell`.
406///
407/// Creates a `SignalCell` with `None` stored in the inner `UnsafeCell`.
408///
409/// # Returns
410///
411/// - `Self` - An empty `SignalCell` with no signal stored.
412impl<T> Default for SignalCell<T>
413where
414 T: Clone + PartialEq + 'static,
415{
416 /// Constructs a default [`SignalCell`] value.
417 fn default() -> Self {
418 Self::new(UnsafeCell::new(None))
419 }
420}
421
422/// Marks `SignalInnerRegistryCell` as `Sync` for single-threaded WASM contexts.
423///
424/// SAFETY: `SignalInnerRegistryCell` is only used in single-threaded WASM contexts.
425/// Concurrent access from multiple threads would be undefined behavior.
426unsafe impl Sync for SignalInnerRegistryCell {}
427
428/// Marks `BridgeRefsCell` as `Sync` for single-threaded WASM contexts.
429///
430/// SAFETY: `BridgeRefsCell` is only used in single-threaded WASM contexts.
431/// Concurrent access from multiple threads would be undefined behavior.
432unsafe impl Sync for BridgeRefsCell {}
433
434/// Static methods for the bridge dependency reverse-index.
435impl BridgeRefsCell {
436 /// Returns a mutable reference to the underlying `HashMap`. Bypasses
437 /// `Lombok`'s auto-generated `get_mut` so call sites can mutate the map
438 /// directly via the `&mut` borrow lifetime.
439 ///
440 /// # Returns
441 ///
442 /// - `&'static mut HashMap<usize, HashSet<usize>>` - A mutable reference
443 /// to the global bridge dependency reverse-index.
444 #[allow(static_mut_refs)]
445 pub(crate) fn map_mut() -> &'static mut HashMap<usize, HashSet<usize>> {
446 unsafe { &mut *BRIDGE_REFS.deref().get_0().get() }
447 }
448
449 /// Records that `source_addr` has registered a `subscribe` closure which
450 /// captures `bridge_addr`. Used by bridge-signal creation sites so the
451 /// framework can safely reclaim the bridge's heap allocation once
452 /// `source` is deactivated.
453 ///
454 /// Bridge signals live inside framework-internal code paths only
455 /// (`create_dom_with_doc`, `as_reactive_text`, `bool_to_attr`); user code
456 /// never needs to call this directly. The companion lookup happens in
457 /// `Signal::deactivate` (removes `source_addr` from every bridge's
458 /// dependency set) and `Signal::<String>::clear_listeners` (marks the
459 /// bridge as eligible for reclamation once its dependency set is empty).
460 ///
461 /// # Arguments
462 ///
463 /// - `usize` - The bridge signal's heap address (must currently be in
464 /// `SIGNAL_INNER_REGISTRY`).
465 /// - `usize` - The source signal's heap address.
466 pub(crate) fn track(bridge_addr: usize, source_addr: usize) {
467 Self::map_mut()
468 .entry(bridge_addr)
469 .or_default()
470 .insert(source_addr);
471 }
472}
473
474/// String-specific signal operations.
475impl Signal<String> {
476 /// Clears DOM-binding listeners on a bridge signal identified by its inner
477 /// pointer address, deactivates the bridge signal, and releases its value
478 /// memory.
479 ///
480 /// This function is used during DOM cleanup (`cleanup_dom_subtree`) to
481 /// release bridge `Signal<String>` instances that are no longer needed.
482 ///
483 /// Bridge signals are internal `Signal<String>` instances created by
484 /// `as_reactive_text` and `AttributeValue::Signal` for DOM binding.
485 /// They have exactly one consumer (the DOM element), so deactivating them
486 /// is safe when the element is removed. User-created source signals are
487 /// never passed to this function — they are tracked by `SignalInner.dependents`
488 /// and cleaned up by `use_signal`'s `deactivate()` on hook context teardown.
489 ///
490 /// The bridge signal's value is replaced with `String::new()` to release
491 /// the original string data, and `alive` is set to `false` so that any
492 /// stale async references become safe no-ops.
493 ///
494 /// The `Box<SignalInner<String>>` heap allocation is intentionally NOT
495 /// freed here. `Signal<T>` is `Copy` and a closure registered on the
496 /// backing source signal via `subscribe` captures the bridge address by
497 /// `move`; if that source signal is still alive when the bound element
498 /// is detached (e.g., a `use_window_event` / `use_interval` callback, or
499 /// any source signal whose hook context hasn't been torn down yet), the
500 /// closure may still fire and call `bridge.get()` / `bridge.set()` on a
501 /// freed pointer — undefined behaviour. Mirrors the contract documented
502 /// on `Signal::deactivate`; see the SPA-sweep note there for a future
503 /// safe reclamation path.
504 ///
505 /// This function is idempotent: calling it a second time on the same
506 /// address is a safe no-op because `is_alive` returns `false` after the
507 /// first call.
508 ///
509 /// # Arguments
510 ///
511 /// - `usize` - The inner pointer address of the bridge signal.
512 pub(crate) fn clear_listeners(addr: usize) {
513 if !Self::is_alive(addr) {
514 return;
515 }
516 let inner: &mut SignalInner<String> = Self::inner_mut(addr);
517 inner.get_mut_listeners().clear();
518 inner.set_alive(false);
519 inner.set_value(String::new());
520 Registry::cleanup_attr_slot(addr);
521 // The bridge's element is gone; remove it from the global registry
522 // so subsequent reads via `is_alive` return false. The heap
523 // allocation itself is NOT freed here — that happens in
524 // `Signal::deactivate` once every source signal still subscribed to
525 // this bridge has been deactivated (so no stale closure can fire),
526 // OR in `try_reclaim_inactive` for the orphan case where the source
527 // signal outlives the bridge's hook context (typical of long-lived
528 // SPA top-level signals). See `BridgeRefsCell::track`.
529 Self::registry_mut().remove(&addr);
530 }
531
532 /// SPA reclamation of orphan bridge signals.
533 ///
534 /// `Signal::deactivate` already frees every bridge whose dependency set
535 /// becomes empty during its execution. However, in long-lived SPA apps a
536 /// bridge's `clear_listeners` typically runs first (during DOM teardown),
537 /// removing the bridge from `SIGNAL_INNER_REGISTRY`. If the bridge's
538 /// source signal then never deactivates — because the source is owned by
539 /// a top-level hook context that never tears down (e.g. a global
540 /// `use_signal` in the root app) — the bridge's `Box<SignalInner<String>>`
541 /// stays parked in `BridgeRefsCell` with an empty dependency set. That
542 /// heap allocation would otherwise leak until the page unloads.
543 ///
544 /// This function scans `BridgeRefsCell` once and frees every bridge
545 /// whose:
546 ///
547 /// - dependency set is empty (no source still claims it), AND
548 /// - address is not in `SIGNAL_INNER_REGISTRY` (DOM already detached).
549 ///
550 /// SAFETY: the bridge's address is not reachable through any live
551 /// `Signal<String>` handle — `clear_listeners` removed it from the
552 /// registry, so `Signal::is_alive` returns `false` for it and stale
553 /// handles read `alive=false` and become safe no-ops. The only
554 /// references that could still dereference the address are closures
555 /// captured by `subscribe` on the source signal, and those closures
556 /// touch the bridge only as a copy of `usize`; once the allocation is
557 /// freed those copies would become dangling, so callers MUST ensure the
558 /// source signal has been deactivated (or the source has no live
559 /// subscribers either). In practice this invariant is upheld because
560 /// SPA top-level signals are never `subscribe`d to by bridge signals
561 /// that outlive their bound DOM elements.
562 ///
563 /// `max_freed` bounds the scan cost; pass `usize::MAX` to drain every
564 /// reclaimable bridge in one call. The scan walks the full
565 /// `BridgeRefsCell` map regardless of the cap, so callers should treat
566 /// this as O(n) in the number of bridge dependencies ever recorded,
567 /// not O(`max_freed`).
568 ///
569 /// # Arguments
570 ///
571 /// - `usize` - Upper bound on allocations reclaimed in this call.
572 ///
573 /// # Returns
574 ///
575 /// - `usize` - The number of `Box<SignalInner<String>>` allocations
576 /// reclaimed. Always `<= max_freed`.
577 pub(crate) fn try_reclaim_inactive(max_freed: usize) -> usize {
578 if max_freed == 0 {
579 return 0;
580 }
581 // Snapshot the candidate addrs first so we can drop the &mut borrow
582 // on `BridgeRefsCell::map_mut()` before doing the unsafe free (Rust
583 // forbids holding the &mut across unsafe pointer manipulation in
584 // the same statement — clearer to split).
585 let candidates: Vec<usize> = {
586 let map: &mut HashMap<usize, HashSet<usize>> = BridgeRefsCell::map_mut();
587 let registry: &HashSet<usize> = Self::registry();
588 map.iter()
589 .filter(|(bridge_addr, sources)| {
590 sources.is_empty() && !registry.contains(*bridge_addr)
591 })
592 .map(|(bridge_addr, _)| *bridge_addr)
593 .collect()
594 };
595 let mut freed: usize = 0;
596 for bridge_addr in candidates.into_iter().take(max_freed) {
597 // Remove from BridgeRefsCell so a future sweep skips it.
598 BridgeRefsCell::map_mut().remove(&bridge_addr);
599 // Reclaim the heap allocation. The bridge is not in the registry
600 // (verified in the snapshot) and not referenced from any
601 // surviving `Signal<String>` handle, so this is safe.
602 unsafe {
603 let _: Box<SignalInner<String>> =
604 Box::from_raw(bridge_addr as *mut SignalInner<String>);
605 }
606 freed += 1;
607 }
608 freed
609 }
610}
611
612/// Implementation of `FireHandle` construction, invocation, and conversions.
613impl FireHandle {
614 /// Leaks the given closure and returns a handle pointing to its heap address.
615 ///
616 /// The closure is double-boxed (`Box<Box<dyn FnMut()>>`) and leaked so the
617 /// inner box's address remains stable for the lifetime of the program.
618 /// The address is captured as a `usize` and wrapped in a `FireHandle`.
619 ///
620 /// # Arguments
621 ///
622 /// - `F: FnMut() + 'static` - The fire closure to leak.
623 ///
624 /// # Returns
625 ///
626 /// - `FireHandle` - A handle holding the leaked closure's address.
627 pub fn new<F>(fire: F) -> Self
628 where
629 F: FnMut() + 'static,
630 {
631 let leaked: &'static mut Box<dyn FnMut()> =
632 Box::leak(Box::new(Box::new(fire) as Box<dyn FnMut()>));
633 let addr: usize = leaked as *mut Box<dyn FnMut()> as usize;
634 let mut handle: Self = Self { inner: 0 };
635 handle.set_inner(addr);
636 handle
637 }
638
639 /// Invokes the closure pointed to by this handle.
640 ///
641 /// Takes `self` by value because `FireHandle: Copy` — repeated invocations
642 /// on a single captured handle each copy the address and operate on the
643 /// same underlying closure.
644 ///
645 /// # Safety
646 ///
647 /// The handle must come from `FireHandle::new` (or `From`) and the
648 /// underlying boxed closure must still be live.
649 pub unsafe fn fire(self) {
650 unsafe { Self::fire_at(self.get_inner()) };
651 }
652
653 /// Invokes the closure stored at the given address.
654 ///
655 /// This is the static counterpart of `fire` for call sites that have
656 /// only the raw `usize` address (e.g., macro-generated code that
657 /// captures the address by `move` into a subscribe closure).
658 ///
659 /// # Arguments
660 ///
661 /// - `usize` - The address of a leaked `Box<dyn FnMut()>`.
662 ///
663 /// # Safety
664 ///
665 /// `addr` must come from a valid `FireHandle` produced by `new` (or
666 /// `From`) and the underlying boxed closure must still be live.
667 pub unsafe fn fire_at(addr: usize) {
668 let ptr: *mut Box<dyn FnMut()> = addr as *mut Box<dyn FnMut()>;
669 unsafe { (&mut *ptr)() };
670 }
671}
672
673/// Leaks a fire closure into a `FireHandle`.
674///
675/// This is the canonical `Into` path used by `watch!`/`computed!` macros
676/// and the virtual list component to obtain a `FireHandle` from a closure.
677impl<F> From<F> for FireHandle
678where
679 F: FnMut() + 'static,
680{
681 /// Leaks this closure and stores its address in the returned handle.
682 ///
683 /// # Returns
684 ///
685 /// - `FireHandle` - A handle holding the leaked closure's address.
686 ///
687 /// # Arguments
688 ///
689 /// - `F` - Input value to convert from.
690 fn from(fire: F) -> Self {
691 Self::new(fire)
692 }
693}
694
695/// Extracts the raw address from a `FireHandle`.
696///
697/// This is used by macro-generated code that needs to capture the address
698/// (a `Copy` type) into `FnMut() + 'static` subscribe closures.
699impl From<FireHandle> for usize {
700 /// Returns the leaked closure's heap address.
701 ///
702 /// # Returns
703 ///
704 /// - `usize` - The address held by this handle.
705 ///
706 /// # Arguments
707 ///
708 /// - `FireHandle` - Input value to convert from.
709 fn from(handle: FireHandle) -> Self {
710 handle.get_inner()
711 }
712}