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}