Skip to main content

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
8use std::cell::Cell;
9#[cfg(safeguards_strict)] #[cfg_attr(published_docs, doc(cfg(safeguards_strict)))]
10use std::cell::RefCell;
11use std::io::Write;
12use std::sync::atomic;
13
14use crate::global::godot_error;
15use crate::meta::error::{CallError, CallResult};
16use crate::obj::Gd;
17use crate::registry::property::Var;
18use crate::{classes, sys};
19
20// ----------------------------------------------------------------------------------------------------------------------------------------------
21// Public re-exports
22
23mod reexport_pub {
24    pub use crate::arg_into_owned;
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::r#gen::classes::class_macros;
28    pub use crate::r#gen::virtuals; // virtual fn names, hashes, signatures
29    pub use crate::meta::private_reexport::*;
30    #[cfg(feature = "trace")] #[cfg_attr(published_docs, doc(cfg(feature = "trace")))]
31    pub use crate::meta::{CowArg, FfiArg, trace};
32    pub use crate::obj::rtti::ObjectRtti;
33    pub use crate::obj::signal::priv_re_export::*;
34    pub use crate::registry::callbacks;
35    pub use crate::registry::plugin::{
36        ClassPlugin, DynTraitImpl, ErasedDynGd, ErasedRegisterFn, ITraitImpl, InherentImpl,
37        PluginItem, Struct,
38    };
39    pub use crate::storage::{
40        IntoVirtualMethodReceiver, RecvGdSelf, RecvMut, RecvRef, Storage, VirtualMethodReceiver,
41        as_storage,
42    };
43    pub use crate::sys::out;
44}
45pub use reexport_pub::*;
46
47// ----------------------------------------------------------------------------------------------------------------------------------------------
48// Global variables
49
50/// Level:
51/// - 0: no error printing (during `expect_panic` in test)
52/// - 1: not yet implemented, but intended for `try_` function calls (which are expected to fail, so error is annoying)
53/// - 2: normal printing
54static ERROR_PRINT_LEVEL: atomic::AtomicU8 = atomic::AtomicU8::new(2);
55
56sys::plugin_registry!(pub __GODOT_PLUGIN_REGISTRY: ClassPlugin);
57#[cfg(all(since_api = "4.3", feature = "register-docs"))] #[cfg_attr(published_docs, doc(cfg(all(since_api = "4.3", feature = "register-docs"))))]
58sys::plugin_registry!(pub __GODOT_DOCS_REGISTRY: DocsPlugin);
59
60// ----------------------------------------------------------------------------------------------------------------------------------------------
61// Call error handling
62
63// Thread-local storage for rich `CallError` produced by `#[func]` methods returning `Result<T, E>`.
64//
65// When a Rust `#[func]` fails (returns Err), the error is stashed here so that Rust's `try_call()` can retrieve it
66// after the Godot round-trip. The varcall FFI callback simultaneously sets `CALL_FAILED_STATUS` so that Godot's own
67// GDScript VM recognizes the failure and aborts the calling script function.
68//
69// Thread-safety: varcall callbacks execute on the calling thread, and `try_call` reads the result on the same
70// thread before any other call can overwrite it. No mutex is needed.
71thread_local! {
72    static LAST_CALL_ERROR: Cell<Option<CallError>> = const { Cell::new(None) };
73
74    // Depth of active Rust-initiated out-calls to Godot on the class out-call path (`out_class_varcall`, reached via
75    // `call`/`try_call`). When > 0, we're waiting for an FFI round-trip. If Godot re-enters Rust and a `#[func]` fails,
76    // the Rust caller will observe the error via the `CallResult`/`CallError` return -- so the in-Godot print would
77    // just be noise. Panic prints are still emitted (backtrace info is worth keeping regardless of out-call context).
78    static OUT_CALL_DEPTH: Cell<u32> = const { Cell::new(0) };
79}
80
81/// Store a [`CallError`] in thread-local storage for later retrieval by [`call_error_take`].
82fn call_error_store(err: CallError) {
83    LAST_CALL_ERROR.set(Some(err));
84}
85
86/// Take the [`CallError`] previously stored by [`call_error_store`], if any.
87///
88/// Returns `None` if no error was stored (i.e. the failure originated from Godot, not from gdext).
89pub(crate) fn call_error_take() -> Option<CallError> {
90    LAST_CALL_ERROR.take()
91}
92
93/// RAII guard marking that a Rust-initiated out-call to Godot is in progress on this thread.
94///
95/// While any guard is live, inbound `#[func]` failures on the same thread skip their `godot_error!` print, since the Rust
96/// caller already observes the failure via the returned `CallError`/panic and the extra print would be redundant noise.
97pub(crate) struct OutCallGuard;
98
99impl OutCallGuard {
100    #[must_use = "guard must be bound to a local; dropping it immediately ends the out-call scope"]
101    pub fn new() -> Self {
102        OUT_CALL_DEPTH.with(|d| d.set(d.get() + 1));
103        Self
104    }
105}
106
107impl Drop for OutCallGuard {
108    fn drop(&mut self) {
109        OUT_CALL_DEPTH.with(|d| d.set(d.get() - 1));
110    }
111}
112
113// ----------------------------------------------------------------------------------------------------------------------------------------------
114// Plugin and global state handling
115
116pub fn next_class_id() -> u16 {
117    static NEXT_CLASS_ID: atomic::AtomicU16 = atomic::AtomicU16::new(0);
118    NEXT_CLASS_ID.fetch_add(1, atomic::Ordering::Relaxed)
119}
120
121pub(crate) fn iterate_plugins(mut visitor: impl FnMut(&ClassPlugin)) {
122    sys::plugin_foreach!(__GODOT_PLUGIN_REGISTRY; visitor);
123}
124
125#[cfg(all(since_api = "4.3", feature = "register-docs"))] #[cfg_attr(published_docs, doc(cfg(all(since_api = "4.3", feature = "register-docs"))))]
126pub(crate) fn iterate_docs_plugins(mut visitor: impl FnMut(&DocsPlugin)) {
127    sys::plugin_foreach!(__GODOT_DOCS_REGISTRY; visitor);
128}
129
130#[cfg(feature = "codegen-full")] // Remove if used in other scenarios.
131pub(crate) fn find_inherent_impl(class_name: crate::meta::ClassId) -> Option<InherentImpl> {
132    // We do this manually instead of using `iterate_plugins()` because we want to break as soon as we find a match.
133    let plugins = __GODOT_PLUGIN_REGISTRY.lock().unwrap();
134
135    plugins.iter().find_map(|elem| {
136        if elem.class_name == class_name
137            && let PluginItem::InherentImpl(inherent_impl) = &elem.item
138        {
139            return Some(inherent_impl.clone());
140        }
141
142        None
143    })
144}
145
146// ----------------------------------------------------------------------------------------------------------------------------------------------
147// Traits and types
148
149// If someone forgets #[godot_api], this causes a compile error, rather than virtual functions not being called at runtime.
150#[allow(non_camel_case_types)]
151#[diagnostic::on_unimplemented(
152    message = "`impl` blocks for Godot classes require the `#[godot_api]` attribute",
153    label = "missing `#[godot_api]` before `impl`",
154    note = "see also: https://godot-rust.github.io/book/register/functions.html#godot-special-functions"
155)]
156pub trait You_forgot_the_attribute__godot_api {}
157
158pub struct ClassConfig {
159    pub is_tool: bool,
160}
161
162// ----------------------------------------------------------------------------------------------------------------------------------------------
163// Type-checkers for user-defined getters/setters in Var
164
165// These functions are used to generate nice error messages if a #[var(get)], [var(get = my_getter)] etc. mismatches types.
166// Don't modify without thorough UX testing; the use of `impl Fn` vs. `fn` is deliberate.
167pub fn typecheck_getter<C, T: Var>(_getter: impl Fn(&C) -> T::PubType) {}
168pub fn typecheck_setter<C, T: Var>(_setter: fn(&mut C, T::PubType)) {}
169
170// ----------------------------------------------------------------------------------------------------------------------------------------------
171// Capability queries and internal access
172
173pub fn auto_init<T>(l: &mut crate::obj::OnReady<T>, base: &Gd<classes::Node>) {
174    l.init_auto(base);
175}
176
177#[cfg(since_api = "4.3")] #[cfg_attr(published_docs, doc(cfg(since_api = "4.3")))]
178pub unsafe fn has_virtual_script_method(
179    object_ptr: sys::GDExtensionObjectPtr,
180    method_sname: sys::GDExtensionConstStringNamePtr,
181) -> bool {
182    unsafe {
183        sys::interface_fn!(object_has_script_method)(sys::to_const_ptr(object_ptr), method_sname)
184            != 0
185    }
186}
187
188/// Ensure `T` is an editor plugin.
189pub const fn is_editor_plugin<T: crate::obj::Inherits<classes::EditorPlugin>>() {}
190
191// Starting from 4.3, Godot has "runtime classes"; this emulation is no longer needed.
192#[cfg(before_api = "4.3")] #[cfg_attr(published_docs, doc(cfg(before_api = "4.3")))]
193pub fn is_class_inactive(is_tool: bool) -> bool {
194    use crate::obj::Singleton;
195
196    if is_tool {
197        return false;
198    }
199
200    // SAFETY: only invoked after global library initialization.
201    let global_config = unsafe { sys::config() };
202    let is_editor = || crate::classes::Engine::singleton().is_editor_hint();
203
204    global_config.tool_only_in_editor //.
205        && global_config.is_editor_or_init(is_editor)
206}
207
208// Starting from 4.3, Godot has "runtime classes"; we only need to check whether editor is running.
209#[cfg(since_api = "4.3")] #[cfg_attr(published_docs, doc(cfg(since_api = "4.3")))]
210pub fn is_class_runtime(is_tool: bool) -> bool {
211    if is_tool {
212        return false;
213    }
214
215    // SAFETY: only invoked after global library initialization.
216    let global_config = unsafe { sys::config() };
217
218    // 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.
219    // If we run all classes in the editor (!tool_only_in_editor), then it's not a runtime class.
220    global_config.tool_only_in_editor
221}
222
223/// Converts a default parameter value to a runtime-immutable `Variant`.
224///
225/// This function is used internally by the `#[opt(default)]` attribute to:
226/// 1. Convert the value using `AsArg` trait for argument conversions (e.g. `"str"` for `AsArg<GString>`).
227/// 2. Apply immutability transformation.
228/// 3. Convert to `Variant` for Godot's storage.
229pub fn opt_default_value<T>(value: impl crate::meta::AsArg<T>) -> crate::builtin::Variant
230where
231    T: crate::meta::GodotImmutable + crate::meta::ToGodot + Clone,
232{
233    // We currently need cow_into_owned() to create an owned value for the immutability transform. This may be revisited once `#[opt]`
234    // supports more types (e.g. `Gd<RefCounted>`, where `cow_into_owned()` would increment ref-counts).
235
236    let value = crate::meta::AsArg::<T>::into_arg(value);
237    let value = value.cow_into_owned();
238    let value = <T as crate::meta::GodotImmutable>::into_runtime_immutable(value);
239    crate::builtin::Variant::from(value)
240}
241
242// ----------------------------------------------------------------------------------------------------------------------------------------------
243// Panic *hook* management
244
245pub fn extract_panic_message(err: &(dyn Send + std::any::Any)) -> String {
246    if let Some(s) = err.downcast_ref::<&'static str>() {
247        s.to_string()
248    } else if let Some(s) = err.downcast_ref::<String>() {
249        s.clone()
250    } else {
251        format!("(panic of type ID {:?})", err.type_id())
252    }
253}
254
255pub fn format_panic_message(panic_info: &std::panic::PanicHookInfo) -> String {
256    let mut msg = extract_panic_message(panic_info.payload());
257
258    if let Some(context) = fetch_last_panic_context() {
259        msg = format!("{msg}\nin {context}"); // used to be "Context: {context}".
260    }
261
262    let prefix = if let Some(location) = panic_info.location() {
263        format!("panic {}:{}", location.file(), location.line())
264    } else {
265        "panic".to_string()
266    };
267
268    // If the message contains newlines, print all of the lines after a line break, and indent them.
269    let lbegin = "\n  ";
270    let indented = msg.replace('\n', lbegin);
271
272    if indented.len() != msg.len() {
273        format!("[{prefix}]{lbegin}{indented}")
274    } else {
275        format!("[{prefix}]  {msg}")
276    }
277}
278
279// Macro instead of function, to avoid 1 extra frame in backtrace.
280#[cfg(safeguards_strict)] #[cfg_attr(published_docs, doc(cfg(safeguards_strict)))]
281#[macro_export]
282macro_rules! format_backtrace {
283    ($prefix:expr_2021, $backtrace:expr_2021) => {{
284        use std::backtrace::BacktraceStatus;
285
286        let backtrace = $backtrace;
287
288        match backtrace.status() {
289            BacktraceStatus::Captured => format!("\n[{}]\n{}\n", $prefix, backtrace),
290            BacktraceStatus::Disabled => {
291                "(backtrace disabled, run application with `RUST_BACKTRACE=1` environment variable)"
292                    .to_string()
293            }
294            BacktraceStatus::Unsupported => {
295                "(backtrace unsupported for current platform)".to_string()
296            }
297            _ => "(backtrace status unknown)".to_string(),
298        }
299    }};
300
301    ($prefix:expr_2021) => {
302        $crate::format_backtrace!($prefix, std::backtrace::Backtrace::capture())
303    };
304}
305
306#[cfg(not(safeguards_strict))] #[cfg_attr(published_docs, doc(cfg(not(safeguards_strict))))]
307#[macro_export]
308macro_rules! format_backtrace {
309    ($prefix:expr $(, $backtrace:expr)? ) => {
310        String::new()
311    };
312}
313
314pub fn set_gdext_hook<F>(godot_print: F)
315where
316    F: Fn() -> bool + Send + Sync + 'static,
317{
318    std::panic::set_hook(Box::new(move |panic_info| {
319        // Flush, to make sure previous Rust output (e.g. test announcement, or debug prints during app) have been printed.
320        let _ignored_result = std::io::stdout().flush();
321
322        let message = format_panic_message(panic_info);
323        if godot_print() {
324            // Also prints to stdout/stderr -- do not print twice.
325            godot_error!("{message}");
326        } else {
327            eprintln!("{message}");
328        }
329
330        let backtrace = format_backtrace!("panic backtrace");
331        eprintln!("{backtrace}");
332        let _ignored_result = std::io::stderr().flush();
333    }));
334}
335
336pub fn set_error_print_level(level: u8) -> u8 {
337    assert!(level <= 2);
338    ERROR_PRINT_LEVEL.swap(level, atomic::Ordering::Relaxed)
339}
340
341pub(crate) fn has_error_print_level(level: u8) -> bool {
342    assert!(level <= 2);
343    ERROR_PRINT_LEVEL.load(atomic::Ordering::Relaxed) >= level
344}
345
346/// Internal type used to store context information for debug purposes. Debug context is stored on the thread-local
347/// ERROR_CONTEXT_STACK, which can later be used to retrieve the current context in the event of a panic. This value
348/// probably shouldn't be used directly; use ['get_gdext_panic_context()'](fetch_last_panic_context) instead.
349#[cfg(safeguards_strict)] #[cfg_attr(published_docs, doc(cfg(safeguards_strict)))]
350struct ScopedFunctionStack {
351    functions: Vec<*const dyn Fn() -> String>,
352}
353
354#[cfg(safeguards_strict)] #[cfg_attr(published_docs, doc(cfg(safeguards_strict)))]
355impl ScopedFunctionStack {
356    /// # Safety
357    /// Function must be removed (using [`pop_function()`](Self::pop_function)) before lifetime is invalidated.
358    unsafe fn push_function<'a, 'b>(&'a mut self, function: &'b (dyn Fn() -> String + 'b)) {
359        // SAFETY: Function has its lifetime `'b` extended to `'static` to satisfy the signature
360        // of `functions` which has an implied `'static` bound.
361        // Given function must be removed before lifetime `'b` is invalidated.
362        let function = unsafe {
363            std::mem::transmute::<
364                *const (dyn Fn() -> String + 'b),
365                *const (dyn Fn() -> String + 'static),
366            >(function)
367        };
368
369        self.functions.push(function);
370    }
371
372    fn pop_function(&mut self) {
373        self.functions.pop().expect("function stack is empty!");
374    }
375
376    fn get_last(&self) -> Option<String> {
377        self.functions.last().cloned().map(|pointer| {
378            // SAFETY:
379            // Invariants provided by push_function assert that any and all functions held by ScopedFunctionStack
380            // are removed before they are invalidated; functions must always be valid.
381            unsafe { (*pointer)() }
382        })
383    }
384}
385
386#[cfg(safeguards_strict)] #[cfg_attr(published_docs, doc(cfg(safeguards_strict)))]
387thread_local! {
388    static ERROR_CONTEXT_STACK: RefCell<ScopedFunctionStack> = const {
389        RefCell::new(ScopedFunctionStack { functions: Vec::new() })
390    }
391}
392
393// Value may return `None`, even from panic hook, if called from a non-Godot thread.
394pub fn fetch_last_panic_context() -> Option<String> {
395    #[cfg(safeguards_strict)] #[cfg_attr(published_docs, doc(cfg(safeguards_strict)))]
396    return ERROR_CONTEXT_STACK.with(|cell| cell.borrow().get_last());
397
398    #[cfg(not(safeguards_strict))] #[cfg_attr(published_docs, doc(cfg(not(safeguards_strict))))]
399    None
400}
401
402// ----------------------------------------------------------------------------------------------------------------------------------------------
403// Panic unwinding and catching
404
405pub struct PanicPayload {
406    payload: Box<dyn std::any::Any + Send + 'static>,
407}
408
409impl PanicPayload {
410    pub fn new(payload: Box<dyn std::any::Any + Send + 'static>) -> Self {
411        Self { payload }
412    }
413
414    // While this could be `&self`, it's usually good practice to pass panic payloads around linearly and have only 1 representation at a time.
415    pub fn into_panic_message(self) -> String {
416        extract_panic_message(self.payload.as_ref())
417    }
418
419    pub fn repanic(self) -> ! {
420        std::panic::resume_unwind(self.payload)
421    }
422}
423
424/// Executes `code`. If a panic is thrown, it is caught and an error message is printed to Godot.
425///
426/// Returns `Err(message)` if a panic occurred, and `Ok(result)` with the result of `code` otherwise.
427///
428/// In contrast to [`handle_fallible_varcall`] and [`handle_fallible_ptrcall`], this function is not intended for use in `try_` functions,
429/// where the error is propagated as a `CallError` in a global variable.
430pub fn handle_panic<E, F, R>(error_context: E, code: F) -> Result<R, PanicPayload>
431where
432    E: Fn() -> String,
433    F: FnOnce() -> R + std::panic::UnwindSafe,
434{
435    #[cfg(not(safeguards_strict))] #[cfg_attr(published_docs, doc(cfg(not(safeguards_strict))))]
436    let _ = error_context; // Unused in Release.
437
438    #[cfg(safeguards_strict)] #[cfg_attr(published_docs, doc(cfg(safeguards_strict)))]
439    ERROR_CONTEXT_STACK.with(|cell| unsafe {
440        // SAFETY: &error_context is valid for lifetime of function, and is removed from LAST_ERROR_CONTEXT before end of function.
441        cell.borrow_mut().push_function(&error_context)
442    });
443
444    let result = std::panic::catch_unwind(code).map_err(PanicPayload::new);
445
446    #[cfg(safeguards_strict)] #[cfg_attr(published_docs, doc(cfg(safeguards_strict)))]
447    ERROR_CONTEXT_STACK.with(|cell| cell.borrow_mut().pop_function());
448    result
449}
450
451// Error code set on the varcall output when a `#[func]` fails (panic, parameter conversion, or `Result<T, E>` returning `Err`).
452//
453// None of the existing GDExtension call errors is great for this scenario -- all lead to misleading messages in the Godot console.
454// A custom out-of-range value causes "Bug: Invalid call error code 1337." in Godot's output, which is at least clearly non-standard.
455// Note that INVALID_METHOD must not be used: it signals that the method doesn't exist, which GDScript may treat as a fatal static error.
456// An alternative would be GDEXTENSION_CALL_ERROR_INSTANCE_IS_NULL.
457//
458// The GDScript VM interprets any non-OK code as "call failed, abort calling function", which is what we want. The "Bug: ..." print is
459// unavoidable at the VM level (no GDExtension code maps to a clean message); the preceding godot-rust `CallError` print carries the
460// actual diagnostic information.
461const CALL_FAILED_STATUS: sys::GDExtensionCallErrorType = 1337;
462
463/// Invokes a function with the _varcall_ calling convention, handling both expected errors and user panics.
464pub fn handle_fallible_varcall<F, R>(
465    call_ctx: &CallContext,
466    out_err: &mut sys::GDExtensionCallError,
467    code: F,
468) where
469    F: FnOnce() -> CallResult<R> + std::panic::UnwindSafe,
470{
471    if handle_fallible_call(call_ctx, code) {
472        // Use CALL_FAILED_STATUS so the GDScript VM recognizes the failure and aborts the calling function.
473        // The Rust-side CallError has been stored in the thread-local, so that try_call() can retrieve it later.
474        *out_err = sys::GDExtensionCallError {
475            error: CALL_FAILED_STATUS,
476            argument: 0,
477            expected: 0,
478        };
479    };
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);
488}
489
490/// Common error handling for fallible calls, handling detectable errors and user panics.
491///
492/// Returns `true` if the call failed, `false` if it succeeded.
493///
494/// On failure, the [`CallError`] is stored in thread-local storage for later retrieval via [`call_error_take`].
495fn handle_fallible_call<F, R>(call_ctx: &CallContext, code: F) -> bool
496where
497    F: FnOnce() -> CallResult<R> + std::panic::UnwindSafe,
498{
499    let outcome: Result<CallResult<R>, PanicPayload> =
500        handle_panic(|| format!("{call_ctx}()"), code);
501
502    let call_error = match outcome {
503        // All good.
504        Ok(Ok(_result)) => return false,
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    //
515    // OUT_CALL_DEPTH > 0 means this failure is observed during a Rust-initiated out-call (e.g. `try_call`); the caller already sees
516    // the `CallError` via return value, so printing here would just be noise.
517    //
518    // Coverage gap: only `Signature::out_class_varcall` sets the guard. If a `#[func]` re-enters Rust via `out_utility_call`,
519    // `out_builtin_ptrcall`, or `out_script_virtual_call`, the redundant print returns. Extend the guard to those paths if reported.
520    //
521    // caused_by_panic() check to avoid printing (2) once the panic message (1) is already printed:
522    //
523    // (1)  ERROR: [panic hot-reload/rust/src/lib.rs:37]
524    //      some panic message
525    //      Context: MyClass::my_method
526    //       at: godot_core::private::set_gdext_hook::{{closure}} (/.../godot-core/src/private.rs:354)
527    //       GDScript backtrace (most recent call first):
528    //           [0] _ready (res://script.gd:9)
529    //     (backtrace disabled, run application with `RUST_BACKTRACE=1` environment variable)
530    //
531    // (2) ERROR: godot-rust function call failed: MyClass::my_method()
532    //        Reason: function panicked: some panic message
533    //     at: ...
534    if has_error_print_level(2)
535        && !call_error.caused_by_panic()
536        && OUT_CALL_DEPTH.with(|d| d.get() == 0)
537    {
538        godot_error!("{call_error}");
539    }
540
541    call_error_store(call_error);
542    true
543}
544
545// Currently unused; implemented due to temporary need and may come in handy.
546pub fn rebuild_gd(object_ref: &classes::Object) -> Gd<classes::Object> {
547    let ptr = object_ref.__object_ptr();
548
549    // SAFETY: ptr comes from valid internal API (and is non-null, so unwrap in from_obj_sys won't fail).
550    unsafe { Gd::from_obj_sys(ptr) }
551}
552
553// ----------------------------------------------------------------------------------------------------------------------------------------------
554
555#[cfg(test)] #[cfg_attr(published_docs, doc(cfg(test)))]
556mod tests {
557    use super::{CallError, PanicPayload, call_error_store, call_error_take};
558    use crate::meta::CallContext;
559    use crate::sys;
560
561    fn make(index: usize) -> CallError {
562        let method_name = format!("method_{index}");
563        let ctx = CallContext::func("Class", &method_name);
564        let payload = PanicPayload::new(Box::new("some panic reason".to_string()));
565
566        CallError::failed_by_user_panic(&ctx, payload)
567    }
568
569    #[test]
570    fn thread_local_store_and_take() {
571        // Initially empty.
572        assert!(call_error_take().is_none());
573
574        // Store, then take.
575        call_error_store(make(1));
576        let e = call_error_take().expect("must be present");
577        assert_eq!(e.method_name(), "method_1");
578
579        // Second take returns None.
580        assert!(call_error_take().is_none());
581    }
582
583    #[test]
584    fn thread_local_overwrite() {
585        // Storing twice overwrites the first.
586        call_error_store(make(1));
587        call_error_store(make(2));
588        let e = call_error_take().expect("must be present");
589        assert_eq!(e.method_name(), "method_2");
590
591        assert!(call_error_take().is_none());
592    }
593
594    /// Regression test: a stale TLS entry from an earlier `#[func]` failure must not be misattributed to a later, unrelated varcall failure.
595    /// `check_out_varcall` drains TLS unconditionally, so a Godot-side error (e.g. wrong arg count) after a stale store must *not* wrap the
596    /// stale error. "TLS" means thread-local storage.
597    #[test]
598    fn stale_tls_not_misattributed() {
599        use crate::meta::error::CallError;
600
601        // Simulate a previous #[func] failure that was never consumed (e.g. GDScript was the caller).
602        call_error_store(make(99));
603
604        // Simulate a subsequent varcall that succeeds -- TLS must be drained.
605        let call_ctx = CallContext::outbound("Object", "call");
606        let ok_err = sys::GDExtensionCallError {
607            error: sys::GDEXTENSION_CALL_OK,
608            argument: 0,
609            expected: 0,
610        };
611        let result =
612            CallError::check_out_varcall(&call_ctx, ok_err, &[] as &[crate::builtin::Variant], &[]);
613        assert!(result.is_ok(), "successful call must return Ok");
614
615        // TLS must now be empty.
616        assert!(
617            call_error_take().is_none(),
618            "TLS must be drained after check_out_varcall"
619        );
620    }
621
622    /// Verify that when a varcall fails with a Godot-side error and there is *no* stale TLS entry,
623    /// the error is decoded from the Godot error struct (no source wrapping).
624    #[test]
625    fn varcall_godot_error_without_tls() {
626        use std::error::Error as _;
627
628        use crate::meta::error::CallError;
629
630        // Ensure TLS is clean.
631        let _ = call_error_take();
632
633        let call_ctx = CallContext::outbound("Node", "rpc_config");
634        let godot_err = sys::GDExtensionCallError {
635            error: sys::GDEXTENSION_CALL_ERROR_TOO_FEW_ARGUMENTS,
636            argument: 2,
637            expected: 3,
638        };
639        let result = CallError::check_out_varcall(
640            &call_ctx,
641            godot_err,
642            &[] as &[crate::builtin::Variant],
643            &[],
644        );
645        let err = result.expect_err("must fail");
646
647        // Must be a direct Godot error, not a wrapped source error.
648        assert!(
649            err.source().is_none(),
650            "Godot-side error must not have a source (stale or otherwise)"
651        );
652    }
653}