use std::cell::Cell;
#[cfg(safeguards_strict)] #[cfg_attr(published_docs, doc(cfg(safeguards_strict)))]
use std::cell::RefCell;
use std::io::Write;
use std::sync::atomic;
use crate::global::godot_error;
use crate::meta::error::{CallError, CallResult};
use crate::obj::Gd;
use crate::registry::property::Var;
use crate::{classes, sys};
mod reexport_pub {
pub use crate::arg_into_owned;
#[cfg(all(since_api = "4.3", feature = "register-docs"))] #[cfg_attr(published_docs, doc(cfg(all(since_api = "4.3", feature = "register-docs"))))]
pub use crate::docs::{DocsItem, DocsPlugin, InherentImplDocs, StructDocs};
pub use crate::r#gen::classes::class_macros;
pub use crate::r#gen::virtuals; pub use crate::meta::private_reexport::*;
#[cfg(feature = "trace")] #[cfg_attr(published_docs, doc(cfg(feature = "trace")))]
pub use crate::meta::{CowArg, FfiArg, trace};
pub use crate::obj::rtti::ObjectRtti;
pub use crate::obj::signal::priv_re_export::*;
pub use crate::registry::callbacks;
pub use crate::registry::plugin::{
ClassPlugin, DynTraitImpl, ErasedDynGd, ErasedRegisterFn, ITraitImpl, InherentImpl,
PluginItem, Struct,
};
pub use crate::storage::{
IntoVirtualMethodReceiver, RecvGdSelf, RecvMut, RecvRef, Storage, VirtualMethodReceiver,
as_storage,
};
pub use crate::sys::out;
}
pub use reexport_pub::*;
static ERROR_PRINT_LEVEL: atomic::AtomicU8 = atomic::AtomicU8::new(2);
sys::plugin_registry!(pub __GODOT_PLUGIN_REGISTRY: ClassPlugin);
#[cfg(all(since_api = "4.3", feature = "register-docs"))] #[cfg_attr(published_docs, doc(cfg(all(since_api = "4.3", feature = "register-docs"))))]
sys::plugin_registry!(pub __GODOT_DOCS_REGISTRY: DocsPlugin);
thread_local! {
static LAST_CALL_ERROR: Cell<Option<CallError>> = const { Cell::new(None) };
}
fn call_error_store(err: CallError) {
LAST_CALL_ERROR.set(Some(err));
}
pub(crate) fn call_error_take() -> Option<CallError> {
LAST_CALL_ERROR.take()
}
pub fn next_class_id() -> u16 {
static NEXT_CLASS_ID: atomic::AtomicU16 = atomic::AtomicU16::new(0);
NEXT_CLASS_ID.fetch_add(1, atomic::Ordering::Relaxed)
}
pub(crate) fn iterate_plugins(mut visitor: impl FnMut(&ClassPlugin)) {
sys::plugin_foreach!(__GODOT_PLUGIN_REGISTRY; visitor);
}
#[cfg(all(since_api = "4.3", feature = "register-docs"))] #[cfg_attr(published_docs, doc(cfg(all(since_api = "4.3", feature = "register-docs"))))]
pub(crate) fn iterate_docs_plugins(mut visitor: impl FnMut(&DocsPlugin)) {
sys::plugin_foreach!(__GODOT_DOCS_REGISTRY; visitor);
}
#[cfg(feature = "codegen-full")] pub(crate) fn find_inherent_impl(class_name: crate::meta::ClassId) -> Option<InherentImpl> {
let plugins = __GODOT_PLUGIN_REGISTRY.lock().unwrap();
plugins.iter().find_map(|elem| {
if elem.class_name == class_name
&& let PluginItem::InherentImpl(inherent_impl) = &elem.item
{
return Some(inherent_impl.clone());
}
None
})
}
#[allow(non_camel_case_types)]
#[diagnostic::on_unimplemented(
message = "`impl` blocks for Godot classes require the `#[godot_api]` attribute",
label = "missing `#[godot_api]` before `impl`",
note = "see also: https://godot-rust.github.io/book/register/functions.html#godot-special-functions"
)]
pub trait You_forgot_the_attribute__godot_api {}
pub struct ClassConfig {
pub is_tool: bool,
}
pub fn typecheck_getter<C, T: Var>(_getter: impl Fn(&C) -> T::PubType) {}
pub fn typecheck_setter<C, T: Var>(_setter: fn(&mut C, T::PubType)) {}
pub fn auto_init<T>(l: &mut crate::obj::OnReady<T>, base: &Gd<classes::Node>) {
l.init_auto(base);
}
#[cfg(since_api = "4.3")] #[cfg_attr(published_docs, doc(cfg(since_api = "4.3")))]
pub unsafe fn has_virtual_script_method(
object_ptr: sys::GDExtensionObjectPtr,
method_sname: sys::GDExtensionConstStringNamePtr,
) -> bool {
unsafe {
sys::interface_fn!(object_has_script_method)(sys::to_const_ptr(object_ptr), method_sname)
!= 0
}
}
pub const fn is_editor_plugin<T: crate::obj::Inherits<classes::EditorPlugin>>() {}
#[cfg(before_api = "4.3")] #[cfg_attr(published_docs, doc(cfg(before_api = "4.3")))]
pub fn is_class_inactive(is_tool: bool) -> bool {
use crate::obj::Singleton;
if is_tool {
return false;
}
let global_config = unsafe { sys::config() };
let is_editor = || crate::classes::Engine::singleton().is_editor_hint();
global_config.tool_only_in_editor && global_config.is_editor_or_init(is_editor)
}
#[cfg(since_api = "4.3")] #[cfg_attr(published_docs, doc(cfg(since_api = "4.3")))]
pub fn is_class_runtime(is_tool: bool) -> bool {
if is_tool {
return false;
}
let global_config = unsafe { sys::config() };
global_config.tool_only_in_editor
}
pub fn opt_default_value<T>(value: impl crate::meta::AsArg<T>) -> crate::builtin::Variant
where
T: crate::meta::GodotImmutable + crate::meta::ToGodot + Clone,
{
let value = crate::meta::AsArg::<T>::into_arg(value);
let value = value.cow_into_owned();
let value = <T as crate::meta::GodotImmutable>::into_runtime_immutable(value);
crate::builtin::Variant::from(value)
}
pub fn extract_panic_message(err: &(dyn Send + std::any::Any)) -> String {
if let Some(s) = err.downcast_ref::<&'static str>() {
s.to_string()
} else if let Some(s) = err.downcast_ref::<String>() {
s.clone()
} else {
format!("(panic of type ID {:?})", err.type_id())
}
}
pub fn format_panic_message(panic_info: &std::panic::PanicHookInfo) -> String {
let mut msg = extract_panic_message(panic_info.payload());
if let Some(context) = fetch_last_panic_context() {
msg = format!("{msg}\nin {context}"); }
let prefix = if let Some(location) = panic_info.location() {
format!("panic {}:{}", location.file(), location.line())
} else {
"panic".to_string()
};
let lbegin = "\n ";
let indented = msg.replace('\n', lbegin);
if indented.len() != msg.len() {
format!("[{prefix}]{lbegin}{indented}")
} else {
format!("[{prefix}] {msg}")
}
}
#[cfg(safeguards_strict)] #[cfg_attr(published_docs, doc(cfg(safeguards_strict)))]
#[macro_export]
macro_rules! format_backtrace {
($prefix:expr_2021, $backtrace:expr_2021) => {{
use std::backtrace::BacktraceStatus;
let backtrace = $backtrace;
match backtrace.status() {
BacktraceStatus::Captured => format!("\n[{}]\n{}\n", $prefix, backtrace),
BacktraceStatus::Disabled => {
"(backtrace disabled, run application with `RUST_BACKTRACE=1` environment variable)"
.to_string()
}
BacktraceStatus::Unsupported => {
"(backtrace unsupported for current platform)".to_string()
}
_ => "(backtrace status unknown)".to_string(),
}
}};
($prefix:expr_2021) => {
$crate::format_backtrace!($prefix, std::backtrace::Backtrace::capture())
};
}
#[cfg(not(safeguards_strict))] #[cfg_attr(published_docs, doc(cfg(not(safeguards_strict))))]
#[macro_export]
macro_rules! format_backtrace {
($prefix:expr $(, $backtrace:expr)? ) => {
String::new()
};
}
pub fn set_gdext_hook<F>(godot_print: F)
where
F: Fn() -> bool + Send + Sync + 'static,
{
std::panic::set_hook(Box::new(move |panic_info| {
let _ignored_result = std::io::stdout().flush();
let message = format_panic_message(panic_info);
if godot_print() {
godot_error!("{message}");
} else {
eprintln!("{message}");
}
let backtrace = format_backtrace!("panic backtrace");
eprintln!("{backtrace}");
let _ignored_result = std::io::stderr().flush();
}));
}
pub fn set_error_print_level(level: u8) -> u8 {
assert!(level <= 2);
ERROR_PRINT_LEVEL.swap(level, atomic::Ordering::Relaxed)
}
pub(crate) fn has_error_print_level(level: u8) -> bool {
assert!(level <= 2);
ERROR_PRINT_LEVEL.load(atomic::Ordering::Relaxed) >= level
}
#[cfg(safeguards_strict)] #[cfg_attr(published_docs, doc(cfg(safeguards_strict)))]
struct ScopedFunctionStack {
functions: Vec<*const dyn Fn() -> String>,
}
#[cfg(safeguards_strict)] #[cfg_attr(published_docs, doc(cfg(safeguards_strict)))]
impl ScopedFunctionStack {
unsafe fn push_function<'a, 'b>(&'a mut self, function: &'b (dyn Fn() -> String + 'b)) {
let function = unsafe {
std::mem::transmute::<
*const (dyn Fn() -> String + 'b),
*const (dyn Fn() -> String + 'static),
>(function)
};
self.functions.push(function);
}
fn pop_function(&mut self) {
self.functions.pop().expect("function stack is empty!");
}
fn get_last(&self) -> Option<String> {
self.functions.last().cloned().map(|pointer| {
unsafe { (*pointer)() }
})
}
}
#[cfg(safeguards_strict)] #[cfg_attr(published_docs, doc(cfg(safeguards_strict)))]
thread_local! {
static ERROR_CONTEXT_STACK: RefCell<ScopedFunctionStack> = const {
RefCell::new(ScopedFunctionStack { functions: Vec::new() })
}
}
pub fn fetch_last_panic_context() -> Option<String> {
#[cfg(safeguards_strict)] #[cfg_attr(published_docs, doc(cfg(safeguards_strict)))]
return ERROR_CONTEXT_STACK.with(|cell| cell.borrow().get_last());
#[cfg(not(safeguards_strict))] #[cfg_attr(published_docs, doc(cfg(not(safeguards_strict))))]
None
}
pub struct PanicPayload {
payload: Box<dyn std::any::Any + Send + 'static>,
}
impl PanicPayload {
pub fn new(payload: Box<dyn std::any::Any + Send + 'static>) -> Self {
Self { payload }
}
pub fn into_panic_message(self) -> String {
extract_panic_message(self.payload.as_ref())
}
pub fn repanic(self) -> ! {
std::panic::resume_unwind(self.payload)
}
}
pub fn handle_panic<E, F, R>(error_context: E, code: F) -> Result<R, PanicPayload>
where
E: Fn() -> String,
F: FnOnce() -> R + std::panic::UnwindSafe,
{
#[cfg(not(safeguards_strict))] #[cfg_attr(published_docs, doc(cfg(not(safeguards_strict))))]
let _ = error_context;
#[cfg(safeguards_strict)] #[cfg_attr(published_docs, doc(cfg(safeguards_strict)))]
ERROR_CONTEXT_STACK.with(|cell| unsafe {
cell.borrow_mut().push_function(&error_context)
});
let result = std::panic::catch_unwind(code).map_err(PanicPayload::new);
#[cfg(safeguards_strict)] #[cfg_attr(published_docs, doc(cfg(safeguards_strict)))]
ERROR_CONTEXT_STACK.with(|cell| cell.borrow_mut().pop_function());
result
}
pub fn handle_fallible_varcall<F, R>(
call_ctx: &CallContext,
out_err: &mut sys::GDExtensionCallError,
code: F,
) where
F: FnOnce() -> CallResult<R> + std::panic::UnwindSafe,
{
if handle_fallible_call(call_ctx, code) {
*out_err = sys::GDExtensionCallError {
error: sys::GDEXTENSION_CALL_ERROR_INVALID_METHOD,
argument: 0,
expected: 0,
};
};
}
pub fn handle_fallible_ptrcall<F>(call_ctx: &CallContext, code: F)
where
F: FnOnce() -> CallResult<()> + std::panic::UnwindSafe,
{
handle_fallible_call(call_ctx, code);
}
fn handle_fallible_call<F, R>(call_ctx: &CallContext, code: F) -> bool
where
F: FnOnce() -> CallResult<R> + std::panic::UnwindSafe,
{
let outcome: Result<CallResult<R>, PanicPayload> =
handle_panic(|| format!("{call_ctx}()"), code);
let call_error = match outcome {
Ok(Ok(_result)) => return false,
Ok(Err(err)) => err,
Err(panic_msg) => CallError::failed_by_user_panic(call_ctx, panic_msg),
};
if has_error_print_level(2) && !call_error.caused_by_panic() {
godot_error!("{call_error}");
}
call_error_store(call_error);
true
}
pub fn rebuild_gd(object_ref: &classes::Object) -> Gd<classes::Object> {
let ptr = object_ref.__object_ptr();
unsafe { Gd::from_obj_sys(ptr) }
}
#[cfg(test)] #[cfg_attr(published_docs, doc(cfg(test)))]
mod tests {
use super::{CallError, PanicPayload, call_error_store, call_error_take};
use crate::meta::CallContext;
use crate::sys;
fn make(index: usize) -> CallError {
let method_name = format!("method_{index}");
let ctx = CallContext::func("Class", &method_name);
let payload = PanicPayload::new(Box::new("some panic reason".to_string()));
CallError::failed_by_user_panic(&ctx, payload)
}
#[test]
fn thread_local_store_and_take() {
assert!(call_error_take().is_none());
call_error_store(make(1));
let e = call_error_take().expect("must be present");
assert_eq!(e.method_name(), "method_1");
assert!(call_error_take().is_none());
}
#[test]
fn thread_local_overwrite() {
call_error_store(make(1));
call_error_store(make(2));
let e = call_error_take().expect("must be present");
assert_eq!(e.method_name(), "method_2");
assert!(call_error_take().is_none());
}
#[test]
fn stale_tls_not_misattributed() {
use crate::meta::error::CallError;
call_error_store(make(99));
let call_ctx = CallContext::outbound("Object", "call");
let ok_err = sys::GDExtensionCallError {
error: sys::GDEXTENSION_CALL_OK,
argument: 0,
expected: 0,
};
let result =
CallError::check_out_varcall(&call_ctx, ok_err, &[] as &[crate::builtin::Variant], &[]);
assert!(result.is_ok(), "successful call must return Ok");
assert!(
call_error_take().is_none(),
"TLS must be drained after check_out_varcall"
);
}
#[test]
fn varcall_godot_error_without_tls() {
use std::error::Error as _;
use crate::meta::error::CallError;
let _ = call_error_take();
let call_ctx = CallContext::outbound("Node", "rpc_config");
let godot_err = sys::GDExtensionCallError {
error: sys::GDEXTENSION_CALL_ERROR_TOO_FEW_ARGUMENTS,
argument: 2,
expected: 3,
};
let result = CallError::check_out_varcall(
&call_ctx,
godot_err,
&[] as &[crate::builtin::Variant],
&[],
);
let err = result.expect_err("must fail");
assert!(
err.source().is_none(),
"Godot-side error must not have a source (stale or otherwise)"
);
}
}