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