Skip to main content

nexus_rt/
callback.rs

1//! Context-owning handler dispatch.
2//!
3//! [`Callback`] is the unified dispatch type in nexus-rt. Context-free
4//! handlers use `Callback<(), CtxFree<F>, P>` (via [`IntoHandler`](crate::IntoHandler));
5//! context-owning handlers use `Callback<C, F, P>` (via [`IntoCallback`]).
6//!
7//! The function convention for context-owning callbacks is
8//! `fn handler(ctx: &mut C, params: Res<T>..., event: E)` — context first,
9//! [`Param`]-resolved resources in the middle, event last.
10//!
11//! Same HRTB double-bound pattern, same macro generation, same ~2-cycle
12//! dispatch. Named functions only (same closure limitation as
13//! [`IntoHandler`](crate::IntoHandler)).
14//!
15//! # When to use Callback
16//!
17//! Use [`Callback`] over [`IntoHandler`](crate::IntoHandler) when each
18//! handler instance needs its own private state that isn't shared via
19//! [`World`](crate::World):
20//!
21//! - **Per-timer context** — each timer carries its own order ID, retry
22//!   count, or deadline metadata.
23//! - **Per-connection state** — each socket handler carries its own codec
24//!   state, read buffer, or session context.
25//! - **Protocol state machines** — each handler instance tracks its own
26//!   position in a protocol handshake or reconnection sequence.
27//!
28//! The `ctx` field is `pub`, so drivers can read or mutate it between
29//! dispatches (e.g. to update a deadline or check a counter).
30//!
31//! # Returning callbacks from functions (Rust 2024)
32//!
33//! When a factory function takes `&Registry` and returns `impl Handler<E>`,
34//! Rust 2024 captures the registry borrow. Use `+ use<...>` to exclude it:
35//!
36//! ```ignore
37//! fn build_callback(
38//!     ctx: MyCtx,
39//!     reg: &Registry,
40//! ) -> impl Handler<DataEvent> + use<> {
41//!     on_data.into_callback(ctx, reg)
42//! }
43//! ```
44//!
45//! See the [crate-level docs](crate#returning-impl-handler-from-functions-rust-2024)
46//! for the full explanation.
47
48use crate::Handler;
49use crate::handler::Param;
50use crate::world::{Registry, World};
51
52// =============================================================================
53// Callback<C, F, Params>
54// =============================================================================
55
56/// Unified dispatch type. Stores per-callback context alongside
57/// pre-resolved resource access.
58///
59/// - Context-free handlers: `Callback<(), CtxFree<F>, P>` — created via
60///   [`IntoHandler::into_handler`](crate::IntoHandler::into_handler).
61/// - Context-owning handlers: `Callback<C, F, P>` — created via
62///   [`IntoCallback::into_callback`].
63///
64/// Both implement [`Handler<E>`].
65///
66/// # Examples
67///
68/// ```
69/// use nexus_rt::{WorldBuilder, ResMut, IntoCallback, Handler, Resource};
70///
71/// #[derive(Resource)]
72/// struct Counter(u64);
73///
74/// struct Ctx { count: u64 }
75///
76/// fn handler(ctx: &mut Ctx, mut val: ResMut<Counter>, event: u32) {
77///     val.0 += event as u64;
78///     ctx.count += 1;
79/// }
80///
81/// let mut builder = WorldBuilder::new();
82/// builder.register(Counter(0));
83/// let mut world = builder.build();
84///
85/// let mut cb = handler.into_callback(Ctx { count: 0 }, world.registry());
86/// cb.run(&mut world, 10u32);
87///
88/// assert_eq!(cb.ctx.count, 1);
89/// assert_eq!(world.resource::<Counter>().0, 10);
90/// ```
91pub struct Callback<C, F, Params: Param> {
92    /// Per-callback owned state. Accessible outside dispatch.
93    pub ctx: C,
94    pub(crate) f: F,
95    pub(crate) state: Params::State,
96    pub(crate) name: &'static str,
97}
98
99// =============================================================================
100// IntoCallback
101// =============================================================================
102
103/// Converts a named function into a [`Callback`].
104///
105/// Identical to [`IntoHandler`](crate::IntoHandler) but injects `&mut C` as
106/// the first parameter. [`ResourceId`](crate::ResourceId)s resolved via
107/// `registry.id::<T>()` at call time — panics if any resource is not
108/// registered.
109///
110/// Use `IntoCallback` when each handler instance needs its own private
111/// state. For stateless handlers (or state shared via [`World`](crate::World)),
112/// prefer [`IntoHandler`](crate::IntoHandler).
113///
114/// # Named functions only
115///
116/// Closures do not work with `IntoCallback` due to Rust's HRTB inference
117/// limitations with GATs. Use named `fn` items instead.
118///
119/// # Examples
120///
121/// ```
122/// use nexus_rt::{WorldBuilder, ResMut, IntoCallback, Handler, Resource};
123///
124/// #[derive(Resource)]
125/// struct Counter(u64);
126///
127/// struct TimerCtx { order_id: u64, fires: u64 }
128///
129/// fn on_timeout(ctx: &mut TimerCtx, mut counter: ResMut<Counter>, _event: ()) {
130///     ctx.fires += 1;
131///     counter.0 += ctx.order_id;
132/// }
133///
134/// let mut builder = WorldBuilder::new();
135/// builder.register(Counter(0));
136/// let mut world = builder.build();
137///
138/// let mut cb = on_timeout.into_callback(
139///     TimerCtx { order_id: 42, fires: 0 },
140///     world.registry(),
141/// );
142/// cb.run(&mut world, ());
143///
144/// assert_eq!(cb.ctx.fires, 1);
145/// assert_eq!(world.resource::<Counter>().0, 42);
146/// ```
147///
148/// # Panics
149///
150/// Panics if any [`Param`] resource is not registered in the
151/// [`Registry`](crate::Registry).
152#[diagnostic::on_unimplemented(
153    message = "this function cannot be converted into a callback",
154    note = "callback signature: `fn(&mut Context, Res<A>, ..., Event)` — context first, then resources, event last",
155    note = "closures with resource parameters are not supported — use a named `fn` when using Param resources"
156)]
157pub trait IntoCallback<C, E, Params> {
158    /// The concrete Callback type produced.
159    type Callback: Handler<E>;
160
161    /// Convert this function + context into a Callback.
162    #[must_use = "the callback must be stored or dispatched — discarding it does nothing"]
163    fn into_callback(self, ctx: C, registry: &Registry) -> Self::Callback;
164}
165
166// =============================================================================
167// Arity 0: fn(ctx: &mut C, E) — context + event only, no Param
168// =============================================================================
169
170impl<C: Send + 'static, E, F: FnMut(&mut C, E) + Send + 'static> IntoCallback<C, E, ()> for F {
171    type Callback = Callback<C, F, ()>;
172
173    fn into_callback(self, ctx: C, registry: &Registry) -> Self::Callback {
174        Callback {
175            ctx,
176            f: self,
177            state: <() as Param>::init(registry),
178            name: std::any::type_name::<F>(),
179        }
180    }
181}
182
183impl<C: Send + 'static, E, F: FnMut(&mut C, E) + Send + 'static> Handler<E> for Callback<C, F, ()> {
184    fn run(&mut self, _world: &mut World, event: E) {
185        (self.f)(&mut self.ctx, event);
186    }
187
188    fn name(&self) -> &'static str {
189        self.name
190    }
191}
192
193// =============================================================================
194// Macro-generated impls (arities 1-8)
195// =============================================================================
196
197macro_rules! impl_into_callback {
198    ($($P:ident),+) => {
199        impl<C: Send + 'static, E, F: Send + 'static, $($P: Param + 'static),+>
200            IntoCallback<C, E, ($($P,)+)> for F
201        where
202            for<'a> &'a mut F:
203                FnMut(&mut C, $($P,)+ E) +
204                FnMut(&mut C, $($P::Item<'a>,)+ E),
205        {
206            type Callback = Callback<C, F, ($($P,)+)>;
207
208            fn into_callback(self, ctx: C, registry: &Registry) -> Self::Callback {
209                let state = <($($P,)+) as Param>::init(registry);
210                {
211                    #[allow(non_snake_case)]
212                    let ($($P,)+) = &state;
213                    registry.check_access(&[
214                        $(
215                            (<$P as Param>::resource_id($P),
216                             std::any::type_name::<$P>()),
217                        )+
218                    ]);
219                }
220                Callback { ctx, f: self, state, name: std::any::type_name::<F>() }
221            }
222        }
223
224        impl<C: Send + 'static, E, F: Send + 'static, $($P: Param + 'static),+>
225            Handler<E> for Callback<C, F, ($($P,)+)>
226        where
227            for<'a> &'a mut F:
228                FnMut(&mut C, $($P,)+ E) +
229                FnMut(&mut C, $($P::Item<'a>,)+ E),
230        {
231            #[allow(non_snake_case)]
232            fn run(&mut self, world: &mut World, event: E) {
233                #[allow(clippy::too_many_arguments)]
234                fn call_inner<Ctx, $($P,)+ Ev>(
235                    mut f: impl FnMut(&mut Ctx, $($P,)+ Ev),
236                    ctx: &mut Ctx,
237                    $($P: $P,)+
238                    event: Ev,
239                ) {
240                    f(ctx, $($P,)+ event);
241                }
242
243                // SAFETY: state was produced by init() on the same registry
244                // that built this world. Single-threaded sequential dispatch
245                // ensures no mutable aliasing across params.
246                #[cfg(debug_assertions)]
247                world.clear_borrows();
248                let ($($P,)+) = unsafe {
249                    <($($P,)+) as Param>::fetch(world, &mut self.state)
250                };
251                call_inner(&mut self.f, &mut self.ctx, $($P,)+ event);
252            }
253
254            fn name(&self) -> &'static str {
255                self.name
256            }
257        }
258    };
259}
260
261all_tuples!(impl_into_callback);
262
263// =============================================================================
264// Tests
265// =============================================================================
266
267#[cfg(test)]
268mod tests {
269    use super::*;
270    use crate::{Local, Res, ResMut, WorldBuilder};
271
272    // -- Helper types ---------------------------------------------------------
273
274    struct TimerCtx {
275        order_id: u64,
276        call_count: u64,
277    }
278
279    struct OrderCache {
280        expired: Vec<u64>,
281    }
282    impl crate::world::Resource for OrderCache {}
283
284    // -- Core dispatch --------------------------------------------------------
285
286    fn ctx_only_handler(ctx: &mut TimerCtx, _event: u32) {
287        ctx.call_count += 1;
288    }
289
290    #[test]
291    fn ctx_only_no_params() {
292        let mut world = WorldBuilder::new().build();
293        let mut cb = ctx_only_handler.into_callback(
294            TimerCtx {
295                order_id: 1,
296                call_count: 0,
297            },
298            world.registry_mut(),
299        );
300        cb.run(&mut world, 42u32);
301        assert_eq!(cb.ctx.call_count, 1);
302    }
303
304    fn ctx_one_res_handler(ctx: &mut TimerCtx, cache: Res<OrderCache>, _event: u32) {
305        ctx.call_count += cache.expired.len() as u64;
306    }
307
308    #[test]
309    fn ctx_one_res() {
310        let mut builder = WorldBuilder::new();
311        builder.register::<OrderCache>(OrderCache {
312            expired: vec![1, 2, 3],
313        });
314        let mut world = builder.build();
315
316        let mut cb = ctx_one_res_handler.into_callback(
317            TimerCtx {
318                order_id: 1,
319                call_count: 0,
320            },
321            world.registry_mut(),
322        );
323        cb.run(&mut world, 0u32);
324        assert_eq!(cb.ctx.call_count, 3);
325    }
326
327    fn ctx_one_res_mut_handler(ctx: &mut TimerCtx, mut cache: ResMut<OrderCache>, _event: u32) {
328        cache.expired.push(ctx.order_id);
329        ctx.call_count += 1;
330    }
331
332    #[test]
333    fn ctx_one_res_mut() {
334        let mut builder = WorldBuilder::new();
335        builder.register::<OrderCache>(OrderCache { expired: vec![] });
336        let mut world = builder.build();
337
338        let mut cb = ctx_one_res_mut_handler.into_callback(
339            TimerCtx {
340                order_id: 42,
341                call_count: 0,
342            },
343            world.registry_mut(),
344        );
345        cb.run(&mut world, 0u32);
346        assert_eq!(cb.ctx.call_count, 1);
347        assert_eq!(world.resource::<OrderCache>().expired, vec![42]);
348    }
349
350    fn ctx_two_params_handler(
351        ctx: &mut TimerCtx,
352        counter: Res<u64>,
353        mut cache: ResMut<OrderCache>,
354        _event: u32,
355    ) {
356        cache.expired.push(*counter);
357        ctx.call_count += 1;
358    }
359
360    #[test]
361    fn ctx_two_params() {
362        let mut builder = WorldBuilder::new();
363        builder.register::<u64>(99);
364        builder.register::<OrderCache>(OrderCache { expired: vec![] });
365        let mut world = builder.build();
366
367        let mut cb = ctx_two_params_handler.into_callback(
368            TimerCtx {
369                order_id: 0,
370                call_count: 0,
371            },
372            world.registry_mut(),
373        );
374        cb.run(&mut world, 0u32);
375        assert_eq!(cb.ctx.call_count, 1);
376        assert_eq!(world.resource::<OrderCache>().expired, vec![99]);
377    }
378
379    fn ctx_three_params_handler(
380        ctx: &mut TimerCtx,
381        a: Res<u64>,
382        b: Res<bool>,
383        mut c: ResMut<OrderCache>,
384        _event: u32,
385    ) {
386        if *b {
387            c.expired.push(*a);
388        }
389        ctx.call_count += 1;
390    }
391
392    #[test]
393    fn ctx_three_params() {
394        let mut builder = WorldBuilder::new();
395        builder.register::<u64>(7);
396        builder.register::<bool>(true);
397        builder.register::<OrderCache>(OrderCache { expired: vec![] });
398        let mut world = builder.build();
399
400        let mut cb = ctx_three_params_handler.into_callback(
401            TimerCtx {
402                order_id: 0,
403                call_count: 0,
404            },
405            world.registry_mut(),
406        );
407        cb.run(&mut world, 0u32);
408        assert_eq!(cb.ctx.call_count, 1);
409        assert_eq!(world.resource::<OrderCache>().expired, vec![7]);
410    }
411
412    // -- Context ownership ----------------------------------------------------
413
414    #[test]
415    fn ctx_mutated_persists() {
416        let mut world = WorldBuilder::new().build();
417        let mut cb = ctx_only_handler.into_callback(
418            TimerCtx {
419                order_id: 1,
420                call_count: 0,
421            },
422            world.registry_mut(),
423        );
424        cb.run(&mut world, 0u32);
425        cb.run(&mut world, 0u32);
426        cb.run(&mut world, 0u32);
427        assert_eq!(cb.ctx.call_count, 3);
428    }
429
430    #[test]
431    fn ctx_accessible_outside_dispatch() {
432        let mut world = WorldBuilder::new().build();
433        let mut cb = ctx_only_handler.into_callback(
434            TimerCtx {
435                order_id: 42,
436                call_count: 0,
437            },
438            world.registry_mut(),
439        );
440        assert_eq!(cb.ctx.order_id, 42);
441        assert_eq!(cb.ctx.call_count, 0);
442        cb.run(&mut world, 0u32);
443        assert_eq!(cb.ctx.call_count, 1);
444    }
445
446    #[test]
447    fn ctx_mutated_outside_dispatch() {
448        let mut world = WorldBuilder::new().build();
449        let mut cb = ctx_only_handler.into_callback(
450            TimerCtx {
451                order_id: 1,
452                call_count: 0,
453            },
454            world.registry_mut(),
455        );
456        cb.ctx.order_id = 99;
457        cb.run(&mut world, 0u32);
458        assert_eq!(cb.ctx.order_id, 99);
459        assert_eq!(cb.ctx.call_count, 1);
460    }
461
462    // -- Safety validation ----------------------------------------------------
463
464    #[test]
465    #[should_panic(expected = "not registered")]
466    fn panics_on_missing_resource() {
467        let mut world = WorldBuilder::new().build();
468
469        fn needs_cache(_ctx: &mut TimerCtx, _cache: Res<OrderCache>, _e: u32) {}
470
471        let _cb = needs_cache.into_callback(
472            TimerCtx {
473                order_id: 0,
474                call_count: 0,
475            },
476            world.registry_mut(),
477        );
478    }
479
480    #[test]
481    #[should_panic(expected = "not registered")]
482    fn panics_on_second_missing() {
483        let mut builder = WorldBuilder::new();
484        builder.register::<u64>(0);
485        let mut world = builder.build();
486
487        fn needs_two(_ctx: &mut TimerCtx, _a: Res<u64>, _b: Res<OrderCache>, _e: u32) {}
488
489        let _cb = needs_two.into_callback(
490            TimerCtx {
491                order_id: 0,
492                call_count: 0,
493            },
494            world.registry_mut(),
495        );
496    }
497
498    // -- Access conflict detection --------------------------------------------
499
500    #[test]
501    #[should_panic(expected = "conflicting access")]
502    fn callback_duplicate_access_panics() {
503        let mut builder = WorldBuilder::new();
504        builder.register::<u64>(0);
505        let mut world = builder.build();
506
507        fn bad(_ctx: &mut u64, a: Res<u64>, b: ResMut<u64>, _e: ()) {
508            let _ = (*a, &*b);
509        }
510
511        let _cb = bad.into_callback(0u64, world.registry_mut());
512    }
513
514    // -- Handler<E> interface -------------------------------------------------
515
516    #[test]
517    fn box_dyn_handler() {
518        let mut builder = WorldBuilder::new();
519        builder.register::<u64>(0);
520        let mut world = builder.build();
521
522        fn add_ctx(ctx: &mut u64, mut val: ResMut<u64>, event: u64) {
523            *val += event + *ctx;
524        }
525
526        let cb = add_ctx.into_callback(10u64, world.registry_mut());
527        let mut boxed: Box<dyn Handler<u64>> = Box::new(cb);
528        boxed.run(&mut world, 5u64);
529        // 0 + 5 + 10 = 15
530        assert_eq!(*world.resource::<u64>(), 15);
531    }
532
533    #[test]
534    fn callback_in_vec_dyn() {
535        let mut builder = WorldBuilder::new();
536        builder.register::<u64>(0);
537        let mut world = builder.build();
538
539        fn add(ctx: &mut u64, mut val: ResMut<u64>, _e: ()) {
540            *val += *ctx;
541        }
542        fn mul(ctx: &mut u64, mut val: ResMut<u64>, _e: ()) {
543            *val *= *ctx;
544        }
545
546        let cb_add = add.into_callback(3u64, world.registry_mut());
547        let cb_mul = mul.into_callback(2u64, world.registry_mut());
548
549        let mut handlers: Vec<Box<dyn Handler<()>>> = vec![Box::new(cb_add), Box::new(cb_mul)];
550
551        for h in &mut handlers {
552            h.run(&mut world, ());
553        }
554        // 0 + 3 = 3, then 3 * 2 = 6
555        assert_eq!(*world.resource::<u64>(), 6);
556    }
557
558    fn with_local(_ctx: &mut u64, mut local: Local<u64>, mut val: ResMut<u64>, _e: ()) {
559        *local += 1;
560        *val = *local;
561    }
562
563    #[test]
564    fn callback_with_local() {
565        let mut builder = WorldBuilder::new();
566        builder.register::<u64>(0);
567        let mut world = builder.build();
568
569        let mut cb = with_local.into_callback(0u64, world.registry_mut());
570        cb.run(&mut world, ());
571        cb.run(&mut world, ());
572        cb.run(&mut world, ());
573        assert_eq!(*world.resource::<u64>(), 3);
574    }
575
576    // -- Integration ----------------------------------------------------------
577
578    #[test]
579    fn callback_interop_with_handler() {
580        use crate::IntoHandler;
581
582        let mut builder = WorldBuilder::new();
583        builder.register::<u64>(0);
584        let mut world = builder.build();
585
586        fn sys_add(mut val: ResMut<u64>, event: u64) {
587            *val += event;
588        }
589        fn cb_mul(ctx: &mut u64, mut val: ResMut<u64>, _e: u64) {
590            *val *= *ctx;
591        }
592
593        let sys = sys_add.into_handler(world.registry_mut());
594        let cb = cb_mul.into_callback(3u64, world.registry_mut());
595
596        let mut handlers: Vec<Box<dyn Handler<u64>>> = vec![Box::new(sys), Box::new(cb)];
597
598        for h in &mut handlers {
599            h.run(&mut world, 5u64);
600        }
601        // 0 + 5 = 5, then 5 * 3 = 15
602        assert_eq!(*world.resource::<u64>(), 15);
603    }
604}