Skip to content

ScriptInstance::call Panics When Using Callable #1031

@Pspritechologist

Description

@Pspritechologist

Obtaining and calling a Callable from SiMut::base_mut causes a panic if done during the call callback on a ScriptInstance.
Below are the files used to replicate this issue.

The actual code to replicate is extremely small, but the files are large due to the amount of functions in the Traits. I've pushed any actually relevant functions to the top of their impl blocks.

Needlessly large code files
// lib.rs
use godot::{classes::Engine, prelude::*};

mod script;
mod language;
mod instance;

pub struct Extension;

#[gdextension]
unsafe impl ExtensionLibrary for Extension {
	fn on_level_init(level: InitLevel) {
		if level == InitLevel::Scene {
			Engine::singleton().register_script_language(&language::ReplLanguage::new_alloc());
		}
	}
}
// language.rs
use crate::script::ReplScript;
use godot::{classes::{IScriptExtension, IScriptLanguageExtension}, prelude::*};

#[derive(GodotClass)]
#[class(tool, init, base = ScriptLanguageExtension)]
pub struct ReplLanguage;

#[godot_api]
impl IScriptLanguageExtension for ReplLanguage {
	fn create_script(&self) -> Option<Gd<godot::classes::Object>> {
		Some(Gd::from_init_fn(ReplScript::init).upcast())
	}

	fn get_name(&self) -> GString { "Repl".into() }
	fn get_type(&self) -> GString { "Repl".into() }
	fn get_extension(&self) -> GString { "repl".into() }
	fn get_reserved_words(&self) -> PackedStringArray { Default::default() }
	fn is_control_flow_keyword(&self, _keyword: GString) -> bool { false }
	fn get_comment_delimiters(&self) -> PackedStringArray { Default::default() }
	fn get_doc_comment_delimiters(&self) -> PackedStringArray { Default::default() }
	fn get_string_delimiters(&self) -> PackedStringArray { Default::default() }
	fn make_template(&self, _template: GString, _class_name: GString, _base_class_name: GString) -> Option<Gd<godot::classes::Script>> { None }
	fn get_built_in_templates(&self, _object: StringName) -> Array<Dictionary> { Default::default() }
	fn validate(&self, _script: GString, _path: GString, _validate_functions: bool, _validate_errors: bool, _validate_warnings: bool, _validate_safe_lines: bool) -> Dictionary { Default::default() }
	fn validate_path(&self, _path: GString) -> GString { Default::default() }
	fn is_using_templates(&mut self) -> bool { false }
	fn has_named_classes(&self) -> bool { false }
	fn supports_builtin_mode(&self) -> bool { false }
	fn supports_documentation(&self) -> bool { false }
	fn can_inherit_from_file(&self) -> bool { false }
	fn can_make_function(&self) -> bool { false }
	fn overrides_external_editor(&mut self) -> bool { false }
	fn find_function(&self, _function: GString, _code: GString) -> i32 { 1 }
	fn make_function(&self, _class_name: GString, _function_name: GString, _function_args: PackedStringArray) -> GString { Default::default() }
	fn open_in_external_editor(&mut self, _script: Option<Gd<godot::classes::Script>>, _line: i32, _column: i32) -> godot::global::Error { godot::global::Error::FAILED }
	fn preferred_file_name_casing(&self) -> godot::classes::script_language::ScriptNameCasing { godot::classes::script_language::ScriptNameCasing::SNAKE_CASE }
	fn complete_code(&self, _code: GString, _path: GString, _owner: Option<Gd<godot::classes::Object>>) -> Dictionary { Default::default() }
	fn lookup_code(&self, _code: GString, _symbol: GString, _path: GString, _owner: Option<Gd<godot::classes::Object>>) -> Dictionary { Default::default() }
	fn auto_indent_code(&self, code: GString, _from_line: i32, _to_line: i32) -> GString { code }
	fn add_global_constant(&mut self, _name: StringName, _value: Variant) { }
	fn add_named_global_constant(&mut self, _name: StringName, _value: Variant) { }
	fn remove_named_global_constant(&mut self, _name: StringName) { }
	fn reload_all_scripts(&mut self) { }
	fn reload_tool_script(&mut self, _script: Option<Gd<godot::classes::Script>>, _soft_reload: bool) { }
	fn get_recognized_extensions(&self) -> PackedStringArray { [self.get_extension()].into() }
	fn get_public_functions(&self) -> Array<Dictionary> { Default::default() }
	fn get_public_constants(&self) -> Dictionary { Default::default() }
	fn get_public_annotations(&self) -> Array<Dictionary> { Default::default() }
	fn handles_global_class_type(&self, type_: GString) -> bool { self.get_type() == type_ }
	fn get_global_class_name(&self, _path: GString) -> Dictionary { Default::default() }
	fn init_ext(&mut self) { }
	fn finish(&mut self) { }
	fn thread_enter(&mut self) { }
	fn thread_exit(&mut self) { }
	fn frame(&mut self) { }
	fn debug_get_error(&self) -> GString { Default::default() }
	fn debug_get_stack_level_count(&self) -> i32 { 0 }
	fn debug_get_stack_level_line(&self, _level: i32) -> i32 { 1 }
	fn debug_get_stack_level_function(&self, _level: i32) -> GString { Default::default() }
	fn debug_get_stack_level_source(&self, _level: i32) -> GString { Default::default() }
	fn debug_get_stack_level_locals(&mut self, _level: i32, _max_subitems: i32, _max_depth: i32) -> Dictionary { Default::default() }
	fn debug_get_stack_level_members(&mut self, _level: i32, _max_subitems: i32, _max_depth: i32) -> Dictionary { Default::default() }
	unsafe fn debug_get_stack_level_instance(&mut self, _level: i32) -> * mut std::ffi::c_void { std::ptr::null_mut() }
	fn debug_get_globals(&mut self, _max_subitems: i32, _max_depth: i32) -> Dictionary { Default::default() }
	fn debug_parse_stack_level_expression(&mut self, _level: i32, _expression: GString, _max_subitems: i32, _max_depth: i32) -> GString { Default::default() }
	fn debug_get_current_stack_info(&mut self) -> Array<Dictionary> { Default::default() }
	fn profiling_start(&mut self) { }
	fn profiling_stop(&mut self) { }
	fn profiling_set_save_native_calls(&mut self, _enable: bool) { }
	unsafe fn profiling_get_accumulated_data(&mut self, _info_array: * mut godot::classes::native::ScriptLanguageExtensionProfilingInfo, _info_max: i32) -> i32 { 0 }
	unsafe fn profiling_get_frame_data(&mut self, _info_array: * mut godot::classes::native::ScriptLanguageExtensionProfilingInfo, _info_max: i32) -> i32 { 0 }
}
// script.rs
use crate::{instance::ReplInstance, language::ReplLanguage};
use godot::{classes::{IScriptExtension, ScriptExtension}, obj::script::create_script_instance, prelude::*};

#[derive(GodotClass)]
#[class(tool, init, base = ScriptExtension)]
pub struct ReplScript {
	base: Base<ScriptExtension>,
}

#[godot_api]
impl IScriptExtension for ReplScript  {
	unsafe fn instance_create(&self, for_object: Gd<godot::classes::Object>) -> *mut std::ffi::c_void {
		create_script_instance(ReplInstance { script_ref: self.to_gd().upcast() }, for_object)
	}
	
	fn get_language(&self) -> Option<Gd<godot::classes::ScriptLanguage>> {
		Some(Gd::from_object(ReplLanguage).upcast())
	}
	
	unsafe fn placeholder_instance_create(&self, _for_object: Gd<godot::classes::Object>) -> *mut std::ffi::c_void { std::ptr::null_mut() }
	fn editor_can_reload_from_file(&mut self) -> bool { false }
	unsafe fn placeholder_erased(&mut self, _placeholder: *mut std::ffi::c_void) { }
	fn instance_has(&self, _object: Gd<godot::classes::Object>) -> bool { false }
	fn get_instance_base_type(&self) -> StringName { "Object".into() }
	fn has_source_code(&self) -> bool { false }
	fn get_source_code(&self) -> GString { Default::default() }
	fn set_source_code(&mut self, _code: GString) { Default::default() }
	fn reload(&mut self, _keep_state: bool) -> godot::global::Error { godot::global::Error::OK }
	fn can_instantiate(&self) -> bool { true }
	fn get_base_script(&self) -> Option<Gd<godot::classes::Script>> { None }
	fn get_global_name(&self) -> StringName { Default::default() }
	fn inherits_script(&self, _script: Gd<godot::classes::Script>) -> bool { false }
	fn get_documentation(&self) -> Array<Dictionary> { Default::default() }
	fn get_class_icon_path(&self) -> GString { Default::default() }
	fn get_script_method_argument_count(&self, _method: StringName) -> Variant { Variant::nil() }
	fn get_method_info(&self, _method: StringName) -> Dictionary { Default::default() }
	fn is_valid(&self) -> bool { true }
	fn is_tool(&self) -> bool { false }
	fn is_abstract(&self) -> bool { false }
	fn has_method(&self, _method: StringName) -> bool { false }
	fn has_static_method(&self, _method: StringName) -> bool { false }
	fn has_script_signal(&self, _signal: StringName) -> bool { false }
	fn get_script_signal_list(&self) -> Array<Dictionary> { Default::default() }
	fn has_property_default_value(&self, _property: StringName) -> bool { false }
	fn get_property_default_value(&self, _property: StringName) -> Variant { Variant::nil() }
	fn update_exports(&mut self) { }
	fn get_script_method_list(&self) -> Array<Dictionary> { Default::default() }
	fn get_script_property_list(&self) -> Array<Dictionary> { Default::default() }
	fn get_member_line(&self, _member: StringName) -> i32 { 1 }
	fn get_constants(&self) -> Dictionary { Default::default() }
	fn get_members(&self) -> Array<StringName> { Default::default() }
	fn is_placeholder_fallback_enabled(&self) -> bool { false }
	fn get_rpc_config(&self) -> Variant { Variant::nil() }
	fn setup_local_to_scene(&mut self) { }
}
// instance.rs
use crate::{language::ReplLanguage, script::ReplScript};
use godot::{classes::Script, global::MethodFlags, meta::{MethodInfo, PropertyInfo}, obj::script::{ScriptInstance, SiMut}, prelude::*};

pub struct ReplInstance {
	pub script_ref: Gd<Script>,
}

impl ScriptInstance for ReplInstance {
	type Base = Object;

	fn call(mut this: SiMut<Self>, method: StringName, args: &[&Variant]) -> Result<Variant, godot::sys::GDExtensionCallErrorType> {
		if !method.to_string().eq("do_name") {
			return Err(godot::sys::GDEXTENSION_CALL_ERROR_INVALID_METHOD);
		}

		let (name, safe): (_, bool) = (args[0].clone(), args[1].to());

		if safe {
			this.base_mut().call("set_name", &[name]);
		} else {
			let callable: Callable = this.base_mut().get("set_name").to();
			callable.call(&[name]);
		}

		Ok(Variant::nil())
	}

	fn get_method_list(&self) -> Vec<godot::meta::MethodInfo> {
		vec![
			MethodInfo {
				id: 0,
				method_name: "do_name".into(),
				class_name: ReplScript::class_name(),
				return_type: PropertyInfo::new_var::<Variant>("return"),
				arguments: Default::default(),
				default_arguments: Default::default(),
				flags: MethodFlags::NORMAL,
			}
		]
	}

	fn has_method(&self, method: StringName) -> bool { method.to_string() == "do_name" }

	fn get_script(&self) -> &Gd<godot::classes::Script> { &self.script_ref }
	fn on_refcount_decremented(&self) -> bool { false }
	fn on_refcount_incremented(&self) { }
	fn set_property(_this: godot::obj::script::SiMut<Self>, _name: StringName, _value: &Variant) -> bool { false }
	fn get_property(&self, _name: StringName) -> Option<Variant> { None }
	fn get_property_list(&self) -> Vec<godot::meta::PropertyInfo> { Default::default() }
	fn get_property_type(&self, _name: StringName) -> VariantType { VariantType::NIL }
	fn get_property_state(&self) -> Vec<(StringName, Variant)> { Default::default() }
	fn property_get_fallback(&self, _name: StringName) -> Option<Variant> { None }
	fn property_set_fallback(_this: godot::obj::script::SiMut<Self>, _name: StringName, _value: &Variant) -> bool { false }
	fn get_method_argument_count(&self, _method: StringName) -> Option<u32> { None }
	fn to_string(&self) -> GString { Default::default() }
	fn class_name(&self) -> GString { Default::default() }
	fn get_language(&self) -> Gd<godot::classes::ScriptLanguage> { Gd::from_object(ReplLanguage).upcast() }
	fn is_placeholder(&self) -> bool { false }
}
# main.gd (Attached to the root Node of a main.tscn)
extends Node

func _ready() -> void:
	var script = ReplScript.new()
	self.set_script(script)
	
	self.call("do_name", "Owo", true)
	
	self.call("do_name", "Awa", false)

Yields the following panic:

ERROR: Rust function panicked: [panic]
  ScriptInstance borrow failed, already bound; T = repl::instance::ReplInstance.
    Make sure to use `SiMut::base_mut()` when possible.
    Details: cannot borrow while accessible mutable borrow exists.
  at /path/checkouts/gdext-76630c89719e160c/6da1bbf/godot-core/src/obj/script.rs:211
  Context: error when calling repl::instance::ReplInstance::call
   at: godot_core::private::handle_panic_with_print (/path/checkouts/gdext-76630c89719e160c/6da1bbf/godot-core/src/private.rs:405)

Of note is that both of the above calls actually have the desired effect (presumably once the call fails due to panic Godot calls the base method as intended), the second one simply also yields a panic.
I assume this would not be the case for calls to class methods rather than base methods.

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions