use std::error::Error;
use std::fmt;
use godot_ffi::join_debug;
use crate::builtin::{Variant, VariantType};
use crate::meta::error::{ConvertError, ErasedConvertError};
use crate::meta::{CallContext, ToGodot};
use crate::private::PanicPayload;
use crate::sys;
pub(crate) type CallResult<R> = Result<R, CallError>;
pub struct CallError {
b: Box<InnerCallError>,
}
#[derive(Debug)]
struct InnerCallError {
class_name: String,
function_name: String,
call_expr: String,
reason: String,
source: Option<SourceError>,
caused_by_panic: bool,
}
impl CallError {
pub fn class_name(&self) -> Option<&str> {
if self.b.class_name.is_empty() {
None
} else {
Some(&self.b.class_name)
}
}
pub fn method_name(&self) -> &str {
&self.b.function_name
}
pub(crate) fn check_arg_count(
call_ctx: &CallContext,
arg_count: usize, default_value_count: usize, param_count: usize, ) -> Result<(), Self> {
if arg_count + default_value_count >= param_count && arg_count <= param_count {
return Ok(());
}
let call_error = Self::failed_param_count(call_ctx, arg_count, param_count);
Err(call_error)
}
pub(crate) fn check_out_varcall<T: ToGodot>(
call_ctx: &CallContext,
err: sys::GDExtensionCallError,
explicit_args: &[T],
varargs: &[Variant],
) -> Result<(), Self> {
let source_error = crate::private::call_error_take();
if err.error == sys::GDEXTENSION_CALL_OK {
return Ok(());
}
let explicit_args_str = join_args(explicit_args.iter().map(|arg| arg.to_variant()));
let vararg_str = if varargs.is_empty() {
String::new()
} else {
format!(", [va] {}", join_args(varargs.iter().cloned()))
};
let call_expr = format!("{call_ctx}({explicit_args_str}{vararg_str})");
if source_error.is_some() {
let mut call_error = Self::new(call_ctx, String::new(), None);
call_error.b.call_expr = call_expr;
call_error.b.source = source_error.map(SourceError::Call);
return Err(call_error);
}
let mut arg_types = Vec::with_capacity(explicit_args.len() + varargs.len());
arg_types.extend(explicit_args.iter().map(|arg| arg.to_variant().get_type()));
arg_types.extend(varargs.iter().map(Variant::get_type));
Err(Self::failed_varcall_inner(
call_ctx,
call_expr,
err,
&arg_types,
explicit_args.len(),
None,
))
}
pub(crate) fn failed_param_conversion<P>(
call_ctx: &CallContext,
param_index: usize,
convert_error: ConvertError,
) -> Self {
let param_ty = sys::short_type_name::<P>();
Self::new(
call_ctx,
format!("parameter #{param_index} ({param_ty}) conversion"),
Some(convert_error),
)
}
fn failed_param_conversion_engine(
call_ctx: &CallContext,
param_index: i32,
actual: VariantType,
expected: VariantType,
) -> Self {
let reason =
format!("parameter #{param_index} -- cannot convert from {actual:?} to {expected:?}");
Self::new(call_ctx, reason, None)
}
pub(crate) fn failed_return_conversion<R>(
call_ctx: &CallContext,
convert_error: ConvertError,
) -> Self {
let return_ty = std::any::type_name::<R>();
Self::new(
call_ctx,
format!("return value {return_ty} conversion"),
Some(convert_error),
)
}
fn failed_param_count(
call_ctx: &CallContext,
arg_count: usize,
param_count: usize,
) -> CallError {
let param_plural = plural(param_count);
let arg_plural = plural(arg_count);
Self::new(
call_ctx,
format!(
"function has {param_count} parameter{param_plural}, but received {arg_count} argument{arg_plural}"
),
None,
)
}
fn failed_varcall_inner(
call_ctx: &CallContext,
call_expr: String,
err: sys::GDExtensionCallError,
arg_types: &[VariantType],
vararg_offset: usize,
source: Option<SourceError>,
) -> Self {
sys::strict_assert_ne!(err.error, sys::GDEXTENSION_CALL_OK);
let sys::GDExtensionCallError {
error,
argument,
expected,
} = err;
let mut call_error = match error {
sys::GDEXTENSION_CALL_ERROR_INVALID_METHOD => {
Self::new(call_ctx, "method not found", None)
}
sys::GDEXTENSION_CALL_ERROR_INVALID_ARGUMENT => {
let from = arg_types[vararg_offset + argument as usize];
let to = VariantType::from_sys(expected as sys::GDExtensionVariantType);
let i = argument + 1;
Self::failed_param_conversion_engine(call_ctx, i, from, to)
}
sys::GDEXTENSION_CALL_ERROR_TOO_MANY_ARGUMENTS
| sys::GDEXTENSION_CALL_ERROR_TOO_FEW_ARGUMENTS => {
let arg_count = arg_types.len() - vararg_offset;
let param_count = expected as usize;
Self::failed_param_count(call_ctx, arg_count, param_count)
}
sys::GDEXTENSION_CALL_ERROR_INSTANCE_IS_NULL => {
Self::new(call_ctx, "instance is null", None)
}
sys::GDEXTENSION_CALL_ERROR_METHOD_NOT_CONST => {
Self::new(call_ctx, "method is not const", None)
}
_ => Self::new(
call_ctx,
format!("unknown reason (error code {error})"),
None,
),
};
call_error.b.source = source;
call_error.b.call_expr = call_expr;
call_error
}
#[doc(hidden)]
pub fn failed_by_user_panic(call_ctx: &CallContext, panic_payload: PanicPayload) -> Self {
let reason = format!("function panicked: {}", panic_payload.into_panic_message());
let mut err = Self::new(call_ctx, reason, None);
err.b.caused_by_panic = true;
err
}
pub(crate) fn caused_by_panic(&self) -> bool {
self.b.caused_by_panic
}
fn new(
call_ctx: &CallContext,
reason: impl Into<String>,
source: Option<ConvertError>,
) -> Self {
let inner = InnerCallError {
class_name: call_ctx.class_name.to_string(),
function_name: call_ctx.function_name.to_string(),
call_expr: format!("{call_ctx}()"),
reason: reason.into(),
source: source.map(|e| SourceError::Convert {
value: e.value().map_or_else(String::new, |v| format!("{v:?}")),
erased_error: e.into(),
}),
caused_by_panic: false, };
Self { b: Box::new(inner) }
}
pub fn message(&self, with_source: bool) -> String {
let InnerCallError {
call_expr,
reason,
source,
..
} = &*self.b;
let reason_str = if reason.is_empty() {
String::new()
} else {
format!("\n Reason: {reason}")
};
let source_str = match source {
Some(SourceError::Convert {
erased_error,
value,
}) if with_source => {
format!(
"\n Source: {erased_error}{}{value}",
if value.is_empty() { "" } else { ": " },
)
}
Some(SourceError::Call(e)) if with_source => {
let message = e.message(true);
format!("\n Source: {message}")
}
_ => String::new(),
};
format!("{call_expr}{reason_str}{source_str}")
}
}
impl fmt::Display for CallError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let message = self.message(true);
write!(f, "godot-rust function call failed: {message}")
}
}
impl fmt::Debug for CallError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{:?}", self.b)
}
}
impl Error for CallError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self.b.source.as_ref() {
Some(SourceError::Convert {
erased_error: e, ..
}) => deref_to::<ErasedConvertError>(e),
Some(SourceError::Call(e)) => deref_to::<CallError>(e),
None => None,
}
}
}
#[derive(Debug)]
enum SourceError {
Convert {
erased_error: ErasedConvertError,
value: String,
},
Call(CallError),
}
fn deref_to<T>(t: &T) -> Option<&(dyn Error + 'static)>
where
T: Error + 'static,
{
Some(t)
}
fn join_args(args: impl Iterator<Item = Variant>) -> String {
join_debug(args)
}
fn plural(count: usize) -> &'static str {
if count == 1 { "" } else { "s" }
}