luaur_rt/thread.rs
1//! The [`Thread`] handle and [`ThreadStatus`]. Mirrors `mlua::Thread` /
2//! `mlua::ThreadStatus`.
3//!
4//! A thread is a Luau coroutine. It is created from a [`Function`] via
5//! [`Lua::create_thread`] (or surfaces from `coroutine.create(...)` evaluated
6//! in Lua) and driven with [`Thread::resume`].
7//!
8//! ## Implementation
9//!
10//! The thread is a first-class Lua value, so the handle holds a registry
11//! reference (like every other handle) keeping the coroutine alive. We also
12//! cache the raw `*mut lua_State` of the coroutine for the resume/xmove dance.
13//!
14//! `resume` mirrors mlua: push the args onto the *parent* state, `lua_xmove`
15//! them to the coroutine, `lua_resume(co, parent, nargs)`, then `lua_xmove` the
16//! results back and convert them. Status is derived from `lua_status` +
17//! `lua_costatus`, matching mlua's `Resumable`/`Running`/`Normal`/`Finished`/
18//! `Error` mapping.
19
20use crate::error::{Error, Result};
21use crate::function::Function;
22use crate::multi::MultiValue;
23use crate::state::{Lua, LuaRef};
24use crate::sync::{NotSync, XRc, NOT_SYNC};
25use crate::sys::*;
26use crate::traits::{FromLuaMulti, IntoLua, IntoLuaMulti};
27
28/// Status of a Lua thread (coroutine). Mirrors `mlua::ThreadStatus`.
29#[derive(Debug, Copy, Clone, Eq, PartialEq)]
30pub enum ThreadStatus {
31 /// The thread was just created or is suspended (yielded) and can be resumed.
32 Resumable,
33 /// The thread is currently running.
34 Running,
35 /// The thread is active but not running (it resumed another thread).
36 Normal,
37 /// The thread has finished executing.
38 Finished,
39 /// The thread raised a Lua error during execution.
40 Error,
41}
42
43/// The raw outcome of one async resume. See [`Thread::resume_for_async`].
44#[cfg(feature = "async")]
45pub(crate) enum AsyncResume {
46 /// The coroutine yielded the internal "future pending" marker.
47 Pending,
48 /// The coroutine yielded values via `coroutine.yield` (a Stream item).
49 Yielded(MultiValue),
50 /// The coroutine finished, returning these values.
51 Returned(MultiValue),
52}
53
54/// A handle to a Lua thread (coroutine). Mirrors `mlua::Thread`.
55///
56/// Under the `send` feature it is `Send` but never `Sync` — see
57/// [`crate::sync::NotSync`].
58#[derive(Clone)]
59pub struct Thread {
60 pub(crate) reference: XRc<LuaRef>,
61 /// The raw coroutine state pointer (cached from the referenced value).
62 pub(crate) thread_state: *mut lua_State,
63 pub(crate) _not_sync: NotSync,
64}
65
66// `Thread` caches a raw `*mut lua_State` (the coroutine), which is `!Send` by
67// default. Under the move-only `send` contract it is sound to move a `Thread`
68// to another thread (the cached pointer stays valid; the VM is single-threaded
69// in use). `!Sync` is preserved by the `NotSync` marker.
70#[cfg(feature = "send")]
71unsafe impl Send for Thread {}
72
73impl Thread {
74 /// Build a [`Thread`] from a registry ref to a thread value. Caches the
75 /// coroutine's raw state via `lua_tothread`.
76 pub(crate) fn from_ref(reference: LuaRef) -> Thread {
77 let state = reference.state();
78 let thread_state = unsafe {
79 reference.push();
80 let ts = lua_tothread(state, -1);
81 lua_pop(state, 1);
82 ts
83 };
84 Thread {
85 reference: XRc::new(reference),
86 thread_state,
87 _not_sync: NOT_SYNC,
88 }
89 }
90
91 pub(crate) unsafe fn push_to_stack(&self) {
92 self.reference.push();
93 }
94
95 /// The owning [`Lua`].
96 pub fn lua(&self) -> Lua {
97 self.reference.lua()
98 }
99
100 /// The raw coroutine state pointer. Mirrors `mlua::Thread::state`.
101 pub fn state(&self) -> *mut lua_State {
102 self.thread_state
103 }
104
105 /// Resume the coroutine, passing `args` and converting its yielded/returned
106 /// values to `R`. Mirrors `mlua::Thread::resume`.
107 ///
108 /// Returns [`Error::CoroutineUnresumable`] if the thread has finished,
109 /// errored, or is otherwise not resumable.
110 pub fn resume<R: FromLuaMulti>(&self, args: impl IntoLuaMulti) -> Result<R> {
111 let lua = self.lua();
112 // Convert the args first so a failing `IntoLua` (e.g. a bad argument)
113 // surfaces *before* we touch any Lua stack — matching mlua.
114 let args: MultiValue = args.into_lua_multi(&lua)?;
115
116 if !matches!(self.status(), ThreadStatus::Resumable) {
117 return Err(Error::CoroutineUnresumable);
118 }
119
120 let parent = lua.state();
121 let co = self.thread_state;
122 unsafe {
123 let nargs = args.len() as c_int;
124 if lua_checkstack(co, nargs.saturating_add(2)) == 0 {
125 return Err(Error::RuntimeError(
126 "stack overflow: too many arguments to coroutine resume".to_string(),
127 ));
128 }
129 // Push args onto the parent, then move them to the coroutine.
130 for v in args.iter() {
131 lua.push_value(v)?;
132 }
133 if nargs > 0 {
134 lua_xmove(parent, co, nargs);
135 }
136
137 self.resume_inner::<R>(&lua, nargs)
138 }
139 }
140
141 /// Resume the coroutine, immediately raising `error` inside it.
142 /// Mirrors `mlua::Thread::resume_error` (a Luau extension).
143 pub fn resume_error<R: FromLuaMulti>(&self, error: impl IntoLua) -> Result<R> {
144 let lua = self.lua();
145 let err_value = error.into_lua(&lua)?;
146
147 if !matches!(self.status(), ThreadStatus::Resumable) {
148 return Err(Error::CoroutineUnresumable);
149 }
150
151 let parent = lua.state();
152 let co = self.thread_state;
153 unsafe {
154 if lua_checkstack(co, 2) == 0 {
155 return Err(Error::RuntimeError("stack overflow".to_string()));
156 }
157 lua.push_value(&err_value)?;
158 lua_xmove(parent, co, 1);
159 // lua_resumeerror does the resume-with-error and returns the status.
160 let status = lua_resumeerror(co, parent);
161 self.finish_resume::<R>(&lua, status)
162 }
163 }
164
165 /// Run `lua_resume` and collect/convert the results. Expects `nargs` already
166 /// moved onto the coroutine stack.
167 unsafe fn resume_inner<R: FromLuaMulti>(&self, lua: &Lua, nargs: c_int) -> Result<R> {
168 let parent = lua.state();
169 let co = self.thread_state;
170 let status = unsafe { lua_resume(co, parent, nargs) };
171 unsafe { self.finish_resume::<R>(lua, status) }
172 }
173
174 /// Common tail of `resume`/`resume_error`: inspect the status, move results
175 /// back to the parent, and convert.
176 unsafe fn finish_resume<R: FromLuaMulti>(&self, lua: &Lua, status: c_int) -> Result<R> {
177 let parent = lua.state();
178 let co = self.thread_state;
179 unsafe {
180 if status != status::OK && status != status::YIELD && status != status::BREAK {
181 // Error: the coroutine left the error object on its own stack.
182 let nres = lua_gettop(co);
183 if nres > 0 {
184 lua_xmove(co, parent, nres);
185 }
186 let err = lua.pop_error(status);
187 // Clear any extra values the coroutine left on the parent.
188 return Err(err);
189 }
190 // `LUA_BREAK` is an interrupt-driven yield: the coroutine produced
191 // no values and its entire register window is still *live* (it must
192 // continue from the break point on the next resume). We must NOT
193 // touch its stack — moving any values off would strip live
194 // registers and corrupt the re-entry. Return an empty result.
195 if status == status::BREAK {
196 return R::from_lua_multi(MultiValue::with_capacity(0), lua);
197 }
198 // Success/yield: the produced values sit on the coroutine stack.
199 let nres = lua_gettop(co);
200 if lua_checkstack(parent, nres.saturating_add(1)) == 0 {
201 return Err(Error::RuntimeError("stack overflow".to_string()));
202 }
203 let base = lua_gettop(parent);
204 if nres > 0 {
205 lua_xmove(co, parent, nres);
206 }
207 let mut results = MultiValue::with_capacity(nres.max(0) as usize);
208 for i in 0..nres {
209 results.push_back(lua.value_from_stack(base + 1 + i)?);
210 }
211 lua_settop(parent, base);
212 R::from_lua_multi(results, lua)
213 }
214 }
215
216 /// Low-level resume used by the async driver. Pushes `args` to the
217 /// coroutine, resumes it once, and returns the raw outcome:
218 ///
219 /// * `Err(e)` — the coroutine raised an error.
220 /// * `Ok(AsyncResume::Pending)` — the coroutine yielded the internal
221 /// "future pending" marker (a single light-userdata == `poll_pending()`);
222 /// the stack is left cleared.
223 /// * `Ok(AsyncResume::Yielded(vals))` — the coroutine yielded `vals`
224 /// (a `coroutine.yield`, i.e. a Stream item).
225 /// * `Ok(AsyncResume::Returned(vals))` — the coroutine finished, returning
226 /// `vals`.
227 ///
228 /// The coroutine stack is fully consumed/cleared on every path.
229 #[cfg(feature = "async")]
230 pub(crate) fn resume_for_async(&self, args: MultiValue) -> Result<AsyncResume> {
231 let lua = self.lua();
232 let parent = lua.state();
233 let co = self.thread_state;
234 unsafe {
235 let nargs = args.len() as c_int;
236 if lua_checkstack(co, nargs.saturating_add(2)) == 0 {
237 return Err(Error::RuntimeError(
238 "stack overflow: too many arguments to coroutine resume".to_string(),
239 ));
240 }
241 for v in args.iter() {
242 lua.push_value(v)?;
243 }
244 if nargs > 0 {
245 lua_xmove(parent, co, nargs);
246 }
247 let status = lua_resume(co, parent, nargs);
248
249 if status != status::OK && status != status::YIELD {
250 let nres = lua_gettop(co);
251 if nres > 0 {
252 lua_xmove(co, parent, nres);
253 }
254 return Err(lua.pop_error(status));
255 }
256
257 let yielded = status == status::YIELD;
258 let nres = lua_gettop(co);
259
260 // Detect the single-light-userdata pending marker (top of the
261 // coroutine stack) on a yield.
262 if yielded
263 && nres == 1
264 && crate::sys::lua_tolightuserdata(co, -1) == crate::async_support::poll_pending()
265 {
266 lua_settop(co, 0);
267 return Ok(AsyncResume::Pending);
268 }
269
270 // Otherwise move the produced values to the parent and convert.
271 if lua_checkstack(parent, nres.saturating_add(1)) == 0 {
272 return Err(Error::RuntimeError("stack overflow".to_string()));
273 }
274 let base = lua_gettop(parent);
275 if nres > 0 {
276 lua_xmove(co, parent, nres);
277 }
278 let mut results = MultiValue::with_capacity(nres.max(0) as usize);
279 for i in 0..nres {
280 results.push_back(lua.value_from_stack(base + 1 + i)?);
281 }
282 lua_settop(parent, base);
283 lua_settop(co, 0);
284
285 if yielded {
286 Ok(AsyncResume::Yielded(results))
287 } else {
288 Ok(AsyncResume::Returned(results))
289 }
290 }
291 }
292
293 /// Resume a yielded async coroutine with the "terminate" signal so it drops
294 /// its in-flight future and parks. Best-effort; ignores errors. Used when an
295 /// [`AsyncThread`](crate::async_support::AsyncThread) is dropped mid-flight.
296 #[cfg(feature = "async")]
297 pub(crate) fn terminate_async(&self) {
298 if !self.is_resumable() {
299 return;
300 }
301 let lua = self.lua();
302 let parent = lua.state();
303 let co = self.thread_state;
304 unsafe {
305 if lua_checkstack(co, 2) == 0 {
306 return;
307 }
308 crate::sys::lua_pushlightuserdatatagged(
309 parent,
310 crate::async_support::poll_terminate(),
311 0,
312 );
313 lua_xmove(parent, co, 1);
314 let _ = lua_resume(co, parent, 1);
315 lua_settop(co, 0);
316 }
317 }
318
319 /// The thread's status. Mirrors `mlua::Thread::status`.
320 pub fn status(&self) -> ThreadStatus {
321 let lua = self.lua();
322 let parent = lua.state();
323 let co = self.thread_state;
324 // A thread whose state is the currently-running state is "Running".
325 if co == parent {
326 return ThreadStatus::Running;
327 }
328 unsafe {
329 // A coroutine yielded by an interrupt (`lua_break`) has raw status
330 // `LUA_BREAK`; `lua_costatus` reports that as "normal", but the
331 // coroutine is in fact resumable (it continues from the break point
332 // on the next resume). Detect it directly.
333 if lua_status(co) == status::BREAK {
334 return ThreadStatus::Resumable;
335 }
336 let cos = lua_costatus(parent, co);
337 match cos {
338 costatus::SUSPENDED => ThreadStatus::Resumable,
339 costatus::RUNNING => ThreadStatus::Running,
340 costatus::NORMAL => ThreadStatus::Normal,
341 costatus::FINISHED => ThreadStatus::Finished,
342 costatus::ERROR => ThreadStatus::Error,
343 _ => {
344 // Fall back to lua_status for any unexpected code.
345 let s = lua_status(co);
346 if s == status::YIELD {
347 ThreadStatus::Resumable
348 } else if s == status::OK {
349 // New (function on stack) vs finished (empty stack).
350 if lua_gettop(co) > 0 {
351 ThreadStatus::Resumable
352 } else {
353 ThreadStatus::Finished
354 }
355 } else {
356 ThreadStatus::Error
357 }
358 }
359 }
360 }
361 }
362
363 /// Whether the thread can be resumed. Mirrors `mlua::Thread::is_resumable`.
364 pub fn is_resumable(&self) -> bool {
365 self.status() == ThreadStatus::Resumable
366 }
367
368 /// Whether the thread is currently running. Mirrors `mlua::Thread::is_running`.
369 pub fn is_running(&self) -> bool {
370 self.status() == ThreadStatus::Running
371 }
372
373 /// Whether the thread is active but not running. Mirrors
374 /// `mlua::Thread::is_normal`.
375 pub fn is_normal(&self) -> bool {
376 self.status() == ThreadStatus::Normal
377 }
378
379 /// Whether the thread has finished executing. Mirrors
380 /// `mlua::Thread::is_finished`.
381 pub fn is_finished(&self) -> bool {
382 self.status() == ThreadStatus::Finished
383 }
384
385 /// Whether the thread raised an error. Mirrors `mlua::Thread::is_error`.
386 pub fn is_error(&self) -> bool {
387 self.status() == ThreadStatus::Error
388 }
389
390 /// Reset the thread to a fresh state and install `func` as its body.
391 /// Mirrors `mlua::Thread::reset` (Luau semantics: any non-running thread can
392 /// be reset).
393 pub fn reset(&self, func: Function) -> Result<()> {
394 let status = self.status();
395 match status {
396 ThreadStatus::Running => {
397 return Err(Error::runtime("cannot reset a running thread"));
398 }
399 ThreadStatus::Normal => {
400 return Err(Error::runtime("cannot reset a normal thread"));
401 }
402 _ => {}
403 }
404 let lua = self.lua();
405 let parent = lua.state();
406 let co = self.thread_state;
407 unsafe {
408 lua_resetthread(co);
409 // Push the new body function onto the coroutine stack.
410 func.push_to_stack();
411 lua_xmove(parent, co, 1);
412 // Re-inherit the *main* globals table into the coroutine, dropping
413 // any sandbox proxy global a prior `Thread::sandbox` had installed
414 // (matches mlua's Luau `reset`: a reset thread sees the main env).
415 lua_pushvalue(parent, LUA_GLOBALSINDEX);
416 lua_xmove(parent, co, 1);
417 lua_replace(co, LUA_GLOBALSINDEX);
418 }
419 Ok(())
420 }
421
422 /// A raw pointer identifying this thread. Mirrors `mlua::Thread::to_pointer`.
423 pub fn to_pointer(&self) -> *const c_void {
424 let state = self.reference.state();
425 unsafe {
426 self.reference.push();
427 let p = lua_topointer(state, -1);
428 lua_pop(state, 1);
429 p
430 }
431 }
432}
433
434impl std::fmt::Debug for Thread {
435 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
436 write!(f, "Thread")
437 }
438}
439
440// ---------------------------------------------------------------------------
441// Async: drive a coroutine as a Rust `Future` / `Stream` (the `async` feature)
442// ---------------------------------------------------------------------------
443
444#[cfg(feature = "async")]
445impl Thread {
446 /// Convert this (resumable) thread into an
447 /// [`AsyncThread`](crate::AsyncThread) that implements
448 /// [`Future`](std::future::Future) and
449 /// [`Stream`](futures_util::stream::Stream).
450 ///
451 /// Mirrors `mlua::Thread::into_async`. `args` are passed to the coroutine on
452 /// its first resume. As a `Future` the thread is driven to completion and
453 /// resolves to its final return value(s); as a `Stream` each
454 /// `coroutine.yield` produces an item.
455 #[cfg_attr(docsrs, doc(cfg(feature = "async")))]
456 pub fn into_async<R: FromLuaMulti>(
457 self,
458 args: impl IntoLuaMulti,
459 ) -> Result<crate::async_support::AsyncThread<R>> {
460 if !self.is_resumable() {
461 return Err(Error::CoroutineUnresumable);
462 }
463 let lua = self.lua();
464 let args = args.into_lua_multi(&lua)?;
465 Ok(crate::async_support::AsyncThread::new(self, args))
466 }
467}
468
469impl PartialEq for Thread {
470 fn eq(&self, other: &Self) -> bool {
471 self.to_pointer() == other.to_pointer()
472 }
473}
474
475impl IntoLua for Thread {
476 fn into_lua(self, _lua: &Lua) -> Result<crate::value::Value> {
477 Ok(crate::value::Value::Thread(self))
478 }
479}
480
481impl IntoLua for &Thread {
482 fn into_lua(self, _lua: &Lua) -> Result<crate::value::Value> {
483 Ok(crate::value::Value::Thread(self.clone()))
484 }
485}
486
487impl FromLua for Thread {
488 fn from_lua(value: crate::value::Value, _lua: &Lua) -> Result<Self> {
489 match value {
490 crate::value::Value::Thread(t) => Ok(t),
491 other => Err(Error::FromLuaConversionError {
492 from: other.type_name(),
493 to: "Thread".to_string(),
494 message: None,
495 }),
496 }
497 }
498}
499
500use crate::traits::FromLua;
501
502impl Lua {
503 /// Create a new coroutine from a [`Function`]. Mirrors
504 /// `mlua::Lua::create_thread`.
505 pub fn create_thread(&self, func: Function) -> Result<Thread> {
506 let state = self.state();
507 unsafe {
508 // Create a new thread; it is pushed on the parent stack.
509 let co = lua_newthread(state);
510 if co.is_null() {
511 return Err(Error::runtime("luaur-rt: failed to create thread"));
512 }
513 // Take a ref to the thread value (still on the parent stack top).
514 let thread = Thread::from_ref(self.pop_ref());
515 // Move the body function onto the coroutine's stack so the first
516 // resume invokes it.
517 func.push_to_stack(); // pushes onto parent stack
518 lua_xmove(state, co, 1);
519 Ok(thread)
520 }
521 }
522
523 /// The currently-running thread. Mirrors `mlua::Lua::current_thread`.
524 ///
525 /// Inside a Rust callback this is the coroutine (or main thread) that
526 /// invoked it. Under the `async` feature, a coroutine created implicitly by
527 /// `call_async` is transparent: this returns its *owner* thread instead, so
528 /// `current_thread()` is stable across the implicit-coroutine boundary
529 /// (matching mlua).
530 pub fn current_thread(&self) -> Thread {
531 let state = self.state();
532 // If we are running on an implicit `call_async` coroutine, report the
533 // owner thread that issued the call.
534 #[cfg(feature = "async")]
535 if let Some(owner) = crate::async_support::implicit_thread_owner(state) {
536 unsafe {
537 lua_pushthread(owner);
538 // The owner-thread value is on the owner's stack; move it to this
539 // state so we can take a ref to it from here.
540 if owner != state {
541 lua_xmove(owner, state, 1);
542 }
543 return Thread::from_ref(self.pop_ref());
544 }
545 }
546 unsafe {
547 lua_pushthread(state);
548 Thread::from_ref(self.pop_ref())
549 }
550 }
551}