Skip to main content

nexus_rt/
catch_unwind.rs

1//! Panic-catching annotation for handlers.
2
3use std::panic::AssertUnwindSafe;
4
5use crate::handler::Handler;
6use crate::world::World;
7
8/// Panic-catching wrapper for [`Handler`] implementations.
9///
10/// Catches panics during [`run()`](Handler::run) so the handler is never
11/// lost during move-out-fire dispatch. This is an annotation — wrap a
12/// concrete handler, then virtualize through your chosen storage (`Box`,
13/// `Flat`, `Flex`, typed slab, etc.).
14///
15/// By constructing this wrapper, the user asserts that the inner handler
16/// (and any [`World`] resources it mutates) can tolerate partial writes
17/// caused by an unwound `run()` call. This is the same assertion as
18/// [`std::panic::AssertUnwindSafe`], applied at the handler level.
19///
20/// # Examples
21///
22/// ```
23/// use nexus_rt::{CatchAssertUnwindSafe, WorldBuilder, ResMut, IntoHandler, Handler, Virtual, Resource};
24///
25/// #[derive(Resource)]
26/// struct Counter(u64);
27///
28/// fn tick(mut counter: ResMut<Counter>, event: u32) {
29///     counter.0 += event as u64;
30/// }
31///
32/// let mut builder = WorldBuilder::new();
33/// builder.register(Counter(0));
34/// let mut world = builder.build();
35///
36/// let handler = tick.into_handler(world.registry());
37/// let guarded = CatchAssertUnwindSafe::new(handler);
38/// let mut boxed: Virtual<u32> = Box::new(guarded);
39///
40/// boxed.run(&mut world, 10);
41/// assert_eq!(world.resource::<Counter>().0, 10);
42/// ```
43pub struct CatchAssertUnwindSafe<H> {
44    handler: H,
45}
46
47impl<H> CatchAssertUnwindSafe<H> {
48    /// Wrap a handler with panic catching.
49    ///
50    /// The caller asserts that the handler and any resources it touches
51    /// are safe to continue using after a caught panic.
52    pub fn new(handler: H) -> Self {
53        Self { handler }
54    }
55}
56
57impl<E, H: Handler<E>> Handler<E> for CatchAssertUnwindSafe<H> {
58    fn run(&mut self, world: &mut World, event: E) {
59        let handler = &mut self.handler;
60        let _ = std::panic::catch_unwind(AssertUnwindSafe(|| {
61            handler.run(world, event);
62        }));
63    }
64
65    fn name(&self) -> &'static str {
66        self.handler.name()
67    }
68}
69
70#[cfg(test)]
71mod tests {
72    use super::*;
73    use crate::{IntoHandler, ResMut, WorldBuilder};
74
75    fn normal_handler(mut val: ResMut<u64>, event: u64) {
76        *val += event;
77    }
78
79    #[test]
80    fn forwards_run() {
81        let mut builder = WorldBuilder::new();
82        builder.register::<u64>(0);
83        let mut world = builder.build();
84
85        let handler = normal_handler.into_handler(world.registry());
86        let mut guarded = CatchAssertUnwindSafe::new(handler);
87
88        guarded.run(&mut world, 10);
89        assert_eq!(*world.resource::<u64>(), 10);
90    }
91
92    fn panicking_handler(_val: ResMut<u64>, _event: u64) {
93        panic!("boom");
94    }
95
96    #[test]
97    fn survives_panic() {
98        let mut builder = WorldBuilder::new();
99        builder.register::<u64>(0);
100        let mut world = builder.build();
101
102        let handler = panicking_handler.into_handler(world.registry());
103        let mut guarded = CatchAssertUnwindSafe::new(handler);
104
105        // Should not panic — caught internally.
106        guarded.run(&mut world, 10);
107
108        // Handler survives — can be called again.
109        guarded.run(&mut world, 10);
110    }
111
112    fn partial_write_then_panic(mut val: ResMut<u64>, event: u64) {
113        *val += event;
114        panic!("mid-flight");
115    }
116
117    #[test]
118    fn world_state_survives_handler_panic() {
119        let mut builder = WorldBuilder::new();
120        builder.register::<u64>(0);
121        let mut world = builder.build();
122
123        let handler = partial_write_then_panic.into_handler(world.registry());
124        let mut guarded = CatchAssertUnwindSafe::new(handler);
125
126        // First call: writes 10, then panics. The write is visible.
127        guarded.run(&mut world, 10);
128        assert_eq!(*world.resource::<u64>(), 10);
129
130        // World is not corrupted — normal handler works afterwards.
131        let handler2 = normal_handler.into_handler(world.registry());
132        let mut guarded2 = CatchAssertUnwindSafe::new(handler2);
133        guarded2.run(&mut world, 5);
134        assert_eq!(*world.resource::<u64>(), 15);
135
136        // The panicking handler can be called again.
137        guarded.run(&mut world, 3);
138        assert_eq!(*world.resource::<u64>(), 18);
139    }
140
141    #[test]
142    fn forwards_name() {
143        let mut builder = WorldBuilder::new();
144        builder.register::<u64>(0);
145        let world = builder.build();
146
147        let handler = normal_handler.into_handler(world.registry());
148        let guarded = CatchAssertUnwindSafe::new(handler);
149
150        assert!(guarded.name().contains("normal_handler"));
151    }
152}