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.

1 Like

“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