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/// while executor.task_count() > 0 { executor.poll(); }
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    /// Returns a [`WorldCtx`] for the currently running runtime.
80    ///
81    /// Reads the world pointer installed by
82    /// [`Runtime::block_on`](crate::Runtime::block_on). The returned handle
83    /// is the same shape as one constructed via [`WorldCtx::new`] — same
84    /// [`Copy`] semantics, same [`with_world`](Self::with_world) /
85    /// [`with_world_ref`](Self::with_world_ref) methods. Mirrors
86    /// `tokio::runtime::Handle::current()`.
87    ///
88    /// Use [`WorldCtx::new`] explicitly when constructing a handle outside
89    /// the runtime context (e.g., capturing into a task before `block_on`).
90    /// Use `current()` when you're already inside a task and want the
91    /// active runtime's world.
92    ///
93    /// # Panics
94    ///
95    /// Panics if called outside a [`Runtime::block_on`](crate::Runtime::block_on)
96    /// context.
97    #[must_use]
98    pub fn current() -> WorldCtx {
99        let ptr = crate::context::current_world_ptr();
100        assert!(
101            !ptr.is_null(),
102            "WorldCtx::current() called outside Runtime::block_on"
103        );
104        Self { ptr }
105    }
106
107    /// Run a closure with exclusive [`World`] access.
108    ///
109    /// Executes synchronously inline — no await point. The closure
110    /// has `&mut World` access for its duration.
111    /// Returns the raw world pointer. Used by context module.
112    pub(crate) fn as_ptr(&self) -> *mut World {
113        self.ptr
114    }
115
116    /// Run a closure with exclusive [`World`] access.
117    ///
118    /// Executes synchronously inline — no await point. The closure has
119    /// `&mut World` for its duration. Use when you need to mutate
120    /// resources; see [`with_world_ref`](Self::with_world_ref) for
121    /// read-only access.
122    pub fn with_world<R>(&self, f: impl FnOnce(&mut World) -> R) -> R {
123        // SAFETY: Single-threaded executor guarantees only one task polls
124        // at a time, so only one with_world is active. World outlives all
125        // tasks (caller invariant from WorldCtx::new).
126        let world = unsafe { &mut *self.ptr };
127        f(world)
128    }
129
130    /// Run a closure with shared [`World`] access.
131    ///
132    /// Use when you only need to read resources.
133    pub fn with_world_ref<R>(&self, f: impl FnOnce(&World) -> R) -> R {
134        // SAFETY: Same invariants as with_world. Shared ref is strictly
135        // less powerful.
136        let world = unsafe { &*self.ptr };
137        f(world)
138    }
139}
140
141#[cfg(test)]
142mod tests {
143    use super::*;
144    use crate::Executor;
145    use nexus_rt::{Handler, IntoHandler, Res, ResMut, WorldBuilder};
146
147    nexus_rt::new_resource!(Val(u64));
148    nexus_rt::new_resource!(Out(u64));
149
150    #[test]
151    #[should_panic(expected = "called outside Runtime::block_on")]
152    fn current_panics_outside_runtime() {
153        // Pins the documented panic contract for `WorldCtx::current()`.
154        // The happy path is exercised transitively by the runtime tests
155        // (every `with_world(...)` call inside a task goes through
156        // `WorldCtx::current()`). This is the direct contract test.
157        let _ = WorldCtx::current();
158    }
159
160    #[test]
161    fn with_world_raw_access() {
162        let mut wb = WorldBuilder::new();
163        wb.register(Val(42));
164        wb.register(Out(0));
165        let mut world = wb.build();
166        let ctx = WorldCtx::new(&mut world);
167
168        let mut executor = Executor::new(4);
169        executor.spawn_boxed(async move {
170            ctx.with_world(|world| {
171                let v = world.resource::<Val>().0;
172                world.resource_mut::<Out>().0 = v + 10;
173            });
174        });
175
176        while executor.task_count() > 0 {
177            executor.poll();
178        }
179        assert_eq!(world.resource::<Out>().0, 52);
180    }
181
182    #[test]
183    fn with_world_ref_read_only() {
184        let mut wb = WorldBuilder::new();
185        wb.register(Val(99));
186        let mut world = wb.build();
187        let ctx = WorldCtx::new(&mut world);
188
189        let result = std::cell::Cell::new(0u64);
190        let result_ptr = std::ptr::from_ref(&result);
191
192        let mut executor = Executor::new(4);
193        executor.spawn_boxed(async move {
194            let v = ctx.with_world_ref(|world| world.resource::<Val>().0);
195            // SAFETY: test-only, single-threaded, Cell is alive.
196            unsafe { &*result_ptr }.set(v);
197        });
198
199        while executor.task_count() > 0 {
200            executor.poll();
201        }
202        assert_eq!(result.get(), 99);
203    }
204
205    #[test]
206    fn with_world_pre_resolved_handler() {
207        let mut wb = WorldBuilder::new();
208        wb.register(Val(42));
209        wb.register(Out(0));
210        let mut world = wb.build();
211        let ctx = WorldCtx::new(&mut world);
212
213        // Pre-resolve: HashMap lookups happen here, once
214        let mut handler = (|val: Res<Val>, mut out: ResMut<Out>, event: u64| {
215            out.0 = val.0 + event;
216        })
217        .into_handler(world.registry());
218
219        let mut executor = Executor::new(4);
220        executor.spawn_boxed(async move {
221            ctx.with_world(|world| handler.run(world, 10));
222        });
223
224        while executor.task_count() > 0 {
225            executor.poll();
226        }
227        assert_eq!(world.resource::<Out>().0, 52);
228    }
229
230    #[test]
231    fn with_world_returns_value() {
232        let mut wb = WorldBuilder::new();
233        wb.register(Val(7));
234        let mut world = wb.build();
235        let ctx = WorldCtx::new(&mut world);
236
237        let result = std::cell::Cell::new(0u64);
238        let result_ptr = std::ptr::from_ref(&result);
239
240        let mut executor = Executor::new(4);
241        executor.spawn_boxed(async move {
242            let v = ctx.with_world(|world| world.resource::<Val>().0 * 6);
243            // SAFETY: test-only, single-threaded, Cell is alive.
244            unsafe { &*result_ptr }.set(v);
245        });
246
247        while executor.task_count() > 0 {
248            executor.poll();
249        }
250        assert_eq!(result.get(), 42);
251    }
252
253    #[test]
254    fn multiple_tasks_share_ctx() {
255        let mut wb = WorldBuilder::new();
256        wb.register(Out(0));
257        let mut world = wb.build();
258        let ctx = WorldCtx::new(&mut world);
259
260        let mut executor = Executor::new(4);
261
262        for i in 1..=3u64 {
263            let ctx = ctx; // Copy
264            executor.spawn_boxed(async move {
265                ctx.with_world(|world| {
266                    world.resource_mut::<Out>().0 += i;
267                });
268            });
269        }
270
271        while executor.task_count() > 0 {
272            executor.poll();
273        }
274        assert_eq!(world.resource::<Out>().0, 6); // 1 + 2 + 3
275    }
276}