In this simple example, the _ready function will never be called on InheritedTest when the extension is compiled in Release x86_64 and is linking to libgodot-cpp.windows.template_release.x86_64.lib. It works in Debug mode when linking to libgodot-cpp.windows.template_debug.x86_64.lib.

Using Godot version 4.2.stable.official.46dc27791.
Using godot-cpp latest commit at d6e5286.

I can verify that the function is never called (and not just optimized away) because it also happens in my real extension (below is a minimal reproduction).

class BaseTest : public Node
{
public:
    GDCLASS(BaseTest, Node);

public:
    static void _bind_methods()
    {
    }
};

class InheritedTest : public BaseTest
{
public:
    GDCLASS(InheritedTest, BaseTest);

public:
    static void _bind_methods()
    {
    }

    void _ready()
    {
        OS::get_singleton()->alert(U"Never called");
    }
};
GDREGISTER_CLASS(BaseTest);
GDREGISTER_CLASS(InheritedTest);

Add InheritedTest to the main scene, build in Release and start Godot. InheritedTest::_ready should be called, but it is not. Test in Debug, and it is called.

I have not been able to figure this out. Any ideas?

  • xyz replied to this.
  • xyz That is not possible, you get linker errors.

    I have narrowed down the error to static void initialize_class() inside the GDCLASS macro. In Release, the functions InheritedTest::_get_bind_methods() and BaseTest::_get_bind_methods() end up having the same address, so the comparison to register the virtuals is never called:

    if (m_class::_get_bind_methods() != m_inherits::_get_bind_methods()) {
        _bind_methods();
        m_inherits::register_virtuals<m_class, m_inherits>();
    }

    I went through my compiler settings and found that /OPT:ICF is enabled in Release which according to the documentation says:

    Because /OPT:ICF can cause the same address to be assigned to different functions or read-only data members (that is, const variables when compiled by using /Gy), it can break a program that depends on unique addresses for functions or read-only data members. For more information, see /Gy (Enable Function-Level Linking).

    I disabled /OPT:ICF and /Gy and now the functions get unique addresses so the problem is fixed. Both of these settings are enabled by default in Visual Studio and have never caused problems for me before. But it is special in this case since GDExtension requires unique function addresses. My bad!

    See:
    https://learn.microsoft.com/en-us/cpp/build/reference/opt-optimizations?view=msvc-170
    https://learn.microsoft.com/en-us/cpp/build/reference/gy-enable-function-level-linking

    waterworks
    What's in your *.gdextension file?
    Does it happen with the base class _ready() as well?

      xyz

      When building for Debug, I am using this .gdextension file, building game_debug.dll, and linking to libgodot-cpp.windows.template_debug.x86_64.lib:

      [configuration]
      
      entry_symbol = "game_init"
      compatibility_minimum = 4.2
      
      [libraries]
      
      windows.release.x86_64 = "win64/game.dll"
      windows.debug.x86_64 = "win64/game_debug.dll"

      When building for Release, I am using this file, building game.dll and linking to libgodot-cpp.windows.template_release.x86_64.lib:

      [configuration]
      
      entry_symbol = "game_init"
      compatibility_minimum = 4.2
      
      [libraries]
      
      windows.release.x86_64 = "win64/game.dll"
      windows.debug.x86_64 = "win64/game.dll"

      From my testing, Godot uses windows.release.x86_64 for exported builds and windows.debug.x86_64 for editor builds, so this should be working.

      Implementing BaseTest::_ready makes InheritedTest::_ready be called in Release, but that is not a solution, as you'd need to implement all possible future virtual overrides as well with stubs which is not maintainable. The real question is why _ready is called in Debug builds?

      • xyz replied to this.

        waterworks Do you have a script with _ready() defined attached to the node?

        Other than that, try disabling all compiler optimization options. Does it happen with other virtual functions like _process() or _input()? Is compiler complaining if you add the override specifier to the declaration? Btw. in node.h _ready() is declared as protected, not as public.

          xyz

          There are no scripts, the code posted in the first post is the only code. There is only GDExtension, no GDScript.

          _ready must be public because Node::register_virtuals accesses it, it is also public in godot-cpp\gen\include\godot_cpp\classes\node.hpp.

          Adding override does not make a difference, the compiler does not complain, but it also does not change the outcome.

          This affects all functions, including _process and _input.

          Disabling optimization does not change anything either. This must be a bug in godot-cpp.

          • xyz replied to this.

            xyz By changing /O2 to /Od. This is not an optimization issue. I have disassembled the DLL and I can see that all my code is included. Something in godot-cpp refuses to call the function. This also happens in exported builds by the way (just that modifying the .gdextension file like above simulates the same thing).

            • xyz replied to this.

              waterworks So it happens even if you use release dll when starting the project from the editor? Does _notification() gets called?

                xyz Implemementing InheritedTest::_notification and responding to NOTIFICATION_READY does work. Same with BaseTest::_notification. I am now testing with Godot_v4.3-rc1_win64 and latest master in godot-cpp and it is still an issue. I believe in 4.2 BaseTest::_notification would never have gotten called, but that is some progress (https://github.com/godotengine/godot-cpp/pull/1381).

                So InheritedTest::_ready not being called is still an issue.

                • xyz replied to this.

                  waterworks Time to report an issue then.
                  Looks strange though. If _ready() is just a regular virtual function then it should have been called no matter what via C++ virtual call mechanism. But I guess there is some additional magic involved in GDCLASS macro or elsewhere that makes things less straightforward.

                  I don't have time to go reproducing this now, but, if you haven't already, you can also try asking over at gdextension channel on Godot's discord. Some of the people who maintain godot-cpp code appear to be active there.

                    xyz I'm assuming the reason _notification works is because it is not virtual, and not part of Node. It is part of Wrapped and when you implement it you are shadowing the empty implementation in Wrapped. The GDCLASS macro then chooses your implementation.

                    Emulating what Node::register_virtuals does by adding BIND_VIRTUAL_METHOD(InheritedTest, _ready); to InheritedTest::_bind_methods makes _ready be called. So there is probably some issue with the generation of register_virtuals.

                    Again obviously that is not a solution because you would have to do that for all the virtuals you want to override, when that should be done in Node::register_virtuals or whichever Godot class you inherit from.

                    • xyz replied to this.

                      waterworks What happens if you compile with the release setup but link to debug lib, and vice versa?

                      Btw, typically you'd want to use _notification() instead of _ready() because by design if a script implements _ready() it will override its C++ implementation. With handling NOTIFICATION_READY you can have script and extension code, both being executed.

                        xyz That is not possible, you get linker errors.

                        I have narrowed down the error to static void initialize_class() inside the GDCLASS macro. In Release, the functions InheritedTest::_get_bind_methods() and BaseTest::_get_bind_methods() end up having the same address, so the comparison to register the virtuals is never called:

                        if (m_class::_get_bind_methods() != m_inherits::_get_bind_methods()) {
                            _bind_methods();
                            m_inherits::register_virtuals<m_class, m_inherits>();
                        }

                        I went through my compiler settings and found that /OPT:ICF is enabled in Release which according to the documentation says:

                        Because /OPT:ICF can cause the same address to be assigned to different functions or read-only data members (that is, const variables when compiled by using /Gy), it can break a program that depends on unique addresses for functions or read-only data members. For more information, see /Gy (Enable Function-Level Linking).

                        I disabled /OPT:ICF and /Gy and now the functions get unique addresses so the problem is fixed. Both of these settings are enabled by default in Visual Studio and have never caused problems for me before. But it is special in this case since GDExtension requires unique function addresses. My bad!

                        See:
                        https://learn.microsoft.com/en-us/cpp/build/reference/opt-optimizations?view=msvc-170
                        https://learn.microsoft.com/en-us/cpp/build/reference/gy-enable-function-level-linking

                          waterworks Great find! And I don't think this counts as a "my bad" -- that side effect is pretty obscure.