Skip to content

Conversation

@Paperback
Copy link

@Paperback Paperback commented Nov 10, 2025

Thanks for the awesome work everyone puts into this project. This is my first rust contribution, so I'm keen to get some feedback and learn.

This pull request closes issue #1200.

It adds a new icon attribute to the #[class] macro.

#[derive(GodotClass)]
#[class(base = Sprite2D, icon = "res://addons/my_extension/player.svg")]
pub struct PlayerOne {}

// default icon behaviour
#[derive(GodotClass)]
#[class(base = Sprite2D)]
pub struct PlayerTwo {}

Because the icon attribute is a TokenStream, we can also write:

const MY_ICON: &str = "res://icon.svg"
#[derive(GodotClass)]
#[class(icon = MY_ICON)]
pub struct Player {}

Considerations

  • Requires api 4.4 >= as it uses GDExtensionClassCreationInfo4::icon_path
  • The .gdextension file [icon] section declarations seem to take precedence

Thanks!

@GodotRust
Copy link

API docs are being generated and will be shortly available at: https://godot-rust.github.io/docs/gdext/pr-1407

@Yarwin Yarwin added feature Adds functionality to the library c: register Register classes, functions and other symbols to GDScript labels Nov 11, 2025
Copy link
Contributor

@Yarwin Yarwin left a comment

Choose a reason for hiding this comment

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

First of all – thanks!!!!


  1. ICON_STRINGS_BY_NAME which keeps GStrings is not being pruned during deinit of the library, which means memory leak during, for example, hot reload.
    Please check it and report if I'm wrong. See also cleanup
    Storing GStrings to keep them valid as long as library is loaded is fine, just don't leak them.

  2. This feature is not tested at all – I think I would create new file in itest/rust/src/register_tests and annotate it with #![cfg(since_api = "4.4")] (similarly to register_docs.rs). Tests allows us to watch for memory leaks, shield us against regression etc – additionally we need to guarantee that all the used APIs are OKay and give an example how they can be used.

  3. I would remove ExtensionConfig (up to discussion) – right now its only purpose is to set default fallback icon for classes, which will be useless and counter-productive after 4.6: godotengine/godot#108607. I'm also finding it a little confusing (like, docs mention tools and build scripts – how would I even use it there?)

  4. #[class(icon = MY_ICON)] is not documented in #[derive(GodotClass)] docs.

  5. Address various silly things, like SAFETY remarks under now-safe blocks, comments not ending with a dot etc.

@Yarwin Yarwin linked an issue Nov 11, 2025 that may be closed by this pull request
@Paperback
Copy link
Author

This is great feedback, I'll make some changes tomorrow and get back to you.

@Paperback
Copy link
Author

I have removed the entire gdextension icon section; I can always create another PR with that in mind once we have a better idea.

Just poking around LoadedClass and ClassMetadata, maybe instead of the specific icon global guard, I should use that instead? For cleanup and integration tests, I want to be able to access that lock so I've made it pub(crate) for now, but I resent that.

At the moment the integration tests I've added are just checking is_instance_valid() but ideally I should check if icons are set in that global guard I think.

Thanks!

@Paperback Paperback changed the title Register custom icons in class and gdextension macros Register custom icons in class macro Nov 13, 2025
@Bromeon
Copy link
Member

Bromeon commented Nov 13, 2025

I have removed the entire gdextension icon section; I can always create another PR with that in mind once we have a better idea.

On that note, I believe it would be better to add a virtual method returning a string, rather than do this via proc-macro. While it's inconsistent with the #[class(icon)] at first, it has some advantages:

  • It allows users to provide values that aren't available at compile time (e.g. load from a file).
  • If we add builder APIs (#4), users will be able to provide runtime strings for method calls, too.
  • There's IDE and type system support (minor point since we already have proc-macros everywhere).

Copy link
Contributor

@Yarwin Yarwin left a comment

Choose a reason for hiding this comment

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

looks pretty much OK, I'll give it a proper run and see if there are any weird issues/quirks – I think we should move icon GString to LoadedClass, but it shouldn't change too much in the implementation/behavior itself.

@Yarwin
Copy link
Contributor

Yarwin commented Nov 14, 2025

Just poking around LoadedClass and ClassMetadata, maybe instead of the specific icon global guard, I should use that instead? For cleanup and integration tests, I want to be able to access that lock so I've made it pub(crate) for now, but I resent that.

I think using LoadedClass would be the right call! We wouldn't have to worry about cleanup and whatnot, since it would be handled auto-magically while dropping the loaded class itself (unregister_classes does it automatically after removing Vec of loaded classes).

At the moment the integration tests I've added are just checking is_instance_valid() but ideally I should check if icons are set in that global guard I think.

IMO checking if Godot is able to register given class should be enough – ideally we should be able to check if icon has been properly set on Godot's side, but (currently) we don't have any API to access that.

@Paperback Paperback force-pushed the class_icons branch 2 times, most recently from b7bc46a to 08ccba9 Compare November 17, 2025 21:20
@Paperback
Copy link
Author

Ok, thank you both for the feedback, I've reworked this a little bit:

  1. Added ExtensionLibrary::default_icon() -> &'static str. Can be changed to GString.
  2. Changed it so icon string is on LoadedClass instead of a global lock.
  3. Added init::DEFAULT_ICON global, resets to empty on deinitialization.
  4. Simplified icon itest.

Should I add default_icon() to the hot-reload test? That feature is not included in tests at the moment.

Thanks!

@Yarwin
Copy link
Contributor

Yarwin commented Nov 18, 2025

I'm getting instant segfault while trying to run itest via ./chesk.sh:

Backtrace
Using environment variable GODOT4_BIN=/home/irwin/apps/godot/godot/G4/Godot_v4.5-stable_linux.x86_64
> cargo build -p itest --no-default-features
    Blocking waiting for file lock on build directory
   Compiling godot-codegen v0.4.2 (/home/irwin/apps/godot/opensource-contr/missing_docs/gdext/godot-codegen)
   Compiling godot-macros v0.4.2 (/home/irwin/apps/godot/opensource-contr/missing_docs/gdext/godot-macros)
   Compiling godot-ffi v0.4.2 (/home/irwin/apps/godot/opensource-contr/missing_docs/gdext/godot-ffi)
   Compiling godot-core v0.4.2 (/home/irwin/apps/godot/opensource-contr/missing_docs/gdext/godot-core)
   Compiling itest v0.0.0 (/home/irwin/apps/godot/opensource-contr/missing_docs/gdext/itest/rust)
   Compiling godot v0.4.2 (/home/irwin/apps/godot/opensource-contr/missing_docs/gdext/godot)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 1m 04s
> /home/irwin/apps/godot/godot/G4/Godot_v4.5-stable_linux.x86_64 --path itest/godot --headless -- \[\]
Initialize godot-rust (API v4.5.stable.official, runtime v4.5.stable.official, safeguards strict)

================================================================
handle_crash: Program crashed with signal 11
Engine version: Godot Engine v4.5.stable.official (876b290332ec6f2e6d173d08162a02aa7e6ca46d)
Dumping the backtrace. Please include this when reporting the bug on: https://github.com/godotengine/godot/issues
[1] /lib/x86_64-linux-gnu/libc.so.6(+0x45330) [0x7695a7a45330] (??:0)
[2] /home/irwin/apps/godot/godot/G4/Godot_v4.5-stable_linux.x86_64() [0x4a182e] (??:0)
[3] /home/irwin/apps/godot/godot/G4/Godot_v4.5-stable_linux.x86_64() [0x4794733] (??:0)
[4] /home/irwin/apps/godot/godot/G4/Godot_v4.5-stable_linux.x86_64() [0x4795c75] (??:0)
[5] /home/irwin/apps/godot/opensource-contr/missing_docs/gdext/itest/godot/../../target/debug/libitest.so(+0x1c15443) [0x76956fc15443] (??:0)
[6] /home/irwin/apps/godot/opensource-contr/missing_docs/gdext/itest/godot/../../target/debug/libitest.so(+0x1c164b1) [0x76956fc164b1] (??:0)
[7] /home/irwin/apps/godot/opensource-contr/missing_docs/gdext/itest/godot/../../target/debug/libitest.so(+0x1bdcfe2) [0x76956fbdcfe2] (??:0)
[8] /home/irwin/apps/godot/opensource-contr/missing_docs/gdext/itest/godot/../../target/debug/libitest.so(+0x13f5f63) [0x76956f3f5f63] (??:0)
[9] /home/irwin/apps/godot/opensource-contr/missing_docs/gdext/itest/godot/../../target/debug/libitest.so(+0x13f5e66) [0x76956f3f5e66] (??:0)
[10] /home/irwin/apps/godot/opensource-contr/missing_docs/gdext/itest/godot/../../target/debug/libitest.so(+0x19a0e2c) [0x76956f9a0e2c] (??:0)
[11] /home/irwin/apps/godot/opensource-contr/missing_docs/gdext/itest/godot/../../target/debug/libitest.so(+0x17f1f1b) [0x76956f7f1f1b] (??:0)
[12] /home/irwin/apps/godot/opensource-contr/missing_docs/gdext/itest/godot/../../target/debug/libitest.so(+0x17c706a) [0x76956f7c706a] (??:0)
[13] /home/irwin/apps/godot/opensource-contr/missing_docs/gdext/itest/godot/../../target/debug/libitest.so(+0x735e9e) [0x76956e735e9e] (??:0)
[14] /home/irwin/apps/godot/opensource-contr/missing_docs/gdext/itest/godot/../../target/debug/libitest.so(+0x13f5dfb) [0x76956f3f5dfb] (??:0)
[15] /home/irwin/apps/godot/godot/G4/Godot_v4.5-stable_linux.x86_64() [0x4744825] (??:0)
[16] /home/irwin/apps/godot/godot/G4/Godot_v4.5-stable_linux.x86_64() [0x4c34da4] (??:0)
[17] /home/irwin/apps/godot/godot/G4/Godot_v4.5-stable_linux.x86_64() [0x42d0ba] (??:0)
[18] /lib/x86_64-linux-gnu/libc.so.6(+0x2a1ca) [0x7695a7a2a1ca] (??:0)
[19] /lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0x8b) [0x7695a7a2a28b] (??:0)
[20] /home/irwin/apps/godot/godot/G4/Godot_v4.5-stable_linux.x86_64() [0x46cb7a] (??:0)
-- END OF C++ BACKTRACE --
================================================================
./check.sh: line 83: 1657107 Aborted                 (core dumped) "$@"

==========================                                                                                                                                                                                                          
godot-rust: checks FAILED.                                                                                                                                                                                                          
==========================                                                                                                                                                                                                          

See memcheck run: https://github.com/godot-rust/gdext/actions/runs/19445857918/job/55672752869?pr=1407.

pub struct LoadedClass {
name: ClassId,

// Class icon needs to be retained for registered for class lifetime, this is not accessed directly.
Copy link
Member

Choose a reason for hiding this comment

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

Grammar? Not clear what you mean.

Comment on lines +13 to +23
#[derive(GodotClass)]
#[class(init, base=RefCounted, icon = ICON)]
struct ClassWithIconRefCounted {
base: Base<RefCounted>,
}

#[derive(GodotClass)]
#[class(init, base=Node, tool, icon = ICON)]
struct ClassWithIconNode {
base: Base<Node>,
}
Copy link
Member

Choose a reason for hiding this comment

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

Is there any notable difference in the icon logic for Node and RefCounted?

If not, it should be enough to just test with RefCounted (to ensure it works also for non-nodes).

Comment on lines +47 to +52
/// Class icon resource path. Will use this icon if available.
///
/// You can also set an icon via `#[class(icon = "path/to/icon.svg")]`.
fn icon() -> &'static str {
""
}
Copy link
Member

@Bromeon Bromeon Nov 19, 2025

Choose a reason for hiding this comment

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

Not 100% sure if this should be in the GodotClass trait... there are quite a few registration properties that aren't currently exposed like this (e.g. #[class(internal)]). For builder APIs in the future, it could otherwise be a separate step:

builder.class::<MyClass>()
    .icon("path/to/icon.svg")
    .internal()
    .build()

But if we decide GodotClass is the right place -- which needs further discussion -- then:

  1. This should document how the path has to look, i.e.

    • relative to where
    • whether it should start with res:// or not
    • maybe an example
  2. Could be named editor_icon. Since trait methods can be called unqualified, and user-defined classes having their own icon isn't that unlikely.

Comment on lines +83 to +90
// Write the extension default icon path in a `Global`.
// Will be provided to Godot during class registration (if not empty) and no class icon is provided.
// Empty by default. Resets on deinitialization, see `gdext_on_level_deinit()`.
#[cfg(since_api = "4.4")]
{
let mut icon = DEFAULT_ICON.lock();
*icon = E::default_icon();
}
Copy link
Member

Choose a reason for hiding this comment

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

Might be simpler to exclude the whole fallback logic from this PR and not try to achieve too many things at once. Icon fallbacks can easily be retrofitted later.

Comment on lines +487 to +492
let chosen = if !icon.is_empty() {
icon
} else {
// Default icon from `ExtensionLibrary::default_class_icon()`.
*crate::init::DEFAULT_ICON.lock()
};
Copy link
Member

Choose a reason for hiding this comment

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

When you have if/else with the condition being a ! negation, it's usually easier to understand if you swap the branches and invert the condition.

Comment on lines +496 to +497
c.icon_path = crate::builtin::GString::from(chosen);
c.godot_params.icon_path = c.icon_path.string_sys();
Copy link
Member

Choose a reason for hiding this comment

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

string_sys() creates a raw pointer to that field. If you move c, you will have use-after-free UB.

It's admittedly hard to see if you are not familiar with this; maybe we should rethink this API (just requiring unsafe doesn't solve the problem).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

c: register Register classes, functions and other symbols to GDScript feature Adds functionality to the library

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Specify icon as part of class declaration

4 participants