Skip to main content

luau_vm/call/
protected.rs

1use luau_common::ByteSlice;
2
3use crate::call::{CallRuntime, ThreadStack};
4use crate::debug::DebugRuntime;
5use crate::function::FunctionRuntime;
6use crate::handle::RawHandle;
7use crate::handle::sealed::Sealed;
8use crate::state::{
9    GlobalState, ProtectedErrorAction, THREAD_STATUS_BREAK, THREAD_STATUS_ERR_ERR,
10    THREAD_STATUS_ERR_MEM, THREAD_STATUS_ERR_RUN, THREAD_STATUS_ERR_SYNTAX, ThreadState,
11};
12use crate::string::StringRuntime;
13use crate::thread::Thread;
14use crate::value::{TValue, TValueCursor};
15use crate::{VmControl, VmError, VmExit, VmResult};
16
17/// Unstable VM error-state capability.
18///
19/// # Safety
20///
21/// The thread must be live, and every cursor must address its current stack.
22/// Callers must preserve the stack shape required by the error protocol.
23#[allow(
24    clippy::missing_safety_doc,
25    reason = "all methods share the capability-level safety contract"
26)]
27pub trait ErrorRuntime: Sealed {
28    unsafe fn set_error_object(&self, error_code: i32, old_top: TValueCursor);
29}
30
31impl ErrorRuntime for Thread {
32    /// `luaD_seterrorobj`
33    unsafe fn set_error_object(&self, error_code: i32, old_top: TValueCursor) {
34        unsafe {
35            match error_code {
36                x if x == THREAD_STATUS_ERR_MEM as i32 => {
37                    let message = self
38                        .intern_string(crate::state::LUA_MEMERRMSG.as_bstr())
39                        .expect("memory error message is fixed during state initialization");
40                    old_top.value_unchecked().set_string_value(message)
41                }
42                x if x == THREAD_STATUS_ERR_ERR as i32 => {
43                    let message = self
44                        .intern_string(crate::state::LUA_ERRERRMSG.as_bstr())
45                        .expect("error handler message is fixed during state initialization");
46                    old_top.value_unchecked().set_string_value(message)
47                }
48                x if x == THREAD_STATUS_ERR_SYNTAX as i32 || x == THREAD_STATUS_ERR_RUN as i32 => {
49                    let top_value = self.stack_top().sub(1).value_unchecked();
50                    old_top.value_unchecked().set_obj(top_value);
51                }
52                _ => unreachable!("invalid Luau error code {error_code}"),
53            }
54
55            self.set_stack_top(old_top.add(1));
56        }
57    }
58}
59
60#[repr(C)]
61pub(crate) struct ErrorFunctionContext {
62    pub(crate) error_function: TValue,
63}
64
65/// `callerrfunc`
66pub(crate) unsafe fn call_error_function(
67    thread: &Thread,
68    context: &mut ErrorFunctionContext,
69) -> VmResult {
70    unsafe {
71        let top = thread.stack_top();
72        let top_offset = thread.save_stack(top);
73
74        top.value_unchecked().set_obj(top.sub(1).value_unchecked());
75        top.sub(1).value_unchecked().set_obj(context.error_function);
76
77        if thread.check_stack(1) == 0 {
78            return crate::run_error!(thread, "stack limit").map_err(Into::into);
79        }
80
81        let top = thread.restore_stack(top_offset);
82        thread.set_stack_top(top.add(1));
83        thread.call_no_yield(top.sub(1), 1)?;
84    }
85    Ok(())
86}
87
88pub type Pfunc<T> = unsafe fn(&Thread, &mut T) -> VmResult;
89
90/// Unstable protected-execution capability.
91///
92/// # Safety
93///
94/// The thread, callback context, saved stack offsets, and error-function
95/// position must remain valid for the protected operation. The callback must
96/// obey the VM unwind protocol and may not let Rust unwinding cross the VM.
97#[allow(
98    clippy::missing_safety_doc,
99    reason = "all methods share the capability-level safety contract"
100)]
101pub trait ProtectedCall: Sealed {
102    unsafe fn raw_run_protected<T>(&self, function: Pfunc<T>, userdata: &mut T) -> VmResult;
103    unsafe fn protected_call_internal<T>(
104        &self,
105        function: Pfunc<T>,
106        userdata: &mut T,
107        old_top: isize,
108        error_function: isize,
109    ) -> VmResult;
110}
111
112#[repr(C)]
113pub struct LuaProtectedErrorFrame {
114    _private: [u8; 0],
115}
116
117#[repr(C)]
118struct ActiveProtectedErrorFrame {
119    prev: *mut LuaProtectedErrorFrame,
120}
121
122struct ProtectedErrorGuard {
123    global: GlobalState,
124    prev: *mut LuaProtectedErrorFrame,
125}
126
127impl Drop for ProtectedErrorGuard {
128    fn drop(&mut self) {
129        self.global.set_protected_error(self.prev);
130    }
131}
132
133impl GlobalState {
134    pub fn protected_error(&self) -> *mut LuaProtectedErrorFrame {
135        unsafe { self.as_ptr().as_ref().unwrap_unchecked().protected_error }
136    }
137
138    pub fn set_protected_error(&self, protected_error: *mut LuaProtectedErrorFrame) {
139        unsafe {
140            self.as_ptr().as_mut().unwrap_unchecked().protected_error = protected_error;
141        }
142    }
143}
144
145impl ProtectedCall for Thread {
146    /// `luaD_rawrunprotected`
147    unsafe fn raw_run_protected<T>(&self, function: Pfunc<T>, userdata: &mut T) -> VmResult {
148        let global = unsafe { self.global() };
149        let mut frame = ActiveProtectedErrorFrame {
150            prev: global.protected_error(),
151        };
152        global.set_protected_error((&raw mut frame).cast());
153        let _protected_error_guard = ProtectedErrorGuard {
154            global,
155            prev: frame.prev,
156        };
157
158        unsafe { function(self, userdata) }
159    }
160
161    /// `luaD_pcall`
162    unsafe fn protected_call_internal<T>(
163        &self,
164        function: Pfunc<T>,
165        userdata: &mut T,
166        old_top: isize,
167        error_function: isize,
168    ) -> VmResult {
169        unsafe {
170            let old_native_call_depth = self.as_ptr().as_ref().unwrap_unchecked().native_call_depth;
171            let old_base_native_call_depth = self
172                .as_ptr()
173                .as_ref()
174                .unwrap_unchecked()
175                .base_native_call_depth;
176            let old_ci = self.save_ci(self.current_call_info_cursor());
177            let old_active = self.as_ptr().as_ref().unwrap_unchecked().is_active;
178            let mut result = self.raw_run_protected(function, userdata);
179
180            if let Err(exit) = result {
181                let VmExit::Error(mut error) = exit else {
182                    return result;
183                };
184                let mut error_object = error;
185
186                if error_function != 0 {
187                    if error != VmError::Runtime {
188                        self.set_error_object(error.status(), self.stack_top());
189                    }
190
191                    let mut error_context = ErrorFunctionContext {
192                        error_function: self.restore_stack(error_function).value_unchecked(),
193                    };
194                    let error_function_result =
195                        self.raw_run_protected(call_error_function, &mut error_context);
196
197                    error_object = match error_function_result {
198                        Ok(()) => VmError::Runtime,
199                        Err(VmExit::Error(VmError::Memory)) if error == VmError::Memory => {
200                            VmError::Memory
201                        }
202                        Err(VmExit::Error(_)) => {
203                            error = VmError::ErrorHandler;
204                            VmError::ErrorHandler
205                        }
206                        Err(exit) => return Err(exit),
207                    };
208
209                    result = Err(VmExit::Error(error));
210                }
211
212                if !old_active {
213                    self.as_ptr().as_mut().unwrap_unchecked().is_active = false;
214                }
215
216                let yieldable = self.as_ptr().as_ref().unwrap_unchecked().native_call_depth
217                    <= self
218                        .as_ptr()
219                        .as_ref()
220                        .unwrap_unchecked()
221                        .base_native_call_depth;
222                self.as_ptr().as_mut().unwrap_unchecked().native_call_depth = old_native_call_depth;
223                self.as_ptr()
224                    .as_mut()
225                    .unwrap_unchecked()
226                    .base_native_call_depth = old_base_native_call_depth;
227
228                if yieldable
229                    && let Some(callback) = self.global().protected_error_callback()
230                    && callback(self) == ProtectedErrorAction::Break
231                {
232                    self.as_ptr().as_mut().unwrap_unchecked().status = THREAD_STATUS_BREAK;
233                    return Err(VmExit::Control(VmControl::Break));
234                }
235
236                let restored_old_top = self.restore_stack(old_top);
237                let restored_ci = self.restore_ci(old_ci);
238                if self.open_upvalue().is_some() {
239                    self.close(restored_old_top.value_unchecked());
240                }
241
242                self.set_error_object(error_object.status(), restored_old_top);
243                self.set_current_call_info(restored_ci);
244                self.set_stack_base(restored_ci.call_info_unchecked().base());
245                self.restore_stack_limit()?;
246            }
247
248            result
249        }
250    }
251}