mlua_codemp_patch/util/
error.rs

1use std::any::Any;
2use std::fmt::Write as _;
3use std::mem::MaybeUninit;
4use std::os::raw::{c_int, c_void};
5use std::panic::{catch_unwind, resume_unwind, AssertUnwindSafe};
6use std::ptr;
7use std::sync::Arc;
8
9use crate::error::{Error, Result};
10use crate::memory::MemoryState;
11use crate::util::{
12    check_stack, get_internal_metatable, get_internal_userdata, init_internal_metatable,
13    push_internal_userdata, push_string, push_table, rawset_field, to_string, TypeKey,
14    DESTRUCTED_USERDATA_METATABLE,
15};
16
17static WRAPPED_FAILURE_TYPE_KEY: u8 = 0;
18
19pub(crate) enum WrappedFailure {
20    None,
21    Error(Error),
22    Panic(Option<Box<dyn Any + Send + 'static>>),
23}
24
25impl TypeKey for WrappedFailure {
26    #[inline(always)]
27    fn type_key() -> *const c_void {
28        &WRAPPED_FAILURE_TYPE_KEY as *const u8 as *const c_void
29    }
30}
31
32impl WrappedFailure {
33    pub(crate) unsafe fn new_userdata(state: *mut ffi::lua_State) -> *mut Self {
34        #[cfg(feature = "luau")]
35        let ud = ffi::lua_newuserdata_t::<Self>(state);
36        #[cfg(not(feature = "luau"))]
37        let ud = ffi::lua_newuserdata(state, std::mem::size_of::<Self>()) as *mut Self;
38        ptr::write(ud, WrappedFailure::None);
39        ud
40    }
41}
42
43// In the context of a lua callback, this will call the given function and if the given function
44// returns an error, *or if the given function panics*, this will result in a call to `lua_error` (a
45// longjmp). The error or panic is wrapped in such a way that when calling `pop_error` back on
46// the Rust side, it will resume the panic.
47//
48// This function assumes the structure of the stack at the beginning of a callback, that the only
49// elements on the stack are the arguments to the callback.
50//
51// This function uses some of the bottom of the stack for error handling, the given callback will be
52// given the number of arguments available as an argument, and should return the number of returns
53// as normal, but cannot assume that the arguments available start at 0.
54unsafe fn callback_error<F, R>(state: *mut ffi::lua_State, f: F) -> R
55where
56    F: FnOnce(c_int) -> Result<R>,
57{
58    let nargs = ffi::lua_gettop(state);
59
60    // We need 2 extra stack spaces to store preallocated memory and error/panic metatable
61    let extra_stack = if nargs < 2 { 2 - nargs } else { 1 };
62    ffi::luaL_checkstack(
63        state,
64        extra_stack,
65        cstr!("not enough stack space for callback error handling"),
66    );
67
68    // We cannot shadow Rust errors with Lua ones, we pre-allocate enough memory
69    // to store a wrapped error or panic *before* we proceed.
70    let ud = WrappedFailure::new_userdata(state);
71    ffi::lua_rotate(state, 1, 1);
72
73    match catch_unwind(AssertUnwindSafe(|| f(nargs))) {
74        Ok(Ok(r)) => {
75            ffi::lua_remove(state, 1);
76            r
77        }
78        Ok(Err(err)) => {
79            ffi::lua_settop(state, 1);
80
81            // Build `CallbackError` with traceback
82            let traceback = if ffi::lua_checkstack(state, ffi::LUA_TRACEBACK_STACK) != 0 {
83                ffi::luaL_traceback(state, state, ptr::null(), 0);
84                let traceback = to_string(state, -1);
85                ffi::lua_pop(state, 1);
86                traceback
87            } else {
88                "<not enough stack space for traceback>".to_string()
89            };
90            let cause = Arc::new(err);
91            let wrapped_error = WrappedFailure::Error(Error::CallbackError { traceback, cause });
92            ptr::write(ud, wrapped_error);
93            get_internal_metatable::<WrappedFailure>(state);
94            ffi::lua_setmetatable(state, -2);
95
96            ffi::lua_error(state)
97        }
98        Err(p) => {
99            ffi::lua_settop(state, 1);
100            ptr::write(ud, WrappedFailure::Panic(Some(p)));
101            get_internal_metatable::<WrappedFailure>(state);
102            ffi::lua_setmetatable(state, -2);
103            ffi::lua_error(state)
104        }
105    }
106}
107
108// Pops an error off of the stack and returns it. The specific behavior depends on the type of the
109// error at the top of the stack:
110//   1) If the error is actually a panic, this will continue the panic.
111//   2) If the error on the top of the stack is actually an error, just returns it.
112//   3) Otherwise, interprets the error as the appropriate lua error.
113// Uses 2 stack spaces, does not call checkstack.
114pub(crate) unsafe fn pop_error(state: *mut ffi::lua_State, err_code: c_int) -> Error {
115    mlua_debug_assert!(
116        err_code != ffi::LUA_OK && err_code != ffi::LUA_YIELD,
117        "pop_error called with non-error return code"
118    );
119
120    match get_internal_userdata::<WrappedFailure>(state, -1, ptr::null()).as_mut() {
121        Some(WrappedFailure::Error(err)) => {
122            ffi::lua_pop(state, 1);
123            err.clone()
124        }
125        Some(WrappedFailure::Panic(panic)) => {
126            if let Some(p) = panic.take() {
127                resume_unwind(p);
128            } else {
129                Error::PreviouslyResumedPanic
130            }
131        }
132        _ => {
133            let err_string = to_string(state, -1);
134            ffi::lua_pop(state, 1);
135
136            match err_code {
137                ffi::LUA_ERRRUN => Error::RuntimeError(err_string),
138                ffi::LUA_ERRSYNTAX => {
139                    Error::SyntaxError {
140                        // This seems terrible, but as far as I can tell, this is exactly what the
141                        // stock Lua REPL does.
142                        incomplete_input: err_string.ends_with("<eof>") || err_string.ends_with("'<eof>'"),
143                        message: err_string,
144                    }
145                }
146                ffi::LUA_ERRERR => {
147                    // This error is raised when the error handler raises an error too many times
148                    // recursively, and continuing to trigger the error handler would cause a stack
149                    // overflow. It is not very useful to differentiate between this and "ordinary"
150                    // runtime errors, so we handle them the same way.
151                    Error::RuntimeError(err_string)
152                }
153                ffi::LUA_ERRMEM => Error::MemoryError(err_string),
154                #[cfg(any(feature = "lua53", feature = "lua52"))]
155                ffi::LUA_ERRGCMM => Error::GarbageCollectorError(err_string),
156                _ => mlua_panic!("unrecognized lua error code"),
157            }
158        }
159    }
160}
161
162// Call a function that calls into the Lua API and may trigger a Lua error (longjmp) in a safe way.
163// Wraps the inner function in a call to `lua_pcall`, so the inner function only has access to a
164// limited lua stack. `nargs` is the same as the the parameter to `lua_pcall`, and `nresults` is
165// always `LUA_MULTRET`. Provided function must *not* panic, and since it will generally be
166// longjmping, should not contain any values that implements Drop.
167// Internally uses 2 extra stack spaces, and does not call checkstack.
168pub(crate) unsafe fn protect_lua_call(
169    state: *mut ffi::lua_State,
170    nargs: c_int,
171    f: unsafe extern "C-unwind" fn(*mut ffi::lua_State) -> c_int,
172) -> Result<()> {
173    let stack_start = ffi::lua_gettop(state) - nargs;
174
175    MemoryState::relax_limit_with(state, || {
176        ffi::lua_pushcfunction(state, error_traceback);
177        ffi::lua_pushcfunction(state, f);
178    });
179    if nargs > 0 {
180        ffi::lua_rotate(state, stack_start + 1, 2);
181    }
182
183    let ret = ffi::lua_pcall(state, nargs, ffi::LUA_MULTRET, stack_start + 1);
184    ffi::lua_remove(state, stack_start + 1);
185
186    if ret == ffi::LUA_OK {
187        Ok(())
188    } else {
189        Err(pop_error(state, ret))
190    }
191}
192
193// Call a function that calls into the Lua API and may trigger a Lua error (longjmp) in a safe way.
194// Wraps the inner function in a call to `lua_pcall`, so the inner function only has access to a
195// limited lua stack. `nargs` and `nresults` are similar to the parameters of `lua_pcall`, but the
196// given function return type is not the return value count, instead the inner function return
197// values are assumed to match the `nresults` param. Provided function must *not* panic, and since
198// it will generally be longjmping, should not contain any values that implements Drop.
199// Internally uses 3 extra stack spaces, and does not call checkstack.
200pub(crate) unsafe fn protect_lua_closure<F, R>(
201    state: *mut ffi::lua_State,
202    nargs: c_int,
203    nresults: c_int,
204    f: F,
205) -> Result<R>
206where
207    F: Fn(*mut ffi::lua_State) -> R,
208    R: Copy,
209{
210    struct Params<F, R: Copy> {
211        function: F,
212        result: MaybeUninit<R>,
213        nresults: c_int,
214    }
215
216    unsafe extern "C-unwind" fn do_call<F, R>(state: *mut ffi::lua_State) -> c_int
217    where
218        F: Fn(*mut ffi::lua_State) -> R,
219        R: Copy,
220    {
221        let params = ffi::lua_touserdata(state, -1) as *mut Params<F, R>;
222        ffi::lua_pop(state, 1);
223
224        (*params).result.write(((*params).function)(state));
225
226        if (*params).nresults == ffi::LUA_MULTRET {
227            ffi::lua_gettop(state)
228        } else {
229            (*params).nresults
230        }
231    }
232
233    let stack_start = ffi::lua_gettop(state) - nargs;
234
235    MemoryState::relax_limit_with(state, || {
236        ffi::lua_pushcfunction(state, error_traceback);
237        ffi::lua_pushcfunction(state, do_call::<F, R>);
238    });
239    if nargs > 0 {
240        ffi::lua_rotate(state, stack_start + 1, 2);
241    }
242
243    let mut params = Params {
244        function: f,
245        result: MaybeUninit::uninit(),
246        nresults,
247    };
248
249    ffi::lua_pushlightuserdata(state, &mut params as *mut Params<F, R> as *mut c_void);
250    let ret = ffi::lua_pcall(state, nargs + 1, nresults, stack_start + 1);
251    ffi::lua_remove(state, stack_start + 1);
252
253    if ret == ffi::LUA_OK {
254        // `LUA_OK` is only returned when the `do_call` function has completed successfully, so
255        // `params.result` is definitely initialized.
256        Ok(params.result.assume_init())
257    } else {
258        Err(pop_error(state, ret))
259    }
260}
261
262pub(crate) unsafe extern "C-unwind" fn error_traceback(state: *mut ffi::lua_State) -> c_int {
263    // Luau calls error handler for memory allocation errors, skip it
264    // See https://github.com/Roblox/luau/issues/880
265    #[cfg(feature = "luau")]
266    if MemoryState::limit_reached(state) {
267        return 0;
268    }
269
270    if ffi::lua_checkstack(state, 2) == 0 {
271        // If we don't have enough stack space to even check the error type, do
272        // nothing so we don't risk shadowing a rust panic.
273        return 1;
274    }
275
276    if get_internal_userdata::<WrappedFailure>(state, -1, ptr::null()).is_null() {
277        let s = ffi::luaL_tolstring(state, -1, ptr::null_mut());
278        if ffi::lua_checkstack(state, ffi::LUA_TRACEBACK_STACK) != 0 {
279            ffi::luaL_traceback(state, state, s, 0);
280            ffi::lua_remove(state, -2);
281        }
282    }
283
284    1
285}
286
287// A variant of `error_traceback` that can safely inspect another (yielded) thread stack
288pub(crate) unsafe fn error_traceback_thread(state: *mut ffi::lua_State, thread: *mut ffi::lua_State) {
289    // Move error object to the main thread to safely call `__tostring` metamethod if present
290    ffi::lua_xmove(thread, state, 1);
291
292    if get_internal_userdata::<WrappedFailure>(state, -1, ptr::null()).is_null() {
293        let s = ffi::luaL_tolstring(state, -1, ptr::null_mut());
294        if ffi::lua_checkstack(state, ffi::LUA_TRACEBACK_STACK) != 0 {
295            ffi::luaL_traceback(state, thread, s, 0);
296            ffi::lua_remove(state, -2);
297        }
298    }
299}
300
301// Initialize the error, panic, and destructed userdata metatables.
302pub(crate) unsafe fn init_error_registry(state: *mut ffi::lua_State) -> Result<()> {
303    check_stack(state, 7)?;
304
305    // Create error and panic metatables
306
307    static ERROR_PRINT_BUFFER_KEY: u8 = 0;
308
309    unsafe extern "C-unwind" fn error_tostring(state: *mut ffi::lua_State) -> c_int {
310        callback_error(state, |_| {
311            check_stack(state, 3)?;
312
313            let err_buf = match get_internal_userdata::<WrappedFailure>(state, -1, ptr::null()).as_ref() {
314                Some(WrappedFailure::Error(error)) => {
315                    let err_buf_key = &ERROR_PRINT_BUFFER_KEY as *const u8 as *const c_void;
316                    ffi::lua_rawgetp(state, ffi::LUA_REGISTRYINDEX, err_buf_key);
317                    let err_buf = ffi::lua_touserdata(state, -1) as *mut String;
318                    ffi::lua_pop(state, 2);
319
320                    (*err_buf).clear();
321                    // Depending on how the API is used and what error types scripts are given, it may
322                    // be possible to make this consume arbitrary amounts of memory (for example, some
323                    // kind of recursive error structure?)
324                    let _ = write!(&mut (*err_buf), "{error}");
325                    Ok(err_buf)
326                }
327                Some(WrappedFailure::Panic(Some(ref panic))) => {
328                    let err_buf_key = &ERROR_PRINT_BUFFER_KEY as *const u8 as *const c_void;
329                    ffi::lua_rawgetp(state, ffi::LUA_REGISTRYINDEX, err_buf_key);
330                    let err_buf = ffi::lua_touserdata(state, -1) as *mut String;
331                    (*err_buf).clear();
332                    ffi::lua_pop(state, 2);
333
334                    if let Some(msg) = panic.downcast_ref::<&str>() {
335                        let _ = write!(&mut (*err_buf), "{msg}");
336                    } else if let Some(msg) = panic.downcast_ref::<String>() {
337                        let _ = write!(&mut (*err_buf), "{msg}");
338                    } else {
339                        let _ = write!(&mut (*err_buf), "<panic>");
340                    };
341                    Ok(err_buf)
342                }
343                Some(WrappedFailure::Panic(None)) => Err(Error::PreviouslyResumedPanic),
344                _ => {
345                    // I'm not sure whether this is possible to trigger without bugs in mlua?
346                    Err(Error::UserDataTypeMismatch)
347                }
348            }?;
349
350            push_string(state, (*err_buf).as_bytes(), true)?;
351            (*err_buf).clear();
352
353            Ok(1)
354        })
355    }
356
357    init_internal_metatable::<WrappedFailure>(
358        state,
359        Some(|state| {
360            ffi::lua_pushcfunction(state, error_tostring);
361            rawset_field(state, -2, "__tostring")
362        }),
363    )?;
364
365    // Create destructed userdata metatable
366
367    unsafe extern "C-unwind" fn destructed_error(state: *mut ffi::lua_State) -> c_int {
368        callback_error(state, |_| Err(Error::CallbackDestructed))
369    }
370
371    push_table(state, 0, 26, true)?;
372    ffi::lua_pushcfunction(state, destructed_error);
373    for &method in &[
374        "__add",
375        "__sub",
376        "__mul",
377        "__div",
378        "__mod",
379        "__pow",
380        "__unm",
381        #[cfg(any(feature = "lua54", feature = "lua53", feature = "luau"))]
382        "__idiv",
383        #[cfg(any(feature = "lua54", feature = "lua53"))]
384        "__band",
385        #[cfg(any(feature = "lua54", feature = "lua53"))]
386        "__bor",
387        #[cfg(any(feature = "lua54", feature = "lua53"))]
388        "__bxor",
389        #[cfg(any(feature = "lua54", feature = "lua53"))]
390        "__bnot",
391        #[cfg(any(feature = "lua54", feature = "lua53"))]
392        "__shl",
393        #[cfg(any(feature = "lua54", feature = "lua53"))]
394        "__shr",
395        "__concat",
396        "__len",
397        "__eq",
398        "__lt",
399        "__le",
400        "__index",
401        "__newindex",
402        "__call",
403        "__tostring",
404        #[cfg(any(feature = "lua54", feature = "lua53", feature = "lua52", feature = "luajit52"))]
405        "__pairs",
406        #[cfg(any(feature = "lua53", feature = "lua52", feature = "luajit52"))]
407        "__ipairs",
408        #[cfg(feature = "luau")]
409        "__iter",
410        #[cfg(feature = "lua54")]
411        "__close",
412    ] {
413        ffi::lua_pushvalue(state, -1);
414        rawset_field(state, -3, method)?;
415    }
416    ffi::lua_pop(state, 1);
417
418    protect_lua!(state, 1, 0, fn(state) {
419        let destructed_mt_key = &DESTRUCTED_USERDATA_METATABLE as *const u8 as *const c_void;
420        ffi::lua_rawsetp(state, ffi::LUA_REGISTRYINDEX, destructed_mt_key);
421    })?;
422
423    // Create error print buffer
424    init_internal_metatable::<String>(state, None)?;
425    push_internal_userdata(state, String::new(), true)?;
426    protect_lua!(state, 1, 0, fn(state) {
427        let err_buf_key = &ERROR_PRINT_BUFFER_KEY as *const u8 as *const c_void;
428        ffi::lua_rawsetp(state, ffi::LUA_REGISTRYINDEX, err_buf_key);
429    })?;
430
431    Ok(())
432}