#[cfg(debug_assertions)] #[cfg_attr(published_docs, doc(cfg(debug_assertions)))]
use std::cell::RefCell;
use std::io::Write;
use std::sync::atomic;
use sys::Global;
use crate::global::godot_error;
use crate::meta::error::CallError;
use crate::meta::CallContext;
use crate::obj::Gd;
use crate::{classes, sys};
mod reexport_pub {
#[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::gen::classes::class_macros;
pub use crate::gen::virtuals; #[cfg(feature = "trace")] #[cfg_attr(published_docs, doc(cfg(feature = "trace")))]
pub use crate::meta::trace;
pub use crate::obj::rtti::ObjectRtti;
pub use crate::registry::callbacks;
pub use crate::registry::plugin::{
ClassPlugin, DynTraitImpl, ErasedDynGd, ErasedRegisterFn, ITraitImpl, InherentImpl,
PluginItem, Struct,
};
pub use crate::registry::signal::priv_re_export::*;
pub use crate::storage::{
as_storage, IntoVirtualMethodReceiver, RecvGdSelf, RecvMut, RecvRef, Storage,
VirtualMethodReceiver,
};
pub use crate::sys::out;
}
pub use reexport_pub::*;
static CALL_ERRORS: Global<CallErrors> = Global::default();
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);
struct CallErrors {
ring_buffer: Vec<Option<CallError>>,
next_id: u8,
generation: u16,
}
impl Default for CallErrors {
fn default() -> Self {
Self {
ring_buffer: [const { None }; Self::MAX_ENTRIES as usize].into(),
next_id: 0,
generation: 0,
}
}
}
impl CallErrors {
const MAX_ENTRIES: u8 = 32;
fn insert(&mut self, err: CallError) -> i32 {
let id = self.next_id;
self.next_id = self.next_id.wrapping_add(1) % Self::MAX_ENTRIES;
if self.next_id == 0 {
self.generation = self.generation.wrapping_add(1);
}
self.ring_buffer[id as usize] = Some(err);
(self.generation as i32) << 16 | id as i32
}
fn remove(&mut self, id: i32) -> Option<CallError> {
let generation = (id >> 16) as u16;
let id = id as u8;
if id < self.next_id {
if generation != self.generation {
return None;
}
} else if generation != self.generation.wrapping_sub(1) {
return None;
}
self.ring_buffer[id as usize].take()
}
}
fn call_error_insert(err: CallError) -> i32 {
let id = CALL_ERRORS.lock().insert(err);
id
}
pub(crate) fn call_error_remove(in_error: &sys::GDExtensionCallError) -> Option<CallError> {
if in_error.error != sys::GODOT_RUST_CUSTOM_CALL_ERROR {
godot_error!("Tried to remove non-godot-rust call error {in_error:?}");
return None;
}
let call_error = CALL_ERRORS.lock().remove(in_error.argument);
if call_error.is_none() {
godot_error!("Failed to remove call error {in_error:?}");
}
call_error
}
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 {
if 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 auto_init<T>(l: &mut crate::obj::OnReady<T>, base: &crate::obj::Gd<crate::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 {
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<crate::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 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) = get_gdext_panic_context() {
msg = format!("{msg}\nContext: {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(debug_assertions)] #[cfg_attr(published_docs, doc(cfg(debug_assertions)))]
#[macro_export]
macro_rules! format_backtrace {
($prefix:expr, $backtrace:expr) => {{
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) => {
$crate::format_backtrace!($prefix, std::backtrace::Backtrace::capture())
};
}
#[cfg(not(debug_assertions))] #[cfg_attr(published_docs, doc(cfg(not(debug_assertions))))]
#[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(debug_assertions)] #[cfg_attr(published_docs, doc(cfg(debug_assertions)))]
struct ScopedFunctionStack {
functions: Vec<*const dyn Fn() -> String>,
}
#[cfg(debug_assertions)] #[cfg_attr(published_docs, doc(cfg(debug_assertions)))]
impl ScopedFunctionStack {
unsafe fn push_function(&mut self, function: &dyn Fn() -> String) {
let function = std::ptr::from_ref(function);
#[allow(clippy::unnecessary_cast)]
let function = function as *const (dyn Fn() -> String + 'static);
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(debug_assertions)] #[cfg_attr(published_docs, doc(cfg(debug_assertions)))]
thread_local! {
static ERROR_CONTEXT_STACK: RefCell<ScopedFunctionStack> = const {
RefCell::new(ScopedFunctionStack { functions: Vec::new() })
}
}
pub fn get_gdext_panic_context() -> Option<String> {
#[cfg(debug_assertions)] #[cfg_attr(published_docs, doc(cfg(debug_assertions)))]
return ERROR_CONTEXT_STACK.with(|cell| cell.borrow().get_last());
#[cfg(not(debug_assertions))] #[cfg_attr(published_docs, doc(cfg(not(debug_assertions))))]
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(debug_assertions))] #[cfg_attr(published_docs, doc(cfg(not(debug_assertions))))]
let _ = error_context;
#[cfg(debug_assertions)] #[cfg_attr(published_docs, doc(cfg(debug_assertions)))]
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(debug_assertions)] #[cfg_attr(published_docs, doc(cfg(debug_assertions)))]
ERROR_CONTEXT_STACK.with(|cell| cell.borrow_mut().pop_function());
result
}
pub fn handle_varcall_panic<F, R>(
call_ctx: &CallContext,
out_err: &mut sys::GDExtensionCallError,
code: F,
) where
F: FnOnce() -> Result<R, CallError> + std::panic::UnwindSafe,
{
let outcome: Result<Result<R, CallError>, PanicPayload> =
handle_panic(|| call_ctx.to_string(), code);
let call_error = match outcome {
Ok(Ok(_result)) => return,
Ok(Err(err)) => err,
Err(panic_msg) => CallError::failed_by_user_panic(call_ctx, panic_msg),
};
let error_id = report_call_error(call_error, true);
*out_err = sys::GDExtensionCallError {
error: sys::GODOT_RUST_CUSTOM_CALL_ERROR,
argument: error_id,
expected: 0,
};
}
pub fn handle_ptrcall_panic<F, R>(call_ctx: &CallContext, code: F)
where
F: FnOnce() -> R + std::panic::UnwindSafe,
{
let outcome: Result<R, PanicPayload> = handle_panic(|| call_ctx.to_string(), code);
let call_error = match outcome {
Ok(_result) => return,
Err(payload) => CallError::failed_by_user_panic(call_ctx, payload),
};
let _id = report_call_error(call_error, false);
}
fn report_call_error(call_error: CallError, track_globally: bool) -> i32 {
if has_error_print_level(2) {
godot_error!("{call_error}");
}
if track_globally {
call_error_insert(call_error)
} else {
0
}
}
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, CallErrors, PanicPayload};
use crate::meta::CallContext;
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 test_call_errors() {
let mut store = CallErrors::default();
let mut id07 = 0;
let mut id13 = 0;
let mut id20 = 0;
for i in 0..24 {
let id = store.insert(make(i));
match i {
7 => id07 = id,
13 => id13 = id,
20 => id20 = id,
_ => {}
}
}
let e = store.remove(id20).expect("must be present");
assert_eq!(e.method_name(), "method_20");
let e = store.remove(id20);
assert!(e.is_none());
for i in 24..CallErrors::MAX_ENTRIES as usize {
store.insert(make(i));
}
for i in 0..10 {
store.insert(make(i));
}
let e = store.remove(id07);
assert!(e.is_none(), "generation overwritten");
let e = store.remove(id13).expect("generation not yet overwritten");
assert_eq!(e.method_name(), "method_13");
}
}