Returning Custom Classes from C++ GDExtension

Godot Version

Godot 4.2.2

Question

Hello,
I’m trying to wrap one of my C++ Libraries to use in GDScript.
I have a data class that looks something like this:

class Moment : public godot::Object
{
    GDCLASS(Moment, godot::Object)

public:
    godot::Vector3 getPose();
    ....

protected:
    void _bind_methods();

private:
    std::vector<Eigen::Vector2f> measurements;
    Eigen::AffineCompact2f pose;
};

this class holds LiDAR (Laser Scans of an Environment) data which I’d like to display in Godot, thus I’ve created a secondary class called MomentManager that loads the data from a file and should make it available to GDScript its interface looks something like:

class MomentManager : public godot::Object
{
    GDCLASS(MomentManager, godot::Object)

public:
    void load_from_file(String path);
    Moment at(size_t index);

protected:
    void _bind_methods();

private:
    std::vector<Moment> moments;
};

But as soon as I try to register the MomentManager::at() method I get a compile error. Everything else works just fine.
I’m using CMake.
Compile Error:

[main] Building folder: visualizer 
[build] Starting build
[proc] Executing command: "C:\Program Files\CMake\bin\cmake.EXE" --build visualizer/build --config Debug --target all --
[build] [1/2   0% :: 0.036] Re-checking globbed directories...
[build] [1/3  33% :: 2.714] Building CXX object src\CMakeFiles\mog-slam-visualizer.dir\godot-extension\MomentsManager.cpp.obj
[build] FAILED: src/CMakeFiles/mog-slam-visualizer.dir/godot-extension/MomentsManager.cpp.obj 
[build] C:\PROGRA~2\MICROS~2\2022\BUILDT~1\VC\Tools\MSVC\1439~1.335\bin\Hostx64\x64\cl.exe  /nologo /TP -DDEBUG_ENABLED -DDEBUG_METHODS_ENABLED -DTYPED_METHOD_BIND -Dmog_slam_visualizer_EXPORTS -Ibuild\app_config -Isrc -Ibuild\_deps\mogs-src\src -Ibuild\_deps\mogs-build\src -Ibuild\_deps\mogs-src\libraries\cereal\include -external:Ibuild\_deps\godot-cpp-src\include -external:Ibuild\_deps\godot-cpp-build\gen\include -external:Ibuild\_deps\godot-cpp-src\gdextension -external:Ibuild\_deps\mogs-src\libraries\Eigen3 -external:W0 /DWIN32 /D_WINDOWS /EHsc /Zi /Ob0 /Od /RTC1 -std:c++latest -MDd /bigobj /D_SILENCE_ALL_CXX23_DEPRECATION_WARNINGS /showIncludes /Fosrc\CMakeFiles\mog-slam-visualizer.dir\godot-extension\MomentsManager.cpp.obj /Fdsrc\CMakeFiles\mog-slam-visualizer.dir\ /FS -c src\godot-extension\MomentsManager.cpp
[build] build\_deps\godot-cpp-src\include\godot_cpp/core/method_bind.hpp(556): error C2027: use of undefined type 'godot::GetTypeInfo<R,void>'
[build]         with
[build]         [
[build]             R=const godot::Moment &
[build]         ]
[build] build\_deps\godot-cpp-src\include\godot_cpp/core/type_info.hpp(104): note: see declaration of 'godot::GetTypeInfo<R,void>'
[build]         with
[build]         [
[build]             R=const godot::Moment &
[build]         ]
[build] build\_deps\godot-cpp-src\include\godot_cpp/core/method_bind.hpp(556): note: the template instantiation context (the oldest one first) is
[build] src\godot-extension\MomentsManager.cpp(64): note: see reference to function template instantiation 'godot::MethodBind *godot::ClassDB::bind_method<godot::MethodDefinition,const godot::Moment&(__cdecl godot::MomentsManager::* )(int64_t) const,>(N,M)' being compiled
[build]         with
[build]         [
[build]             N=godot::MethodDefinition,
[build]             M=const godot::Moment &(__cdecl godot::MomentsManager::* )(int64_t) const
[build]         ]
[build] build\_deps\godot-cpp-src\include\godot_cpp/core/class_db.hpp(293): note: see reference to function template instantiation 'godot::MethodBind *godot::create_method_bind<godot::MomentsManager,const godot::Moment&,int64_t>(R (__cdecl godot::MomentsManager::* )(int64_t) const)' being compiled
[build]         with
[build]         [
[build]             R=const godot::Moment &
[build]         ]
[build] build\_deps\godot-cpp-src\include\godot_cpp/core/method_bind.hpp(587): note: see reference to class template instantiation 'godot::MethodBindTRC<godot::MomentsManager,const godot::Moment &,int64_t>' being compiled
[build] build\_deps\godot-cpp-src\include\godot_cpp/core/method_bind.hpp(552): note: while compiling class template member function 'GDExtensionClassMethodArgumentMetadata godot::MethodBindTRC<godot::MomentsManager,const godot::Moment &,int64_t>::get_argument_metadata(int) const'
[build] build\_deps\godot-cpp-src\include\godot_cpp/core/method_bind.hpp(556): error C2065: 'METADATA': undeclared identifier
[build] ninja: build stopped: subcommand failed.
[proc] The command: "C:\Program Files\CMake\bin\cmake.EXE" --build visualizer/build --config Debug --target all -- exited with code: 1
[driver] Build completed: 00:00:02.761
[build] Build finished with exit code 1

Hmm, I haven’t done a whole lot with gdextensions yet but isn’t _bind_methods() supposed to be static? Try that I think it might go. Rest looks about right to me.

source: https://github.com/godotengine/godot-docs/blob/master/tutorials/scripting/gdextension/gdextension_cpp_example.rst

Oh, yes that’s true, but I only have this error in this example, in my real code its static, and I still have the error.

OK, hmm. Well I couldn’t make much of it, so I googled it, and it took me to this older post here I wonder if you’ve seen: binding-errors-when-returning-an-array

There they mention the pointer to array was an issue, but just returning the Array was not, so perhaps that’s worth a look. But I don’t think that’s it, because why would that just pop up when you get at() in there.

So here’s my other thought: friend class. Maybe add a friend class MomentManager to the Moment declaration, and maybe that would expose whatever obscure generated private functions Godot is making for Moment to MomentManager.

That’s all I got for now, that’s a tricky one. GL.

Can you update the post to match the state of your real code? Also can you post the bind methods implementation?

I can’t do this, since this will create a cyclic include dependencies, since both MomentManger and Moment would need each other.

I can’t find an edit button on my original post, only the link and bookmark button.

Here is the implementations of the _bind_methods() function

void
godot::MomentsManager::_bind_methods()
{
    ClassDB::bind_method(D_METHOD("load_from_file", "path"), &MomentsManager::loadFromFile);
    ClassDB::bind_method(D_METHOD("generate_image", "index", "scale"), &MomentsManager::generateImage);
    ClassDB::bind_method(D_METHOD("size"), &MomentsManager::size);
    ClassDB::bind_method(D_METHOD("at", "index"), &MomentsManager::at); // once I add this line the compilation erros start to occur
}

and the one from Moment

void
godot::Moment::_bind_methods()
{
    godot::ClassDB::bind_method(D_METHOD("get_pose"), &godot::Moment::getPose);
}

And my register_types function looks like this

void initialize_module(godot::ModuleInitializationLevel level)
{
    if (level != godot::MODULE_INITIALIZATION_LEVEL_SCENE)
        return;

    godot::ClassDB::register_class<godot::Moment>();
    godot::ClassDB::register_class<godot::MomentsManager>();
}

Oh, ok. Well I’m still thinking this one through some and I found this (*), it mentions that function parameter and return values must be Godot API types, and I wonder if you would get the same errors if at()'s return type was godot::Object, instead of Moment.

(*) Stack Overflow Link - “using-custom-types-in-godot-with-gdextension”

1 Like

Note: For future posts do not paraphrase or type code by hand without testing it before posting. Double check for typos and make sure it is an accurate representation of your “real” code.

The issue stems from three major things. One of which is I assume to just be typos. The other two are what @MortalWombat had already pointed out.

  1. _bind_methods() must be static.
  2. Your bindings must only expose types allowed by GDExtension.
    • Object types must be pointers e.g. Moment*.
    • size_t may not be a valid Variant primitive type depending on the compiler implementation. Use int64_t. You can see the convertible types in the variant.hpp file.

Solution

After fixing typos:

// Header
class MomentManager : public Object {
  // ...
  Moment *at(int64_t index);
}

// Source
Moment *MomentManager::at(int64_t index) {
  // ...
}
2 Likes

Thanks for the help, now everything compiles fine, but I’m not able to use the return type of at() in GDScript. For example

# Test.gd
var moments_manager: MomentsManager

func __ready():
	moments_manager = MomentsManager.new()
	moments_manager.load_from_file("res://path/to/file")
	var moment = moments_manager.at(0)
	print(moment.get_pose()) # Stops with: "Invalid call. Non-existent function 'get_pose' in 'Object'."

And if I try to pass the moment into a new function defined within my MomentManager as void insert(Moment* moment); it just receives a nullptr.

How are you initializing Moment objects? Can you post context around calling insert and the implementation of at?

You can find all the relevant code in these two repositories:

For convenience I’ll provide some snippets here. The load_from_file function looks like this

bool
godot::MomentsManager::loadFromFile(const String& path)
{
    try
    {
        auto rawMoments = utils::LogReader::allSteps(path.utf8().get_data());
        moments.reserve(rawMoments.size());
        for (auto& rawMoment : rawMoments)
            moments.emplace_back(std::move(rawMoment));
    }
    catch (std::exception& e)
    {
        return false;
    }

    return true;
}

The utlis::LogReader::allSteps function is implented in the the second repo and returns a vector of utils::definitions::Timesteps thus i created a constructor for the Moment class that looks like this:

godot::Moment::Moment(utils::definitions::Timestep&& other)
: utils::definitions::Timestep{std::move(other)}
{
}

The at function is implemented like this now

godot::Moment*
godot::MomentsManager::at(int64_t index)
{
    return &moments[index];
}

did you try casting the Moment godot::Object return value to a Moment instance? like:

var moment = moments_manager.at(0) as Moment;

I’m not sure if casting it would work the same since it’s a pointer, maybe you have to cast it to a Ref<Moment> (or something else, like that). But casting to Moment like in above line would be my first thought.

The casts unfortunally didn’t work:(

GDExtension objects have to be properly initialized and go through a registration process.

Recommended

AFAIK, the recommended way to interface with GDExtension is to memnew() your objects. memnew will properly initialize and register the object with godot.

Moment* moment = memnew(Moment);
moment->timestamp = rawMoment.timestamp;

/* Where moments is defined as: std::vector<Moment*> moments; */
moments.push_back(moment);

Otherwise

You could specify the parent class’ constructor in the copy and move constructor:

Moment::Moment(Timestep&& other)
: Object("Moment"), Timestep{other}
{}

Moment::Moment(const Timestep& other)
: Object("Moment"), Timestep{other}
{}

I’m not sure what side-effects there are or lack thereof, but this seems to work at least for emplacing and fetching with at later. I didn’t test creating a Moment object from gdscript.

You can dig further by checking out

  • include/godot_cpp/classes/wrapped.hpp
  • include/godot_cpp/core/memory.hpp

and their source files.

Also, to see the output of the GDCLASS and GDEXTENSION_CLASS macros you can compile the project and pass -E to (well clang at least).


If you find out more about this I’d like to hear about it. I’m interested in learning more gdextension.

2 Likes

“If you find out more about this I’d like to hear about it. I’m interested in learning more gdextension.”

I learned a lot digging into the Terrain3D project code, it’s really well put together and a great example of multiple GDExtension objects interoperating and communicating with Godot just fine, like native classes.

“The casts unfortunally didn’t work:(” - nuts, well thanks for trying. Sounds like you have some other options now, gl. TBH it’s out of my paygrade, I’m just here for the learning at this point.

1 Like

Thanks that help a lot, I tested both ways, the second suggestion worked somewhat, meaning that I no longer got a nullptr back but it just returned random values, I’m guessing it has to do with what’s described in this issue.

The first method with using memnew worked perfectly, I’m just a bit disappointed to have to handle raw pointers in times where we have smart pointers and such, also I’m guessing I have to memdelete these classes in the destructor, which could easily lead to memory leaks.

Summary

Two things have to be consider to create instances of your class in GDExtension and to use them in GDScript aswell.

Initialization

As @indicainkwell pointed out we have to initialize objects that we want to initialize in our GDExtension but also modify in GDScript via the memnew method.

Meaning if that to create a new instance of my Moment class I could do something like this:

auto* pMoment = memnew(Moment);
pMoment = rawMoment; // Assuming a asign operator exists for these types

An important thing to remember is that we have to free this memory if we don’t need it anymore with a call to memdelete otherwise we create a memory leak. Thus we have to add the following line to our code

memdelete(pMoment);

Edit: This next section is outdated and I later found out that godot already implments such a wrapper, this is explained at the end of the post.

Since this isn’t really up to date with the new C++ standards, I’d advise to create a wrapper that handles the creations of such objects, this could be implemented like this

template<typename T>
class External
{
public:
    External() : pInstance{memnew(T)} {}
    External(const External&) = delete;
    External(External&& other) : pInstance{other.pInstance} { other.pInstance = nullptr; }
    External& operator=(const External&) = delete;
    External& operator=(External&& other) {
        if (this != &other) {
            pInstance = other.pInstance;
            other.pInstance = nullptr;
        }

        return *this;
    }
    ~External() { if (pInstance == nullptr) return; memdelete(pInstance); }

    const T* get() const { return pInstance; }
    T* get() { return pInstance; }

    const T* operator->() const { return pInstance; }
    T* operator->() { return pInstance; }

    template<typename U>
    void assign(const U& other) { *pInstance = other; }
    template<typename U>
    void assign(U&& other) { *pInstance = std::move(other); }

private:
    T* pInstance;
};

With this wrapper we are able to achive the goal of the first to snippets with the following lines of code, and we don’t have to worry about resource leaks

External<Moment> moment;
moment.assign(rawMoment);

Returning and Passing as Argument

To return an Object to GDScript we have to possiblities as shown by @MortalWombat and @indicainkwell.

  • Either by returning a pointer
  • Or a Ref object

E.g.

Moment* at(int64_t index);

or

Ref<Moment> at(int64_t index);

Be aware that if you want to return a Ref than your class has to inherit from RefCounted or a child of it, with the pointer method we are able to return every type that inherits of Object.

Edit: Using Ref instead of memnew

One can skip the whole External thing mentioned above and instead use a create a Ref<Moment> which does all the memory management for us. To do this we can do the following:

Ref<Moment> moment;
moment.instantiate();
moment->operator=(rawMoment);
3 Likes

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.