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}