Skip to main content

nexus_async_rt/
world_ctx.rs

1//! Lightweight handle for synchronous [`World`](nexus_rt::World) access from
2//! async tasks.
3//!
4//! [`WorldCtx`] wraps a raw pointer to a [`World`](nexus_rt::World). It is
5//! [`Copy`] so tasks can capture it cheaply (8 bytes). The scoped
6//! [`with_world`](WorldCtx::with_world) API runs a closure synchronously
7//! inline during task poll — no await point needed.
8//!
9//! # Pre-resolved parameters
10//!
11//! Closures going through [`IntoHandler`](nexus_rt::IntoHandler) resolve
12//! [`ResourceId`](nexus_rt::ResourceId)s at build time (one HashMap lookup
13//! per type). At dispatch time, each resource access is a single pointer
14//! deref. Build the handler before spawning, move it into the task, and
15//! call [`Handler::run`](nexus_rt::Handler::run) inside `with_world`.
16
17use nexus_rt::World;
18
19/// [`Copy`] handle for synchronous [`World`] access from async tasks.
20///
21/// # Safety Contract
22///
23/// - **Single-threaded only.** No concurrent `with_world` calls.
24/// - **World outlives tasks.** The [`World`] must not be dropped while
25///   any task holds a `WorldCtx`.
26///
27/// Both invariants are enforced structurally by the single-threaded
28/// executor: only one task polls at a time, and the user owns the
29/// [`World`] alongside the executor in the same scope.
30///
31/// # Examples
32///
33/// ```ignore
34/// use nexus_async_rt::{Executor, WorldCtx};
35/// use nexus_rt::{WorldBuilder, Res, ResMut, IntoHandler, Handler};
36///
37/// let mut world = builder.build();
38/// let ctx = WorldCtx::new(&mut world);
39///
40/// // Pre-resolve at setup — single HashMap lookup per type
41/// let mut on_quote = (|mut books: ResMut<Books>, q: Quote| {
42///     books.update(q);
43/// }).into_handler(world.registry());
44///
45/// let mut executor = Executor::new(64);
46/// executor.spawn_boxed(async move {
47///     let data = read_socket().await;
48///     // Single deref per resource at dispatch time
49///     ctx.with_world(|world| on_quote.run(world, data));
50/// });
51///
52/// executor.drain();
53/// ```
54#[derive(Clone, Copy)]
55pub struct WorldCtx {
56    ptr: *mut World,
57}
58
59impl WorldCtx {
60    /// Create a context handle from a mutable [`World`] reference.
61    ///
62    /// # Safety Contract (enforced by caller, not by the type system)
63    ///
64    /// - The [`World`] must outlive all tasks using this handle.
65    /// - The caller must not use `&mut World` directly while tasks hold
66    ///   a `WorldCtx` — all World access must go through `with_world`.
67    /// - Single-threaded use only (no concurrent `with_world` calls).
68    ///
69    /// These invariants are structurally enforced by [`crate::Runtime`]:
70    /// the World is created before the runtime, `block_on` takes
71    /// `&mut self` preventing direct World access during execution,
72    /// and the single-threaded executor prevents concurrent polls.
73    pub fn new(world: &mut World) -> Self {
74        Self {
75            ptr: std::ptr::from_mut(world),
76        }
77    }
78
79    /// Run a closure with exclusive [`World`] access.
80    ///
81    /// Executes synchronously inline — no await point. The closure
82    /// has `&mut World` access for its duration.
83    /// Returns the raw world pointer. Used by context module.
84    pub(crate) fn as_ptr(&self) -> *mut World {
85        self.ptr
86    }
87
88    pub fn with_world<R>(&self, f: impl FnOnce(&mut World) -> R) -> R {
89        // SAFETY: Single-threaded executor guarantees only one task polls
90        // at a time, so only one with_world is active. World outlives all
91        // tasks (caller invariant from WorldCtx::new).
92        let world = unsafe { &mut *self.ptr };
93        f(world)
94    }
95
96    /// Run a closure with shared [`World`] access.
97    ///
98    /// Use when you only need to read resources.
99    pub fn with_world_ref<R>(&self, f: impl FnOnce(&World) -> R) -> R {
100        // SAFETY: Same invariants as with_world. Shared ref is strictly
101        // less powerful.
102        let world = unsafe { &*self.ptr };
103        f(world)
104    }
105}
106
107#[cfg(test)]
108mod tests {
109    use super::*;
110    use crate::Executor;
111    use nexus_rt::{Handler, IntoHandler, Res, ResMut, WorldBuilder};
112
113    nexus_rt::new_resource!(Val(u64));
114    nexus_rt::new_resource!(Out(u64));
115
116    #[test]
117    fn with_world_raw_access() {
118        let mut wb = WorldBuilder::new();
119        wb.register(Val(42));
120        wb.register(Out(0));
121        let mut world = wb.build();
122        let ctx = WorldCtx::new(&mut world);
123
124        let mut executor = Executor::new(4);
125        executor.spawn_boxed(async move {
126            ctx.with_world(|world| {
127                let v = world.resource::<Val>().0;
128                world.resource_mut::<Out>().0 = v + 10;
129            });
130        });
131
132        executor.drain();
133        assert_eq!(world.resource::<Out>().0, 52);
134    }
135
136    #[test]
137    fn with_world_ref_read_only() {
138        let mut wb = WorldBuilder::new();
139        wb.register(Val(99));
140        let mut world = wb.build();
141        let ctx = WorldCtx::new(&mut world);
142
143        let result = std::cell::Cell::new(0u64);
144        let result_ptr = std::ptr::from_ref(&result);
145
146        let mut executor = Executor::new(4);
147        executor.spawn_boxed(async move {
148            let v = ctx.with_world_ref(|world| world.resource::<Val>().0);
149            // SAFETY: test-only, single-threaded, Cell is alive.
150            unsafe { &*result_ptr }.set(v);
151        });
152
153        executor.drain();
154        assert_eq!(result.get(), 99);
155    }
156
157    #[test]
158    fn with_world_pre_resolved_handler() {
159        let mut wb = WorldBuilder::new();
160        wb.register(Val(42));
161        wb.register(Out(0));
162        let mut world = wb.build();
163        let ctx = WorldCtx::new(&mut world);
164
165        // Pre-resolve: HashMap lookups happen here, once
166        let mut handler = (|val: Res<Val>, mut out: ResMut<Out>, event: u64| {
167            out.0 = val.0 + event;
168        })
169        .into_handler(world.registry());
170
171        let mut executor = Executor::new(4);
172        executor.spawn_boxed(async move {
173            ctx.with_world(|world| handler.run(world, 10));
174        });
175
176        executor.drain();
177        assert_eq!(world.resource::<Out>().0, 52);
178    }
179
180    #[test]
181    fn with_world_returns_value() {
182        let mut wb = WorldBuilder::new();
183        wb.register(Val(7));
184        let mut world = wb.build();
185        let ctx = WorldCtx::new(&mut world);
186
187        let result = std::cell::Cell::new(0u64);
188        let result_ptr = std::ptr::from_ref(&result);
189
190        let mut executor = Executor::new(4);
191        executor.spawn_boxed(async move {
192            let v = ctx.with_world(|world| world.resource::<Val>().0 * 6);
193            // SAFETY: test-only, single-threaded, Cell is alive.
194            unsafe { &*result_ptr }.set(v);
195        });
196
197        executor.drain();
198        assert_eq!(result.get(), 42);
199    }
200
201    #[test]
202    fn multiple_tasks_share_ctx() {
203        let mut wb = WorldBuilder::new();
204        wb.register(Out(0));
205        let mut world = wb.build();
206        let ctx = WorldCtx::new(&mut world);
207
208        let mut executor = Executor::new(4);
209
210        for i in 1..=3u64 {
211            let ctx = ctx; // Copy
212            executor.spawn_boxed(async move {
213                ctx.with_world(|world| {
214                    world.resource_mut::<Out>().0 += i;
215                });
216            });
217        }
218
219        executor.drain();
220        assert_eq!(world.resource::<Out>().0, 6); // 1 + 2 + 3
221    }
222}