1use 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
20mod 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; 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
47static 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
60thread_local! {
72 static LAST_CALL_ERROR: Cell<Option<CallError>> = const { Cell::new(None) };
73
74 static OUT_CALL_DEPTH: Cell<u32> = const { Cell::new(0) };
79}
80
81fn call_error_store(err: CallError) {
83 LAST_CALL_ERROR.set(Some(err));
84}
85
86pub(crate) fn call_error_take() -> Option<CallError> {
90 LAST_CALL_ERROR.take()
91}
92
93pub(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
113pub 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")] pub(crate) fn find_inherent_impl(class_name: crate::meta::ClassId) -> Option<InherentImpl> {
132 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#[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
162pub 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
170pub 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
188pub const fn is_editor_plugin<T: crate::obj::Inherits<classes::EditorPlugin>>() {}
190
191#[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 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 && global_config.is_editor_or_init(is_editor)
206}
207
208#[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 let global_config = unsafe { sys::config() };
217
218 global_config.tool_only_in_editor
221}
222
223pub 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 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
242pub 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}"); }
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 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#[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 let _ignored_result = std::io::stdout().flush();
321
322 let message = format_panic_message(panic_info);
323 if godot_print() {
324 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#[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 unsafe fn push_function<'a, 'b>(&'a mut self, function: &'b (dyn Fn() -> String + 'b)) {
359 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 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
393pub 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
402pub 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 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
424pub 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; #[cfg(safeguards_strict)] #[cfg_attr(published_docs, doc(cfg(safeguards_strict)))]
439 ERROR_CONTEXT_STACK.with(|cell| unsafe {
440 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
451const CALL_FAILED_STATUS: sys::GDExtensionCallErrorType = 1337;
462
463pub 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 *out_err = sys::GDExtensionCallError {
475 error: CALL_FAILED_STATUS,
476 argument: 0,
477 expected: 0,
478 };
479 };
480}
481
482pub 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
490fn 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 Ok(Ok(_result)) => return false,
505
506 Ok(Err(err)) => err,
508
509 Err(panic_msg) => CallError::failed_by_user_panic(call_ctx, panic_msg),
511 };
512
513 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
545pub fn rebuild_gd(object_ref: &classes::Object) -> Gd<classes::Object> {
547 let ptr = object_ref.__object_ptr();
548
549 unsafe { Gd::from_obj_sys(ptr) }
551}
552
553#[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 assert!(call_error_take().is_none());
573
574 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 assert!(call_error_take().is_none());
581 }
582
583 #[test]
584 fn thread_local_overwrite() {
585 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 #[test]
598 fn stale_tls_not_misattributed() {
599 use crate::meta::error::CallError;
600
601 call_error_store(make(99));
603
604 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 assert!(
617 call_error_take().is_none(),
618 "TLS must be drained after check_out_varcall"
619 );
620 }
621
622 #[test]
625 fn varcall_godot_error_without_tls() {
626 use std::error::Error as _;
627
628 use crate::meta::error::CallError;
629
630 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 assert!(
649 err.source().is_none(),
650 "Godot-side error must not have a source (stale or otherwise)"
651 );
652 }
653}