Skip to main content

ferroui_hot_patch/
lib.rs

1//! Explicit synchronous patch boundaries and conservative source admission.
2//! A supervisor must validate the entire build input set, not just a changed function.
3#[cfg(debug_assertions)]
4mod supervisor;
5#[cfg(debug_assertions)]
6pub use subsecond::{aslr_reference, JumpTable};
7#[cfg(debug_assertions)]
8pub use supervisor::{AdmittedPatch, PatchSupervisor, SourceSnapshot};
9#[cfg(debug_assertions)]
10mod registration;
11#[cfg(debug_assertions)]
12pub use registration::FunctionRegistration;
13mod admission;
14pub use admission::{admit, Admission};
15/// Invoke at the callback boundary, each time the callback runs.
16#[inline(always)]
17pub fn hot_reloadable<R>(function: impl FnMut() -> R) -> R {
18    #[cfg(debug_assertions)]
19    {
20        let _exit = CallbackGuard::enter();
21        subsecond::call(function)
22    }
23    #[cfg(not(debug_assertions))]
24    {
25        let mut function = function;
26        function()
27    }
28}
29
30/// Invoke a registered synchronous function with a handler or command argument.
31/// Pass a tuple for multiple arguments. Call this at every invocation of the
32/// existing subscription; the function pointer is resolved through the current
33/// patch table each time. Argument and return types must live in stable code.
34/// This does not admit a function or rebuild its view: the owning supervisor
35/// still validates the complete source revision before installing a patch.
36#[inline(always)]
37pub fn hot_reloadable_with<A, R>(function: fn(A) -> R, argument: A) -> R {
38    #[cfg(debug_assertions)]
39    {
40        let _exit = CallbackGuard::enter();
41        subsecond::HotFn::current(function).call((argument,))
42    }
43    #[cfg(not(debug_assertions))]
44    {
45        function(argument)
46    }
47}
48
49#[cfg(debug_assertions)]
50struct CallbackGuard;
51#[cfg(debug_assertions)]
52impl CallbackGuard {
53    fn enter() -> Self {
54        DEPTH.with(|depth| depth.set(depth.get() + 1));
55        Self
56    }
57}
58#[cfg(debug_assertions)]
59impl Drop for CallbackGuard {
60    fn drop(&mut self) {
61        DEPTH.with(|depth| depth.set(depth.get() - 1));
62    }
63}
64
65#[cfg(debug_assertions)]
66ferroui_system::ferro_thread_local! { static DEPTH: std::cell::Cell<usize> = const { std::cell::Cell::new(0) }; }
67#[cfg(debug_assertions)]
68fn in_callback() -> bool {
69    DEPTH.with(|depth| depth.get() != 0)
70}
71
72#[cfg(debug_assertions)]
73pub mod restart;
74
75#[cfg(debug_assertions)]
76pub mod inputs;
77
78#[cfg(all(test, debug_assertions))]
79mod callback_tests {
80    use super::*;
81
82    #[test]
83    fn typed_callback_preserves_arguments_and_nested_quiescence() {
84        fn command((mut text, delta): (String, usize)) -> String {
85            assert!(in_callback());
86            hot_reloadable(|| assert!(in_callback()));
87            assert!(in_callback());
88            text.push_str(&delta.to_string());
89            text
90        }
91        assert!(!in_callback());
92        assert_eq!(hot_reloadable_with(command, ("count=".into(), 3)), "count=3");
93        assert!(!in_callback());
94        fn handler(value: &mut i32) {
95            assert!(in_callback());
96            *value += 1;
97        }
98        let mut value = 7;
99        hot_reloadable_with(handler, &mut value);
100        assert_eq!(value, 8);
101        assert!(!in_callback());
102    }
103
104    #[test]
105    fn panicking_typed_callback_releases_quiescence_guard() {
106        fn handler(_: ()) {
107            assert!(in_callback());
108            panic!("application handler failed");
109        }
110        assert!(std::panic::catch_unwind(|| hot_reloadable_with(handler, ())).is_err());
111        assert!(!in_callback());
112    }
113}