godot_core/
private.rs

1/*
2 * Copyright (c) godot-rust; Bromeon and contributors.
3 * This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
5 * file, You can obtain one at https://mozilla.org/MPL/2.0/.
6 */
7
8#[cfg(debug_assertions)] #[cfg_attr(published_docs, doc(cfg(debug_assertions)))]
9use std::cell::RefCell;
10use std::io::Write;
11use std::sync::atomic;
12
13use sys::Global;
14
15use crate::global::godot_error;
16use crate::meta::error::CallError;
17use crate::meta::CallContext;
18use crate::obj::Gd;
19use crate::{classes, sys};
20
21// ----------------------------------------------------------------------------------------------------------------------------------------------
22// Public re-exports
23
24mod reexport_pub {
25    pub use crate::gen::classes::class_macros;
26    #[cfg(feature = "trace")] #[cfg_attr(published_docs, doc(cfg(feature = "trace")))]
27    pub use crate::meta::trace;
28    pub use crate::obj::rtti::ObjectRtti;
29    pub use crate::registry::callbacks;
30    pub use crate::registry::plugin::{
31        ClassPlugin, DynTraitImpl, ErasedDynGd, ErasedRegisterFn, ITraitImpl, InherentImpl,
32        PluginItem, Struct,
33    };
34    #[cfg(since_api = "4.2")] #[cfg_attr(published_docs, doc(cfg(since_api = "4.2")))]
35    pub use crate::registry::signal::priv_re_export::*;
36    pub use crate::storage::{as_storage, Storage};
37    pub use crate::sys::out;
38}
39pub use reexport_pub::*;
40
41// ----------------------------------------------------------------------------------------------------------------------------------------------
42// Global variables
43
44static CALL_ERRORS: Global<CallErrors> = Global::default();
45
46/// Level:
47/// - 0: no error printing (during `expect_panic` in test)
48/// - 1: not yet implemented, but intended for `try_` function calls (which are expected to fail, so error is annoying)
49/// - 2: normal printing
50static ERROR_PRINT_LEVEL: atomic::AtomicU8 = atomic::AtomicU8::new(2);
51
52sys::plugin_registry!(pub __GODOT_PLUGIN_REGISTRY: ClassPlugin);
53
54// ----------------------------------------------------------------------------------------------------------------------------------------------
55// Call error handling
56
57// Note: if this leads to many allocated IDs that are not removed, we could limit to 1 per thread-ID.
58// Would need to check if re-entrant calls with multiple errors per thread are possible.
59struct CallErrors {
60    ring_buffer: Vec<Option<CallError>>,
61    next_id: u8,
62    generation: u16,
63}
64
65impl Default for CallErrors {
66    fn default() -> Self {
67        Self {
68            ring_buffer: [const { None }; Self::MAX_ENTRIES as usize].into(),
69            next_id: 0,
70            generation: 0,
71        }
72    }
73}
74
75impl CallErrors {
76    const MAX_ENTRIES: u8 = 32;
77
78    fn insert(&mut self, err: CallError) -> i32 {
79        let id = self.next_id;
80
81        self.next_id = self.next_id.wrapping_add(1) % Self::MAX_ENTRIES;
82        if self.next_id == 0 {
83            self.generation = self.generation.wrapping_add(1);
84        }
85
86        self.ring_buffer[id as usize] = Some(err);
87
88        (self.generation as i32) << 16 | id as i32
89    }
90
91    // Returns success or failure.
92    fn remove(&mut self, id: i32) -> Option<CallError> {
93        let generation = (id >> 16) as u16;
94        let id = id as u8;
95
96        // If id < next_id, the generation must be the current one -- otherwise the one before.
97        if id < self.next_id {
98            if generation != self.generation {
99                return None;
100            }
101        } else if generation != self.generation.wrapping_sub(1) {
102            return None;
103        }
104
105        // Returns Some if there's still an entry, None if it was already removed.
106        self.ring_buffer[id as usize].take()
107    }
108}
109
110/// Inserts a `CallError` into a global variable and returns its ID to later remove it.
111fn call_error_insert(err: CallError) -> i32 {
112    // Wraps around if entire i32 is depleted. If this happens in practice (unlikely, users need to deliberately ignore errors that are printed),
113    // we just overwrite the oldest errors, should still work.
114    let id = CALL_ERRORS.lock().insert(err);
115    id
116}
117
118pub(crate) fn call_error_remove(in_error: &sys::GDExtensionCallError) -> Option<CallError> {
119    // Error checks are just quality-of-life diagnostic; do not throw panics if they fail.
120
121    if in_error.error != sys::GODOT_RUST_CUSTOM_CALL_ERROR {
122        godot_error!("Tried to remove non-godot-rust call error {in_error:?}");
123        return None;
124    }
125
126    let call_error = CALL_ERRORS.lock().remove(in_error.argument);
127    if call_error.is_none() {
128        // Just a quality-of-life diagnostic; do not throw panics if something like this fails.
129        godot_error!("Failed to remove call error {in_error:?}");
130    }
131
132    call_error
133}
134
135// ----------------------------------------------------------------------------------------------------------------------------------------------
136// Plugin and global state handling
137
138pub fn next_class_id() -> u16 {
139    static NEXT_CLASS_ID: atomic::AtomicU16 = atomic::AtomicU16::new(0);
140    NEXT_CLASS_ID.fetch_add(1, atomic::Ordering::Relaxed)
141}
142
143pub(crate) fn iterate_plugins(mut visitor: impl FnMut(&ClassPlugin)) {
144    sys::plugin_foreach!(__GODOT_PLUGIN_REGISTRY; visitor);
145}
146
147#[cfg(feature = "codegen-full")] // Remove if used in other scenarios.
148pub(crate) fn find_inherent_impl(class_name: crate::meta::ClassName) -> Option<InherentImpl> {
149    // We do this manually instead of using `iterate_plugins()` because we want to break as soon as we find a match.
150    let plugins = __GODOT_PLUGIN_REGISTRY.lock().unwrap();
151
152    plugins.iter().find_map(|elem| {
153        if elem.class_name == class_name {
154            if let PluginItem::InherentImpl(inherent_impl) = &elem.item {
155                return Some(inherent_impl.clone());
156            }
157        }
158
159        None
160    })
161}
162
163// ----------------------------------------------------------------------------------------------------------------------------------------------
164// Traits and types
165
166// If someone forgets #[godot_api], this causes a compile error, rather than virtual functions not being called at runtime.
167#[allow(non_camel_case_types)]
168#[diagnostic::on_unimplemented(
169    message = "`impl` blocks for Godot classes require the `#[godot_api]` attribute",
170    label = "missing `#[godot_api]` before `impl`",
171    note = "see also: https://godot-rust.github.io/book/register/functions.html#godot-special-functions"
172)]
173pub trait You_forgot_the_attribute__godot_api {}
174
175pub struct ClassConfig {
176    pub is_tool: bool,
177}
178
179// ----------------------------------------------------------------------------------------------------------------------------------------------
180// Capability queries and internal access
181
182pub fn auto_init<T>(l: &mut crate::obj::OnReady<T>, base: &crate::obj::Gd<crate::classes::Node>) {
183    l.init_auto(base);
184}
185
186#[cfg(since_api = "4.3")] #[cfg_attr(published_docs, doc(cfg(since_api = "4.3")))]
187pub unsafe fn has_virtual_script_method(
188    object_ptr: sys::GDExtensionObjectPtr,
189    method_sname: sys::GDExtensionConstStringNamePtr,
190) -> bool {
191    sys::interface_fn!(object_has_script_method)(sys::to_const_ptr(object_ptr), method_sname) != 0
192}
193
194/// Ensure `T` is an editor plugin.
195pub const fn is_editor_plugin<T: crate::obj::Inherits<crate::classes::EditorPlugin>>() {}
196
197// Starting from 4.3, Godot has "runtime classes"; this emulation is no longer needed.
198#[cfg(before_api = "4.3")] #[cfg_attr(published_docs, doc(cfg(before_api = "4.3")))]
199pub fn is_class_inactive(is_tool: bool) -> bool {
200    if is_tool {
201        return false;
202    }
203
204    // SAFETY: only invoked after global library initialization.
205    let global_config = unsafe { sys::config() };
206    let is_editor = || crate::classes::Engine::singleton().is_editor_hint();
207
208    global_config.tool_only_in_editor //.
209        && global_config.is_editor_or_init(is_editor)
210}
211
212// Starting from 4.3, Godot has "runtime classes"; we only need to check whether editor is running.
213#[cfg(since_api = "4.3")] #[cfg_attr(published_docs, doc(cfg(since_api = "4.3")))]
214pub fn is_class_runtime(is_tool: bool) -> bool {
215    if is_tool {
216        return false;
217    }
218
219    // SAFETY: only invoked after global library initialization.
220    let global_config = unsafe { sys::config() };
221
222    // If this is not a #[class(tool)] and we only run tool classes in the editor, then don't run this in editor -> make it a runtime class.
223    // If we run all classes in the editor (!tool_only_in_editor), then it's not a runtime class.
224    global_config.tool_only_in_editor
225}
226
227// ----------------------------------------------------------------------------------------------------------------------------------------------
228// Panic *hook* management
229
230pub fn extract_panic_message(err: &(dyn Send + std::any::Any)) -> String {
231    if let Some(s) = err.downcast_ref::<&'static str>() {
232        s.to_string()
233    } else if let Some(s) = err.downcast_ref::<String>() {
234        s.clone()
235    } else {
236        format!("(panic of type ID {:?})", err.type_id())
237    }
238}
239
240pub fn format_panic_message(panic_info: &std::panic::PanicHookInfo) -> String {
241    let mut msg = extract_panic_message(panic_info.payload());
242
243    if let Some(context) = get_gdext_panic_context() {
244        msg = format!("{msg}\nContext: {context}");
245    }
246
247    let prefix = if let Some(location) = panic_info.location() {
248        format!("panic {}:{}", location.file(), location.line())
249    } else {
250        "panic".to_string()
251    };
252
253    // If the message contains newlines, print all of the lines after a line break, and indent them.
254    let lbegin = "\n  ";
255    let indented = msg.replace('\n', lbegin);
256
257    if indented.len() != msg.len() {
258        format!("[{prefix}]{lbegin}{indented}")
259    } else {
260        format!("[{prefix}]  {msg}")
261    }
262}
263
264// Macro instead of function, to avoid 1 extra frame in backtrace.
265#[cfg(debug_assertions)] #[cfg_attr(published_docs, doc(cfg(debug_assertions)))]
266#[macro_export]
267macro_rules! format_backtrace {
268    ($prefix:expr, $backtrace:expr) => {{
269        use std::backtrace::BacktraceStatus;
270
271        let backtrace = $backtrace;
272
273        match backtrace.status() {
274            BacktraceStatus::Captured => format!("\n[{}]\n{}\n", $prefix, backtrace),
275            BacktraceStatus::Disabled => {
276                "(backtrace disabled, run application with `RUST_BACKTRACE=1` environment variable)"
277                    .to_string()
278            }
279            BacktraceStatus::Unsupported => {
280                "(backtrace unsupported for current platform)".to_string()
281            }
282            _ => "(backtrace status unknown)".to_string(),
283        }
284    }};
285
286    ($prefix:expr) => {
287        $crate::format_backtrace!($prefix, std::backtrace::Backtrace::capture())
288    };
289}
290
291#[cfg(not(debug_assertions))] #[cfg_attr(published_docs, doc(cfg(not(debug_assertions))))]
292#[macro_export]
293macro_rules! format_backtrace {
294    ($prefix:expr $(, $backtrace:expr)? ) => {
295        String::new()
296    };
297}
298
299pub fn set_gdext_hook<F>(godot_print: F)
300where
301    F: Fn() -> bool + Send + Sync + 'static,
302{
303    std::panic::set_hook(Box::new(move |panic_info| {
304        // Flush, to make sure previous Rust output (e.g. test announcement, or debug prints during app) have been printed.
305        let _ignored_result = std::io::stdout().flush();
306
307        let message = format_panic_message(panic_info);
308        if godot_print() {
309            // Also prints to stdout/stderr -- do not print twice.
310            godot_error!("{message}");
311        } else {
312            eprintln!("{message}");
313        }
314
315        let backtrace = format_backtrace!("panic backtrace");
316        eprintln!("{backtrace}");
317        let _ignored_result = std::io::stderr().flush();
318    }));
319}
320
321pub fn set_error_print_level(level: u8) -> u8 {
322    assert!(level <= 2);
323    ERROR_PRINT_LEVEL.swap(level, atomic::Ordering::Relaxed)
324}
325
326pub(crate) fn has_error_print_level(level: u8) -> bool {
327    assert!(level <= 2);
328    ERROR_PRINT_LEVEL.load(atomic::Ordering::Relaxed) >= level
329}
330
331/// Internal type used to store context information for debug purposes. Debug context is stored on the thread-local
332/// ERROR_CONTEXT_STACK, which can later be used to retrieve the current context in the event of a panic. This value
333/// probably shouldn't be used directly; use ['get_gdext_panic_context()'](get_gdext_panic_context) instead.
334#[cfg(debug_assertions)] #[cfg_attr(published_docs, doc(cfg(debug_assertions)))]
335struct ScopedFunctionStack {
336    functions: Vec<*const dyn Fn() -> String>,
337}
338
339#[cfg(debug_assertions)] #[cfg_attr(published_docs, doc(cfg(debug_assertions)))]
340impl ScopedFunctionStack {
341    /// # Safety
342    /// Function must be removed (using [`pop_function()`](Self::pop_function)) before lifetime is invalidated.
343    unsafe fn push_function(&mut self, function: &dyn Fn() -> String) {
344        let function = std::ptr::from_ref(function);
345        #[allow(clippy::unnecessary_cast)]
346        let function = function as *const (dyn Fn() -> String + 'static);
347        self.functions.push(function);
348    }
349
350    fn pop_function(&mut self) {
351        self.functions.pop().expect("function stack is empty!");
352    }
353
354    fn get_last(&self) -> Option<String> {
355        self.functions.last().cloned().map(|pointer| {
356            // SAFETY:
357            // Invariants provided by push_function assert that any and all functions held by ScopedFunctionStack
358            // are removed before they are invalidated; functions must always be valid.
359            unsafe { (*pointer)() }
360        })
361    }
362}
363
364#[cfg(debug_assertions)] #[cfg_attr(published_docs, doc(cfg(debug_assertions)))]
365thread_local! {
366    static ERROR_CONTEXT_STACK: RefCell<ScopedFunctionStack> = const {
367        RefCell::new(ScopedFunctionStack { functions: Vec::new() })
368    }
369}
370
371// Value may return `None`, even from panic hook, if called from a non-Godot thread.
372pub fn get_gdext_panic_context() -> Option<String> {
373    #[cfg(debug_assertions)] #[cfg_attr(published_docs, doc(cfg(debug_assertions)))]
374    return ERROR_CONTEXT_STACK.with(|cell| cell.borrow().get_last());
375
376    #[cfg(not(debug_assertions))] #[cfg_attr(published_docs, doc(cfg(not(debug_assertions))))]
377    None
378}
379
380// ----------------------------------------------------------------------------------------------------------------------------------------------
381// Panic unwinding and catching
382
383pub struct PanicPayload {
384    payload: Box<dyn std::any::Any + Send + 'static>,
385}
386
387impl PanicPayload {
388    pub fn new(payload: Box<dyn std::any::Any + Send + 'static>) -> Self {
389        Self { payload }
390    }
391
392    // While this could be `&self`, it's usually good practice to pass panic payloads around linearly and have only 1 representation at a time.
393    pub fn into_panic_message(self) -> String {
394        extract_panic_message(self.payload.as_ref())
395    }
396
397    pub fn repanic(self) -> ! {
398        std::panic::resume_unwind(self.payload)
399    }
400}
401
402/// Executes `code`. If a panic is thrown, it is caught and an error message is printed to Godot.
403///
404/// Returns `Err(message)` if a panic occurred, and `Ok(result)` with the result of `code` otherwise.
405///
406/// In contrast to [`handle_varcall_panic`] and [`handle_ptrcall_panic`], this function is not intended for use in `try_` functions,
407/// where the error is propagated as a `CallError` in a global variable.
408pub fn handle_panic<E, F, R>(error_context: E, code: F) -> Result<R, PanicPayload>
409where
410    E: Fn() -> String,
411    F: FnOnce() -> R + std::panic::UnwindSafe,
412{
413    #[cfg(not(debug_assertions))] #[cfg_attr(published_docs, doc(cfg(not(debug_assertions))))]
414    let _ = error_context; // Unused in Release.
415
416    #[cfg(debug_assertions)] #[cfg_attr(published_docs, doc(cfg(debug_assertions)))]
417    ERROR_CONTEXT_STACK.with(|cell| unsafe {
418        // SAFETY: &error_context is valid for lifetime of function, and is removed from LAST_ERROR_CONTEXT before end of function.
419        cell.borrow_mut().push_function(&error_context)
420    });
421
422    let result = std::panic::catch_unwind(code).map_err(PanicPayload::new);
423
424    #[cfg(debug_assertions)] #[cfg_attr(published_docs, doc(cfg(debug_assertions)))]
425    ERROR_CONTEXT_STACK.with(|cell| cell.borrow_mut().pop_function());
426    result
427}
428
429// TODO(bromeon): make call_ctx lazy-evaluated (like error_ctx) everywhere;
430// or make it eager everywhere and ensure it's cheaply constructed in the call sites.
431pub fn handle_varcall_panic<F, R>(
432    call_ctx: &CallContext,
433    out_err: &mut sys::GDExtensionCallError,
434    code: F,
435) where
436    F: FnOnce() -> Result<R, CallError> + std::panic::UnwindSafe,
437{
438    let outcome: Result<Result<R, CallError>, PanicPayload> =
439        handle_panic(|| call_ctx.to_string(), code);
440
441    let call_error = match outcome {
442        // All good.
443        Ok(Ok(_result)) => return,
444
445        // Call error signalled by Godot's or gdext's validation.
446        Ok(Err(err)) => err,
447
448        // Panic occurred (typically through user): forward message.
449        Err(panic_msg) => CallError::failed_by_user_panic(call_ctx, panic_msg),
450    };
451
452    let error_id = report_call_error(call_error, true);
453
454    // Abuse 'argument' field to store our ID.
455    *out_err = sys::GDExtensionCallError {
456        error: sys::GODOT_RUST_CUSTOM_CALL_ERROR,
457        argument: error_id,
458        expected: 0,
459    };
460
461    //sys::interface_fn!(variant_new_nil)(sys::AsUninit::as_uninit(ret));
462}
463
464pub fn handle_ptrcall_panic<F, R>(call_ctx: &CallContext, code: F)
465where
466    F: FnOnce() -> R + std::panic::UnwindSafe,
467{
468    let outcome: Result<R, PanicPayload> = handle_panic(|| call_ctx.to_string(), code);
469
470    let call_error = match outcome {
471        // All good.
472        Ok(_result) => return,
473
474        // Panic occurred (typically through user): forward message.
475        Err(payload) => CallError::failed_by_user_panic(call_ctx, payload),
476    };
477
478    let _id = report_call_error(call_error, false);
479}
480
481fn report_call_error(call_error: CallError, track_globally: bool) -> i32 {
482    // Print failed calls to Godot's console.
483    // TODO Level 1 is not yet set, so this will always print if level != 0. Needs better logic to recognize try_* calls and avoid printing.
484    // But a bit tricky with multiple threads and re-entrancy; maybe pass in info in error struct.
485    if has_error_print_level(2) {
486        godot_error!("{call_error}");
487    }
488
489    // Once there is a way to auto-remove added errors, this could be always true.
490    if track_globally {
491        call_error_insert(call_error)
492    } else {
493        0
494    }
495}
496
497// Currently unused; implemented due to temporary need and may come in handy.
498pub fn rebuild_gd(object_ref: &classes::Object) -> Gd<classes::Object> {
499    let ptr = object_ref.__object_ptr();
500
501    // SAFETY: ptr comes from valid internal API (and is non-null, so unwrap in from_obj_sys won't fail).
502    unsafe { Gd::from_obj_sys(ptr) }
503}
504
505// ----------------------------------------------------------------------------------------------------------------------------------------------
506
507#[cfg(test)] #[cfg_attr(published_docs, doc(cfg(test)))]
508mod tests {
509    use super::{CallError, CallErrors, PanicPayload};
510    use crate::meta::CallContext;
511
512    fn make(index: usize) -> CallError {
513        let method_name = format!("method_{index}");
514        let ctx = CallContext::func("Class", &method_name);
515        let payload = PanicPayload::new(Box::new("some panic reason".to_string()));
516
517        CallError::failed_by_user_panic(&ctx, payload)
518    }
519
520    #[test]
521    fn test_call_errors() {
522        let mut store = CallErrors::default();
523
524        let mut id07 = 0;
525        let mut id13 = 0;
526        let mut id20 = 0;
527        for i in 0..24 {
528            let id = store.insert(make(i));
529            match i {
530                7 => id07 = id,
531                13 => id13 = id,
532                20 => id20 = id,
533                _ => {}
534            }
535        }
536
537        let e = store.remove(id20).expect("must be present");
538        assert_eq!(e.method_name(), "method_20");
539
540        let e = store.remove(id20);
541        assert!(e.is_none());
542
543        for i in 24..CallErrors::MAX_ENTRIES as usize {
544            store.insert(make(i));
545        }
546        for i in 0..10 {
547            store.insert(make(i));
548        }
549
550        let e = store.remove(id07);
551        assert!(e.is_none(), "generation overwritten");
552
553        let e = store.remove(id13).expect("generation not yet overwritten");
554        assert_eq!(e.method_name(), "method_13");
555    }
556}