Skip to main content

mlua_luau_scheduler/
functions.rs

1#![allow(clippy::too_many_lines)]
2
3use mlua::prelude::*;
4
5use crate::{
6    error_callback::ThreadErrorCallback,
7    queue::{DeferredThreadQueue, SpawnedThreadQueue},
8    threads::{ThreadId, ThreadMap},
9    traits::LuaSchedulerExt,
10    util::{LuaThreadOrFunction, is_poll_pending},
11};
12
13const ERR_METADATA_NOT_ATTACHED: &str = "\
14Lua state does not have scheduler metadata attached!\
15\nThis is most likely caused by creating functions outside of a scheduler.\
16\nScheduler functions must always be created from within an active scheduler.\
17";
18
19const EXIT_IMPL_LUA: &str = r"
20exit(...)
21yield()
22";
23
24const WRAP_IMPL_LUA: &str = r"
25local t = create(...)
26return function(...)
27    local r = { resume(t, ...) }
28    if r[1] then
29        return select(2, unpack(r))
30    else
31        error(r[2], 2)
32    end
33end
34";
35
36/**
37    A collection of lua functions that may be called to interact with a [`Scheduler`].
38
39    Note that these may all be implemented using [`LuaSchedulerExt`], however, this struct
40    is implemented using internal (non-public) APIs, and generally has better performance.
41*/
42pub struct Functions {
43    /**
44        Implementation of `coroutine.resume` that handles async polling properly.
45
46        Defers onto the scheduler queue if the thread calls an async function.
47    */
48    pub resume: LuaFunction,
49    /**
50        Implementation of `coroutine.wrap` that handles async polling properly.
51
52        Defers onto the scheduler queue if the thread calls an async function.
53    */
54    pub wrap: LuaFunction,
55    /**
56        Resumes a function / thread once instantly, and runs until first yield.
57
58        Spawns onto the scheduler queue if not completed.
59    */
60    pub spawn: LuaFunction,
61    /**
62        Defers a function / thread onto the scheduler queue.
63
64        Does not resume instantly, only adds to the queue.
65    */
66    pub defer: LuaFunction,
67    /**
68        Cancels a function / thread, removing it from the queue.
69    */
70    pub cancel: LuaFunction,
71    /**
72        Exits the scheduler, stopping all other threads and closing the scheduler.
73
74        Yields the calling thread to ensure that it does not continue.
75    */
76    pub exit: LuaFunction,
77}
78
79impl Functions {
80    /**
81        Creates a new collection of Lua functions that may be called to interact with a [`Scheduler`].
82
83        # Errors
84
85        Errors when out of memory, or if default Lua globals are missing.
86
87        # Panics
88
89        Panics when the given [`Lua`] instance does not have an attached [`Scheduler`].
90    */
91    pub fn new(lua: Lua) -> LuaResult<Self> {
92        let spawn_queue = lua
93            .app_data_ref::<SpawnedThreadQueue>()
94            .expect(ERR_METADATA_NOT_ATTACHED)
95            .clone();
96        let defer_queue = lua
97            .app_data_ref::<DeferredThreadQueue>()
98            .expect(ERR_METADATA_NOT_ATTACHED)
99            .clone();
100        let error_callback = lua
101            .app_data_ref::<ThreadErrorCallback>()
102            .expect(ERR_METADATA_NOT_ATTACHED)
103            .clone();
104        let thread_map = lua
105            .app_data_ref::<ThreadMap>()
106            .expect(ERR_METADATA_NOT_ATTACHED)
107            .clone();
108
109        let resume_queue = defer_queue.clone();
110        let resume_map = thread_map.clone();
111        let resume =
112            lua.create_function(move |lua, (thread, args): (LuaThread, LuaMultiValue)| {
113                let _span = tracing::trace_span!("Scheduler::fn_resume").entered();
114                match thread.resume::<LuaMultiValue>(args.clone()) {
115                    Ok(v) => {
116                        if v.front().is_some_and(is_poll_pending) {
117                            // Pending, defer to scheduler and return nil
118                            resume_queue.push_item(lua, &thread, args)?;
119                            (true, LuaValue::Nil).into_lua_multi(lua)
120                        } else {
121                            // Not pending, store the value if thread is done
122                            if thread.status() != LuaThreadStatus::Resumable {
123                                let id = ThreadId::from(&thread);
124                                if resume_map.is_tracked(id) {
125                                    resume_map.insert(id, Ok(v.clone()));
126                                }
127                            }
128                            (true, v).into_lua_multi(lua)
129                        }
130                    }
131                    Err(e) => {
132                        // Not pending, store the error
133                        let id = ThreadId::from(&thread);
134                        if resume_map.is_tracked(id) {
135                            resume_map.insert(id, Err(e.clone()));
136                        }
137                        (false, e.to_string()).into_lua_multi(lua)
138                    }
139                }
140            })?;
141
142        let wrap_env = lua.create_table_from(vec![
143            ("resume", resume.clone()),
144            ("error", lua.globals().get::<LuaFunction>("error")?),
145            ("select", lua.globals().get::<LuaFunction>("select")?),
146            ("unpack", lua.globals().get::<LuaFunction>("unpack")?),
147            (
148                "create",
149                lua.globals()
150                    .get::<LuaTable>("coroutine")?
151                    .get::<LuaFunction>("create")?,
152            ),
153        ])?;
154        let wrap = lua
155            .load(WRAP_IMPL_LUA)
156            .set_name("=__scheduler_wrap")
157            .set_environment(wrap_env)
158            .into_function()?;
159
160        let spawn_map = thread_map.clone();
161        let spawn = lua.create_function(
162            move |lua, (tof, args): (LuaThreadOrFunction, LuaMultiValue)| {
163                let _span = tracing::trace_span!("Scheduler::fn_spawn").entered();
164                let thread = tof.into_thread(lua)?;
165                if thread.status() == LuaThreadStatus::Resumable {
166                    // NOTE: We need to resume the thread once instantly for correct behavior,
167                    // and only if we get the pending value back we can spawn to async executor
168                    match thread.resume::<LuaMultiValue>(args.clone()) {
169                        Ok(v) => {
170                            if v.front().is_some_and(is_poll_pending) {
171                                spawn_queue.push_item(lua, &thread, args)?;
172                            } else {
173                                // Not pending, store the value if thread is done
174                                if thread.status() != LuaThreadStatus::Resumable {
175                                    let id = ThreadId::from(&thread);
176                                    if spawn_map.is_tracked(id) {
177                                        spawn_map.insert(id, Ok(v));
178                                    }
179                                }
180                            }
181                        }
182                        Err(e) => {
183                            error_callback.call(&e);
184                            // Not pending, store the error
185                            let id = ThreadId::from(&thread);
186                            if spawn_map.is_tracked(id) {
187                                spawn_map.insert(id, Err(e));
188                            }
189                        }
190                    }
191                }
192                Ok(thread)
193            },
194        )?;
195
196        let defer = lua.create_function(
197            move |lua, (tof, args): (LuaThreadOrFunction, LuaMultiValue)| {
198                let _span = tracing::trace_span!("Scheduler::fn_defer").entered();
199                let thread = tof.into_thread(lua)?;
200                if thread.status() == LuaThreadStatus::Resumable {
201                    defer_queue.push_item(lua, &thread, args)?;
202                }
203                Ok(thread)
204            },
205        )?;
206
207        let close = lua
208            .globals()
209            .get::<LuaTable>("coroutine")?
210            .get::<LuaFunction>("close")?;
211        let close_key = lua.create_registry_value(close)?;
212        let cancel = lua.create_function(move |lua, thread: LuaThread| {
213            let _span = tracing::trace_span!("Scheduler::fn_cancel").entered();
214            let close: LuaFunction = lua.registry_value(&close_key)?;
215            match close.call(thread) {
216                Err(LuaError::CoroutineUnresumable) | Ok(()) => Ok(()),
217                Err(e) => Err(e),
218            }
219        })?;
220
221        let exit_env = lua.create_table_from(vec![
222            (
223                "exit",
224                lua.create_function(|lua, code: Option<u8>| {
225                    let _span = tracing::trace_span!("Scheduler::fn_exit").entered();
226                    let code = code.unwrap_or_default();
227                    lua.set_exit_code(code);
228                    Ok(())
229                })?,
230            ),
231            (
232                "yield",
233                lua.globals()
234                    .get::<LuaTable>("coroutine")?
235                    .get::<LuaFunction>("yield")?,
236            ),
237        ])?;
238        let exit = lua
239            .load(EXIT_IMPL_LUA)
240            .set_name("=__scheduler_exit")
241            .set_environment(exit_env)
242            .into_function()?;
243
244        Ok(Self {
245            resume,
246            wrap,
247            spawn,
248            defer,
249            cancel,
250            exit,
251        })
252    }
253}
254
255impl Functions {
256    /**
257        Injects [`Scheduler`]-compatible functions into the given [`Lua`] instance.
258
259        This will overwrite the following functions:
260
261        - `coroutine.resume`
262        - `coroutine.wrap`
263
264        # Errors
265
266        Errors when out of memory, or if default Lua globals are missing.
267    */
268    pub fn inject_compat(&self, lua: &Lua) -> LuaResult<()> {
269        let co: LuaTable = lua.globals().get("coroutine")?;
270        co.set("resume", self.resume.clone())?;
271        co.set("wrap", self.wrap.clone())?;
272        Ok(())
273    }
274}