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

Class without init cannot be returned from Rust to GDScript #194

Closed
ttencate opened this issue Mar 21, 2023 · 4 comments
Closed

Class without init cannot be returned from Rust to GDScript #194

ttencate opened this issue Mar 21, 2023 · 4 comments
Labels
bug c: register Register classes, functions and other symbols to GDScript

Comments

@ttencate
Copy link
Contributor

ttencate commented Mar 21, 2023

#[derive(GodotClass)]
#[class]
pub struct World {
}

#[derive(GodotClass)]
#[class(init)]
pub struct WorldGen {
}

#[godot_api]
impl WorldGen {
    #[func]
    fn generate(&mut self) -> Gd<World> {
        Gd::new(World {})
    }
}

When calling WorldGen.new().generate() from GDScript, Godot prints an error:

ERROR: Class 'World' has no native extension.
   at: set_object_extension_instance (core/object/class_db.cpp:356)

If I put #[class(init)] on World instead of bare #[class], it starts working. Same if I manually implement init on GodotExt.

I did expect World not to be instantiable from GDScript or addable in the editor, but when instantiated from Rust it should still be possible tot pass it back to GDScript.

@Bromeon Bromeon added bug c: register Register classes, functions and other symbols to GDScript labels Mar 21, 2023
@ttencate
Copy link
Contributor Author

Notice that the editor calls init sometimes, even if you're not creating any instances yourself.

#[godot_api]
impl GodotExt for World {
    func init(_base: Base<RefCounted>) {
        unimplemented!("should never be called");
    }
}

It seems to have something to do with generating documentation:

...
#14 godot_core::registry::callbacks::create<my_crate::World> (_class_userdata=<optimized out>)
    at .../godot-core/src/registry.rs:282
#15 0x0000555559d9f6da in ClassDB::class_get_default_property_value (p_class=..., p_property=..., 
    r_valid=0x7fffffffd4e0) at core/object/class_db.cpp:1464
#16 0x0000555557190e66 in get_documentation_default_value (p_class_name=..., p_property_name=..., 
    r_default_value_valid=@0x7fffffffd4e0: false) at editor/doc_tools.cpp:339
#17 0x00005555571dffe4 in DocTools::generate (this=0x55555f96ef80, p_basic_types=true)
    at editor/doc_tools.cpp:453
#18 0x0000555557331bdd in EditorHelp::generate_doc () at editor/editor_help.cpp:2190
#19 EditorNode::EditorNode (this=<optimized out>, this=<optimized out>) at editor/editor_node.cpp:6625
#20 0x0000555556470a01 in Main::start () at main/main.cpp:2795
#21 0x00005555563de303 in main (argc=<optimized out>, argv=0x7fffffffe508)
    at platform/linuxbsd/godot_linuxbsd.cpp:71

@lilizoey
Copy link
Member

lilizoey commented May 1, 2023

Checking the godot source-code, we need to set the create_instance_func in GDExtensionClassCreationInfo to be a null-pointer for godot to understand that a class cannot be instantiated:

bool ClassDB::can_instantiate(const StringName &p_class) {
	OBJTYPE_RLOCK;

	ClassInfo *ti = classes.getptr(p_class);
	ERR_FAIL_COND_V_MSG(!ti, false, "Cannot get class '" + String(p_class) + "'.");
#ifdef TOOLS_ENABLED
	if (ti->api == API_EDITOR && !Engine::get_singleton()->is_editor_hint()) {
		return false;
	}
#endif
	return (!ti->disabled && ti->creation_func != nullptr && !(ti->gdextension && !ti->gdextension->create_instance));
}

Alternatively we can set a class to be virtual.

When generating documentation godot will try to instantiate a new object of that class. This is so that godot can call get_property_list on that object to generate documentation. If we make the class either not instantiable or virtual, godot will not try to instantiate a new object of that class and will instead attempt to instantiate a direct inheriter of that class. failing this i believe godot just fails to generate docs for that class's properties.

@lilizoey
Copy link
Member

lilizoey commented May 1, 2023

godotengine/godot#58972 see also here for a guide on what abstract vs virtual means and such. but the basic point is that we do need to properly set our class as not being instantiable.

@Bromeon
Copy link
Member

Bromeon commented Aug 3, 2024

I just tested the original snippet. Nowadays, the API expects explicit init or no_init, and the error message is quite detailed if forgotten:

error[E0277]: Class `World` requires either an `init` constructor, or explicit opt-out
  --> itest/rust/src/register_tests/var_test.rs:27:12
   |
27 | pub struct World {
   |            ^^^^^ needs `init`
   |
   = help: the trait `GodotDefault` is not implemented for `World`
   = note: To provide a default constructor, use `#[class(init)]` or implement an `init` method
   = note: To opt out, use `#[class(no_init)]`
   = note: see also: https://godot-rust.github.io/book/register/constructors.html

So, after adding no_init, we have this updated code:

#[derive(GodotClass)]
#[class(no_init)]
pub struct World {}

#[derive(GodotClass)]
#[class(init)]
pub struct WorldGen {}

#[godot_api]
impl WorldGen {
    #[func]
    fn generate(&mut self) -> Gd<World> {
        Gd::from_object(World {})
    }
}

Now, calling from GDScript:

func test_world_gen():
    var w = WorldGen.new().generate()
    print(w)

This works as expected, prints <RefCounted#-9223371997294098691>.
The issue should thus be solved 🍺

@Bromeon Bromeon closed this as completed Aug 3, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug c: register Register classes, functions and other symbols to GDScript
Projects
None yet
Development

No branches or pull requests

3 participants