Skip to content

Make memnew(RefCounted) return Ref, to improve ownership safety#111965

Merged
Repiteo merged 1 commit into
godotengine:masterfrom
Ivorforce:memnew-typed
Mar 19, 2026
Merged

Make memnew(RefCounted) return Ref, to improve ownership safety#111965
Repiteo merged 1 commit into
godotengine:masterfrom
Ivorforce:memnew-typed

Conversation

@Ivorforce
Copy link
Copy Markdown
Member

@Ivorforce Ivorforce commented Oct 23, 2025

This PR improves RefCounted ownership semantics for newly created objects, preventing potential crashes and other footguns at build time.

Background

When memnew initializes RefCounted subclasses, they start with an (effective) allocation count of 0.

This creates two ownership related problems:

  • When stored as RefCounted * initially, some code may increase and later decrease the refcount (e.g. when passing as Variant). This destructs the object, because the refcount is decreased to 0. The actual owner's RefCounted * will be a dangling pointer. The owner should have stored Ref<RefCounted> instead.
  • When in the postinitialize_handler / NOTIFICATION_POSTINITIALIZE, RefCounted objects still have an allocation count of 0, because the caller of memnew didn't claim a reference yet. If the allocation count is increased and later decreased during this function, the object will unexpectedly destruct (see RefCounted releases itself if it is referenced in NOTIFICATION_POSTINITIALIZE #108395).

The ideal (only?) way to fix this is to start RefCounted with a refcount of 1. The memnew caller must take ownership of the refcount after the call. This just means we return Ref from memnew. Most callers already store the result in a Ref, so they are compatible with this change.

Overview

Implementing the change was fairly straight-forward; I add and specialize memnew_result_t such that for RefCounted types, memnew returns Ref<T>.

The rest of the changes are fixing current ownership problems. Mostly, it just involves changing T * to Ref<T>. It has some inherent risk, but I don't think it's huge.

@Ivorforce Ivorforce requested review from a team as code owners October 23, 2025 19:04
@Ivorforce Ivorforce requested a review from a team October 23, 2025 19:04
@Ivorforce Ivorforce requested review from a team as code owners October 23, 2025 19:04
@Ivorforce Ivorforce requested a review from a team October 23, 2025 19:04
@Ivorforce Ivorforce requested review from a team as code owners October 23, 2025 19:04
@Ivorforce Ivorforce changed the title Make memnew(RefCounted) return Ref, to force callers to take ownership of it through a reference Make memnew(RefCounted) return Ref, to improve ownership safety Oct 23, 2025
Comment thread scene/gui/color_picker.cpp Outdated
Comment thread scene/resources/resource_format_text.cpp Outdated
@AThousandShips
Copy link
Copy Markdown
Member

There would probably need to be some way to create these without explicit ownership as there are times when we need to not do refcounting for safety, also in some low-level construction I'd say, though usually the cases where we want to avoid refcounting are when we already have a reference and we avoid referencing it again

@KoBeWi
Copy link
Copy Markdown
Member

KoBeWi commented Oct 23, 2025

To me it looks like the problem is that some people used RefCounted wrong. You should either not use it with memnew(), or wrap that in Ref (AFAIK Ref<Type>(memnew(Type)) ensures proper ownership). Using instantiate() is more or less established in the codebase, so this PR only adds another available syntax for creating RefCounted objects. Instead of doing that, we could prevent the wrong usage.

@Ivorforce
Copy link
Copy Markdown
Member Author

Ivorforce commented Oct 23, 2025

There would probably need to be some way to create these without explicit ownership as there are times when we need to not do refcounting for safety, also in some low-level construction I'd say, though usually the cases where we want to avoid refcounting are when we already have a reference and we avoid referencing it again

Could you provide an example? Usually the only reason to avoid ref counting is performance, and you can still pass RefCounted objects around by reference.

To me it looks like the problem is that some people used RefCounted wrong.

This is part of the problem, but just using .instantiate() wouldn't solve the second problem from the OP.

You should either not use it with memnew(), or wrap that in Ref (AFAIK Ref(memnew(Type)) ensures proper ownership). Using instantiate() is more or less established in the codebase, so this PR only adds another available syntax for creating RefCounted objects.

This PR basically enforces the Ref<Type>(memnew(Type)) style for callers with memnew. It doesn't introduce new syntax.

Instead of doing that, we could prevent the wrong usage.

Yea, disallowing memnew for all RefCounted objects could be done as well, in the context of this PR. It's just a matter of stylistic preference whether we want memnew for all objects or just non-refcounted ones.

@AThousandShips
Copy link
Copy Markdown
Member

Could you provide an example? Usually the only reason to avoid ref counting is performance, and you can still pass RefCounted objects around by reference.

I can't remember where it was currently, but and I think that was related to creating Ref where there already was ownership, but it had to do with dangling copies and extending ownership in some contexts, but it might not be directly relevant, but might be cases where the choice to use a raw pointer is not an error

This PR basically enforces the Ref<Type>(memnew(Type)) style for callers with memnew.

But that's establishing a different style than what is established

@KoBeWi
Copy link
Copy Markdown
Member

KoBeWi commented Oct 23, 2025

This PR basically enforces the Ref(memnew(Type)) style for callers with memnew. It doesn't introduce new syntax.

I meant "new syntax" for RefCounted objects. After this PR you can do Ref<Type> var = memnew(Type), which wasn't possible before.

This is part of the problem, but just using .instantiate() wouldn't solve the second problem from the OP.

Right, so I guess that would justify this change.

@AThousandShips
Copy link
Copy Markdown
Member

After this PR you can do Ref<Type> var = memnew(Type), which wasn't possible before.

That is fully code wise possible currently? You don't have to do Ref<Type> var = Ref<Type>(memnew(Type))

@Ivorforce
Copy link
Copy Markdown
Member Author

Ivorforce commented Oct 23, 2025

I can't remember where it was currently, but and I think that was related to creating Ref where there already was ownership, but it had to do with dangling copies and extending ownership in some contexts, but it might not be directly relevant, but might be cases where the choice to use a raw pointer is not an error

I think using any refcounted object without a primary owner is an error. Passing it around via pointer is fine, but owning one without it is always unsafe, because anyone you pass it to can destroy your object (even by accident).

This PR basically enforces the Ref<Type>(memnew(Type)) style for callers with memnew.

But that's establishing a different style than what is established

That's true. I have some problems with .instantiate(), but not enough to warrant churning the whole codebase to change it. It's a good argument for going with the "forbid" route instead.

This PR basically enforces the Ref(memnew(Type)) style for callers with memnew. It doesn't introduce new syntax.

I meant "new syntax" for RefCounted objects. After this PR you can do Ref<Type> var = memnew(Type), which wasn't possible before.

This is already possible in master. Ref has an implicit conversion from T *.

Copy link
Copy Markdown
Contributor

@dsnopek dsnopek left a comment

Choose a reason for hiding this comment

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

This needs a rebase to address conflicts in the tests.

However, I'm strongly in support of this change! The core functional changes look good to me. And I've reviewed all the conversions where we were keeping refcounted objects as a pointer, and they all look good to me

@Ivorforce Ivorforce force-pushed the memnew-typed branch 2 times, most recently from 36ce7e0 to d446bcf Compare March 18, 2026 19:01
Comment thread platform/linuxbsd/wayland/wayland_thread.cpp Outdated
Comment thread scene/resources/2d/tile_set.cpp Outdated
Comment thread editor/scene/3d/node_3d_editor_plugin.h Outdated
Comment thread editor/import/3d/scene_import_settings.h Outdated
Comment thread editor/animation/animation_player_editor_plugin.h Outdated
Comment thread editor/animation/animation_player_editor_plugin.h Outdated
Comment thread editor/scene/3d/skeleton_3d_editor_plugin.h Outdated
Comment thread modules/mbedtls/crypto_mbedtls.cpp Outdated
Comment thread modules/mbedtls/tls_context_mbedtls.cpp Outdated
Comment thread scene/3d/lightmapper.cpp Outdated
@Repiteo Repiteo modified the milestones: 4.x, 4.7 Mar 19, 2026
@Repiteo Repiteo merged commit fb4a304 into godotengine:master Mar 19, 2026
20 checks passed
@Repiteo
Copy link
Copy Markdown
Contributor

Repiteo commented Mar 19, 2026

Thanks!

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants