Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Provide safe way for implementing IScriptExtension::instance_has #1013

Merged
merged 1 commit into from
Feb 1, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions godot-codegen/src/special_cases/codegen_special_cases.rs
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,9 @@ const SELECTED_CLASSES: &[&str] = &[
"SceneTreeTimer",
"Script",
"ScriptExtension",
"ScriptNameCasing",
"ScriptLanguage",
"ScriptLanguageExtension",
"Sprite2D",
"SpriteFrames",
"TextServer",
Expand Down
41 changes: 40 additions & 1 deletion godot-core/src/obj/script.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,19 @@ use godot_cell::panicking::{GdCell, MutGuard, RefGuard};
use godot_cell::blocking::{GdCell, MutGuard, RefGuard};

use crate::builtin::{GString, StringName, Variant, VariantType};
use crate::classes::{Script, ScriptLanguage};
use crate::classes::{Object, Script, ScriptLanguage};
use crate::meta::{MethodInfo, PropertyInfo};
use crate::obj::{Base, Gd, GodotClass};
use crate::sys;

#[cfg(before_api = "4.3")]
use self::bounded_ptr_list::BoundedPtrList;

#[cfg(since_api = "4.2")]
use crate::classes::IScriptExtension;
#[cfg(since_api = "4.2")]
use crate::obj::Inherits;

/// Implement custom scripts that can be attached to objects in Godot.
///
/// To use script instances, implement this trait for your own type.
Expand Down Expand Up @@ -337,6 +342,40 @@ pub unsafe fn create_script_instance<T: ScriptInstance>(
}
}

/// Checks if an instance of the script exists for a given object.
///
/// This function both checks if the passed script matches the one currently assigned to the passed object, as well as verifies that
/// there is an instance for the script.
///
/// Use this function to implement [`IScriptExtension::instance_has`](crate::classes::IScriptExtension::instance_has).
#[cfg(since_api = "4.2")]
pub fn script_instance_exists<O, S>(object: &Gd<O>, script: &Gd<S>) -> bool
where
O: Inherits<Object>,
S: Inherits<Script> + IScriptExtension + super::Bounds<Declarer = super::bounds::DeclUser>,
{
let object_script_variant = object.upcast_ref().get_script();

if object_script_variant.is_nil() {
return false;
}

let object_script: Gd<Script> = object_script_variant.to();

if object_script.upcast_ref::<Script>().__object_ptr() != script.upcast_ref().__object_ptr() {
return false;
}

let Some(language) = script.bind().get_language() else {
return false;
};

let get_instance_fn = sys::interface_fn!(object_get_script_instance);
let instance = unsafe { get_instance_fn(object.obj_sys(), language.obj_sys()) };
TitanNano marked this conversation as resolved.
Show resolved Hide resolved

!instance.is_null()
}
Comment on lines +351 to +377
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe in the future we can revisit some parts:

  • __object_ptr() access
  • safety annotation, see here
  • Variant::object_id(), once polyfill works, see here

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, let me know when you go the Variant::object_id() polyfill to work, and I can integrate these points.


/// Mutable/exclusive reference guard for a `T` where `T` implements [`ScriptInstance`].
///
/// This can be used to access the base object of a [`ScriptInstance`], which in turn can be used to make reentrant calls to engine APIs.
Expand Down
56 changes: 43 additions & 13 deletions itest/godot/ScriptInstanceTests.gd
Original file line number Diff line number Diff line change
Expand Up @@ -5,103 +5,133 @@

extends TestSuite

func create_script_instance() -> RefCounted:
var script = TestScript.new()
func create_script_instance() -> Array:
var language: TestScriptLanguage = TestScriptLanguage.new()
var script: TestScript = language.new_script()
var script_owner = RefCounted.new()

script_owner.script = script

return script_owner
return [script_owner, language]


func test_script_instance_get_property():
var object = create_script_instance()
var tuple := create_script_instance()
var object: RefCounted = tuple[0]
var language: TestScriptLanguage = tuple[1]

var value: int = object.script_property_a

assert_eq(value, 10)
language.free()


func test_script_instance_set_property():
var object = create_script_instance()
var tuple := create_script_instance()
var object: RefCounted = tuple[0]
var language: TestScriptLanguage = tuple[1]

assert_eq(object.script_property_b, false)

object.script_property_b = true

assert_eq(object.script_property_b, true)
language.free()


func test_script_instance_call():
var object = create_script_instance()
var tuple := create_script_instance()
var object: RefCounted = tuple[0]
var language: TestScriptLanguage = tuple[1]

var arg_a = "test string"
var arg_b = 5

var result = object.script_method_a(arg_a, arg_b)

assert_eq(result, "{0}{1}".format([arg_a, arg_b]))
language.free()


func test_script_instance_property_list():
var object = create_script_instance()
var tuple := create_script_instance()
var object: RefCounted = tuple[0]
var language: TestScriptLanguage = tuple[1]

var list = object.get_property_list()

assert_eq(list[-1]["name"], "script_property_a");
assert_eq(list[-1]["type"], Variant.Type.TYPE_INT)
language.free()


func test_script_instance_method_list():
var object = create_script_instance()
var tuple := create_script_instance()
var object: RefCounted = tuple[0]
var language: TestScriptLanguage = tuple[1]

var list = object.get_method_list()

assert_eq(list[-1]["name"], "script_method_a")
assert_eq(list[-1]["args"][0]["type"], Variant.Type.TYPE_STRING)
assert_eq(list[-1]["args"][1]["type"], Variant.Type.TYPE_INT)
language.free()


func test_script_instance_has_method():
var object = create_script_instance()
var tuple := create_script_instance()
var object: RefCounted = tuple[0]
var language: TestScriptLanguage = tuple[1]

assert(object.has_method("script_method_a"));
assert(!object.has_method("script_method_z"));
language.free()


func test_script_instance_to_string():
var object = create_script_instance()
var tuple := create_script_instance()
var object: RefCounted = tuple[0]
var language: TestScriptLanguage = tuple[1]

assert_eq(object.to_string(), "script instance to string")
language.free()


func test_script_instance_mut_call():
var object = create_script_instance()
var tuple := create_script_instance()
var object: RefCounted = tuple[0]
var language: TestScriptLanguage = tuple[1]
var before = object.script_property_b

var result = object.script_method_toggle_property_b()

assert(result)
assert_eq(object.script_property_b, !before)
language.free()


func test_script_instance_re_entering_call():
var object = create_script_instance()
var tuple := create_script_instance()
var object: RefCounted = tuple[0]
var language: TestScriptLanguage = tuple[1]
var before = object.script_property_b

var result = object.script_method_re_entering()

assert(result)
assert_eq(object.script_property_b, !before)
language.free()


func test_object_script_instance():
var object = Node.new()
var script = TestScript.new()
var language: TestScriptLanguage = TestScriptLanguage.new()
var script: TestScript = language.new_script()

object.script = script

var result = object.script_method_re_entering()

assert(result)
object.free()
language.free()
Loading
Loading