Search code examples
c++godot

Exposing functions, structs, and type aliases to Godot 4 with GDExtension?


I'm messing around with Godot 4 and GDExtension, trying to hook up some C++ code. Right now, I'm a bit stuck because I can't find any good examples on how to expose not just classes, but functions, structs, and type aliases. Here's what I got:

register_types.cpp:

#include "register_types.h"
#include "../godot-cpp/gdextension/gdextension_interface.h"
#include "../godot-cpp/include/godot_cpp/core/class_db.hpp"
#include "../godot-cpp/include/godot_cpp/core/defs.hpp"
#include "../godot-cpp/include/godot_cpp/godot.hpp"
#include "cards.hpp"

void initialize_gdextension_types(godot::ModuleInitializationLevel p_level) {
  if (p_level != godot::MODULE_INITIALIZATION_LEVEL_CORE) {
    return;
  }
  // TODO: register my functions and structs
  // TODO: figure out how to do it
  godot::ObjectDB::register_object<Error>(); // <- no member named 'register_object' :)
}

void uninitialize_gdextension_types(godot::ModuleInitializationLevel p_level) {
  if (p_level != godot::MODULE_INITIALIZATION_LEVEL_CORE) {
    return;
  }
}

extern "C" {
// Initialization
// NOTE: I don't think we need to touch anything here
GDExtensionBool GDE_EXPORT
poker_utils_library_init(GDExtensionInterfaceGetProcAddress p_get_proc_address,
                         GDExtensionClassLibraryPtr p_library,
                         GDExtensionInitialization *r_initialization) {
  godot::GDExtensionBinding::InitObject init_obj(p_get_proc_address, p_library,
                                                 r_initialization);
  init_obj.register_initializer(initialize_gdextension_types);
  init_obj.register_terminator(uninitialize_gdextension_types);
  init_obj.set_minimum_library_initialization_level(
      godot::MODULE_INITIALIZATION_LEVEL_CORE);

  return init_obj.init();
}
}

cards.hpp:

#ifndef CARDS_H_
#define CARDS_H_

#include <string>
#include <variant>

struct Error {
  std::string message;
};

using CardIndexResult = std::variant<int, Error>;

CardIndexResult getCardIndex(const std::string &card);

#endif // CARDS_H_

I tried the godot::ObjectDB::register_object<Error>(); thing, but no dice. So, how do I:

  1. Get the Error struct into Godot?
  2. Make getCardIndex callable from Godot scripts?
  3. Deal with the CardIndexResult type alias?

For future reference

I was able to compile the following:

// register_types.cpp
#include "register_types.h"
#include "../godot-cpp/gdextension/gdextension_interface.h"
#include "../godot-cpp/include/godot_cpp/core/class_db.hpp"
#include "../godot-cpp/include/godot_cpp/core/defs.hpp"
#include "../godot-cpp/include/godot_cpp/godot.hpp"

#include "deck.hpp"

void initialize_gdextension_types(godot::ModuleInitializationLevel p_level) {
  if (p_level != godot::MODULE_INITIALIZATION_LEVEL_CORE) {
    return;
  }
  godot::ClassDB::bind_static_method("@GlobalScope",
                                     godot::D_METHOD("get_deck"), &getDeck);
}

void uninitialize_gdextension_types(godot::ModuleInitializationLevel p_level) {
  if (p_level != godot::MODULE_INITIALIZATION_LEVEL_CORE) {
    return;
  }
}

extern "C" {
// Initialization
// NOTE: I don't think we need to touch anything here
GDExtensionBool GDE_EXPORT
poker_utils_library_init(GDExtensionInterfaceGetProcAddress p_get_proc_address,
                         GDExtensionClassLibraryPtr p_library,
                         GDExtensionInitialization *r_initialization) {
  godot::GDExtensionBinding::InitObject init_obj(p_get_proc_address, p_library,
                                                 r_initialization);
  init_obj.register_initializer(initialize_gdextension_types);
  init_obj.register_terminator(uninitialize_gdextension_types);
  init_obj.set_minimum_library_initialization_level(
      godot::MODULE_INITIALIZATION_LEVEL_CORE);

  return init_obj.init();
}
}

And this is the defintion of the deck.hpp file:

#ifndef GODOT_PLUGIN_DECK_H_
#define GODOT_PLUGIN_DECK_H_

#include "../godot-cpp/include/godot_cpp/godot.hpp"
#include "../godot-cpp/include/godot_cpp/templates/vector.hpp"
#include "../godot-cpp/include/godot_cpp/variant/variant.hpp"

godot::PackedStringArray getDeck();

#endif // GODOT_PLUGIN_DECK_H_

That function is a encapsulation of my original function, see it here:

// deck.cpp
#include "deck.hpp"
#include "../src/deck.hpp"

godot::PackedStringArray getDeck() {
  godot::PackedStringArray cards;
  auto deck = poker::getDeck();

  for (const auto &card : deck) {
    cards.push_back(godot::String(card.c_str()));
  }

  return cards;
};

However, get_deck isn't showing at @GlobalScope

The extension is being detected by Godot and loaded, after restarting the editor I went right into the documentation of @GlobalScope but get_deck isn't showing. IDK what I'm doing wrong at this point.

I saw the mistake, did some corrections and now things work

So I ended making a class to encapsulate my functions, on register_types.cpp at the initialize_gdextension_types function I put godot::ClassDB::register_class<Poker>(). The class itself takes care of registering its methods:

// poker.cpp
void Poker::_bind_methods() {
  godot::ClassDB::bind_static_method("Poker", godot::D_METHOD("get_deck"),
                                     &Poker::getDeck);
  godot::ClassDB::bind_static_method(
      "Poker", godot::D_METHOD("get_deck_shuffled"), &Poker::getDeckShuffled);
}

Now Godot can see the class and use its methods!


Solution

  • You're on the right track, but for your custom class or struct to be handled by Godot, it has to inherit from Godot's Object type (or any other class), since you'll need a few specific member functions for the interaction with the engine (including scripting).

    To do this, you have to use inheritance and an extra macro inserting the additional members:

    class Error: public godot::Object {
      GDCLASS(Error, godot::Object);
    }
    

    This should allow you to register the class in your initialization function and then pass it around, and create/destroy instances of it:

    godot::ClassDB::register_class<Error>();
    

    If your custom class needs additional members, you'd add a protected member static void _bind_methods(); to your Error class, which is then able to use similar ClassDB methods to add any callbacks or properties you want to expose.

    To bind a global function that's not a member, you can use ``, something like this:

    godot::ClassDB::bind_static_method("ParentClassName", "getCardIndex", &getCardIndex, godot::String());
    

    I'm not sure how to do it with global scope, I think you can pass "@GlobalScope" or so, but I've never done it before and can only type from memory right now, so take this note with a grain of salt.