Skip to content

How does py4godot work?

Niklas Zimmer edited this page Nov 22, 2024 · 20 revisions

Motivation

The goal of this page is to clarify how py4godot works, making it easier for others to understand, fork, or contribute to the project. A common challenge I see with many open-source projects is that their complexity can make it hard for newcomers to get involved—I often feel the same way. Recognizing this, I decided to take on the challenge of documenting py4godot more clearly for anyone who may want to contribute.

Over the next few months, I plan to add detailed notes that explain the design and structure of py4godot. Please keep in mind that my code is a work in progress. It’s not polished, and you might encounter some messy C++ and Python code along the way. If that doesn’t appeal to you, feel free to skip this page - but if you're curious, I hope this documentation provides a helpful starting point.

Note: This is a work in progress. Hopefully, over the next few months, you'll see more content published here.

Cython

This project uses cython to bind python to C++. The reason, I use it is mostly historically, because when I started for Godot 3, I saw that it was being used by godot-python

gdextension

You need a json file to get all the classes and their methods, Godot exposes. You can dump it like that: godot --dump-extension-api. We also need a header file which exposes the gdextension functionality. So we can run godot --dump-gdextension-interface to obtain it. The resulting header file exposes methods, we can later use for our bindings to godot

Structure of Python classes

In the following the structure of the classes beign generated from the information in the json api file of godot will be explained. They are being generated by the file generation_files/generate_classes.pyand generation_files/generate_classes_pxd.py

pxd class files

Here is an example for the pxd file, I generate for the bindings:

# distutils: language=c++
cimport py4godot.classes.Node as py4godot_node
from py4godot.classes.Object cimport *
from libcpp.memory cimport shared_ptr, allocator
from py4godot.classes.cpp_bridge cimport Node3D as CPPNode3D 
from py4godot.classes.core cimport *
cpdef enum Node3D__RotationEditMode:
 Node3D__ROTATION_EDIT_MODE_EULER = 0
 Node3D__ROTATION_EDIT_MODE_QUATERNION = 1
 Node3D__ROTATION_EDIT_MODE_BASIS = 2


cdef class Node3D(py4godot_node.Node):


 cdef shared_ptr [CPPNode3D] Node3D_internal_class_ptr


 cdef void set_gdowner(self, void* owner)

 cdef object py__transform
 cdef object py__global_transform
 cdef object py__position
 cdef object py__rotation
 cdef object py__rotation_degrees
 cdef object py__quaternion
 cdef object py__basis
 cdef object py__scale
 cdef object py__rotation_edit_mode
 cdef object py__rotation_order
 cdef object py__top_level
 cdef object py__global_position
 cdef object py__global_basis
 cdef object py__global_rotation
 cdef object py__global_rotation_degrees
 cdef object py__visible
 cdef object py__visibility_parent

 cdef public object visibility_changed

Let's break down, what is happening here. First the imports:

cimport py4godot.classes.Node as py4godot_node
from py4godot.classes.Object cimport *
from libcpp.memory cimport shared_ptr, allocator
from py4godot.classes.cpp_bridge cimport Node3D as CPPNode3D 
from py4godot.classes.core cimport *

First, we import the parent class, as we need to specify that our class will inherit from it. We can probably ignore the Object import you see here, as it isn’t actually used. Next, we import some pointer functionality from C++ so we can work with shared pointers. Later, we’ll see that we maintain a pointer to the equivalent C++ class for our Cython class. Finally, we import some core classes, though they won’t be used in the rest of the code.

cdef class Node3D(py4godot_node.Node):


 cdef shared_ptr [CPPNode3D] Node3D_internal_class_ptr


 cdef void set_gdowner(self, void* owner)

Here we define our Cython class. As we can see, it holds a pointer to the equivalent C++ class. We also have a method method set_gdowner. This is used to set the shared_ptr to the correct values. We'll dive later into it.

 cdef object py__transform
 cdef object py__global_transform
 cdef object py__position
 cdef object py__rotation
 cdef object py__rotation_degrees
 cdef object py__quaternion
 cdef object py__basis
 cdef object py__scale
 cdef object py__rotation_edit_mode
 cdef object py__rotation_order
 cdef object py__top_level
 cdef object py__global_position
 cdef object py__global_basis
 cdef object py__global_rotation
 cdef object py__global_rotation_degrees
 cdef object py__visible
 cdef object py__visibility_parent

 cdef public object visibility_changed

Finally, the py__ values, you see, are the properties of this class. As I'm managing memory in Python, I need some mechanism, when classes would be deleted by Python garbage collection. To make this feasible, I store them in the class and let python take care of it.
The last cdef public object visibility_changed, you see here is actually a Signal, which the user can use when scripting. Unfortunately I couldn't specify the type here, as it would have lead to a cyclic import. We'll dive later into signals

pyx class files

Now let's look at our pyx files, where our whole logic actually is. We'll break it down in the next few paragraphs.

#imports...

cdef class Node3D(py4godot_node.Node):
  @staticmethod
  def constructor():
    cdef Node3D class_ = Node3D.__new__(Node3D)
    class_.Node3D_internal_class_ptr = construct_Node3D()
    class_.set_gdowner(class_.Node3D_internal_class_ptr.get().get_godot_owner())
    return class_
  @staticmethod
  def new():
    cdef Node3D class_ = Node3D.__new__(Node3D)
    class_.Node3D_internal_class_ptr = construct_Node3D()
    class_.set_gdowner(class_.Node3D_internal_class_ptr.get().get_godot_owner())
    return class_
  def __init__(self):
    if py_utils.shouldCreateObject:
      self.Node3D_internal_class_ptr = construct_Node3D()
      self.set_gdowner(self.Node3D_internal_class_ptr.get().get_godot_owner())



  def __cinit__(self):
    self.Node3D_internal_class_ptr = make_shared[CPPNode3D]()
    self.already_deallocated = False
    self.Node_internal_class_ptr = make_shared[CPPNode]()
    self.Object_internal_class_ptr = make_shared[CPPObject]()

    cdef StringName visibility_changed_name = py_c_string_to_string_name("visibility_changed")
    self.visibility_changed = BuiltinSignal(self, visibility_changed_name)

  cdef void set_gdowner(self, void* owner):
    self.Node3D_internal_class_ptr.get().set_gdowner_Node3D(owner)
    self.Node_internal_class_ptr.get().set_gdowner_Node(owner)
    self.Object_internal_class_ptr.get().set_gdowner_Object(owner)
  def __dealloc__(self):
    pass
  @staticmethod
  def cast(Object other):
    assert other != None # Object to be casted must not be None
    cdef Node3D cls = Node3D.__new__(Node3D)
    cls.Node3D_internal_class_ptr = cast_to_Node3D(other.Object_internal_class_ptr.get())
    cls.set_gdowner(other.Object_internal_class_ptr.get().get_godot_owner())
    return cls

  @property
  def transform(self):
    cdef _ret = self. get_transform()
    return _ret
  @transform.setter
  def transform(self, Transform3D value):
    self.set_transform(value)

  # More properties...

  def set_transform(self, Transform3D local ):
    assert(not local is None)
    self.py__transform = local
    self.Node3D_internal_class_ptr.get().py_set_transform(local.Transform3D_internal_class_ptr)

  def get_transform(self):
    cdef Transform3D _ret = Transform3D.__new__(Transform3D)
    _ret.Transform3D_internal_class_ptr = self.Node3D_internal_class_ptr.get().py_get_transform()
    _ret.set_gdowner(_ret.Transform3D_internal_class_ptr.get().get_godot_owner())

    self.py__transform = _ret
    return _ret

  # More methods ...

First let's look at the various methods of constructing the classes:

#imports...

cdef class Node3D(py4godot_node.Node):
  @staticmethod
  def constructor():
    cdef Node3D class_ = Node3D.__new__(Node3D)
    class_.Node3D_internal_class_ptr = construct_Node3D()
    class_.set_gdowner(class_.Node3D_internal_class_ptr.get().get_godot_owner())
    return class_
  @staticmethod
  def new():
    cdef Node3D class_ = Node3D.__new__(Node3D)
    class_.Node3D_internal_class_ptr = construct_Node3D()
    class_.set_gdowner(class_.Node3D_internal_class_ptr.get().get_godot_owner())
    return class_
  def __init__(self):
    if py_utils.shouldCreateObject:
      self.Node3D_internal_class_ptr = construct_Node3D()
      self.set_gdowner(self.Node3D_internal_class_ptr.get().get_godot_owner())



  def __cinit__(self):
    self.Node3D_internal_class_ptr = make_shared[CPPNode3D]()
    self.already_deallocated = False
    self.Node_internal_class_ptr = make_shared[CPPNode]()
    self.Object_internal_class_ptr = make_shared[CPPObject]()

    cdef StringName visibility_changed_name = py_c_string_to_string_name("visibility_changed")
    self.visibility_changed = BuiltinSignal(self, visibility_changed_name)

construct, new and __init__ are all methods for creating an object. The methods construct and new are mostly historical, and I'm trying to get to __init__. But they all share the same functionality: We are creating a pointer to a C++ class and then set all the pointers within the class with it. As I've said: We will later see, how set_gdowner works exactly. The __cinit__ is always called, even when we are using __new__. It creates Pointers for the whole inheritance chain of our class. On top of that, with self.already_deallocated = False, we set a property, that makes sure, that our class doesn't accidently get deleted twice. Finally we also set our signal visibility_changed to create a Signal, the user can later use.

  cdef void set_gdowner(self, void* owner):
    self.Node3D_internal_class_ptr.get().set_gdowner_Node3D(owner)
    self.Node_internal_class_ptr.get().set_gdowner_Node(owner)
    self.Object_internal_class_ptr.get().set_gdowner_Object(owner)
  def __dealloc__(self):
    pass
  @staticmethod
  def cast(Object other):
    assert other != None # Object to be casted must not be None
    cdef Node3D cls = Node3D.__new__(Node3D)
    cls.Node3D_internal_class_ptr = cast_to_Node3D(other.Object_internal_class_ptr.get())
    cls.set_gdowner(other.Object_internal_class_ptr.get().get_godot_owner())
    return cls

Next We have the method set_gdowner. This sets all class pointers, we can have. This goes up the inheritance chain. You might ask, why we have multiple pointers. This is because we later use these objects to call the methods. But as we have multiple classes, we use the corresponding class to call the method. For example, the class Object has the method get_class. So we would use the Object_internal_class_ptr for this and thus must set it. The method cast is used, so that we are able to cast a node to another. In Godot, when getting a return value like Node from get_node in the Node class, we need to cast this object, we get as Node to the correct type, in order to use specific methods, like methods for Node3D. 

In the method cast,  C++ method cast_to_Node3D to cast our other pointer and then add it to our Node3D object which was created without the pointers being filled.

  @property
  def transform(self):
    cdef _ret = self. get_transform()
    return _ret
  @transform.setter
  def transform(self, Transform3D value):
    self.set_transform(value)

This is an example of how properties are created in py4godot. Properties are defined using the @property annotation, which connects them to their respective setter and getter methods. These methods are specified in the gdextension-api.json file.

  def set_transform(self, Transform3D local ):
    assert(not local is None)
    self.py__transform = local
    self.Node3D_internal_class_ptr.get().py_set_transform(local.Transform3D_internal_class_ptr)

  def get_transform(self):
    cdef Transform3D _ret = Transform3D.__new__(Transform3D)
    _ret.Transform3D_internal_class_ptr = self.Node3D_internal_class_ptr.get().py_get_transform()
    _ret.set_gdowner(_ret.Transform3D_internal_class_ptr.get().get_godot_owner())

    self.py__transform = _ret
    return _ret

Finally, here you can see, how methods are created. In set_transform The py__transform is an attribute set in order to use garbage collection from Python. After that, we use our C++ class and call it with the C++ pointer from our argument. In the get_transform, we first create our empty return value. We fill it then with pointer returned by the py_get_transform method of our C++ class. Additionally, we propagate the pointer with set_godot_owner. Finally, we set our current value for garbage collection to the value, we just used to set.

C++ classes

The C++ classes contain the logic of the interaction with the gdextension logic. The reason, we use them instead of putting the logic directly into Cython is, because it is much easier to debug like this.

Header files

This is an example of a typical header file for a C++ class in py4godot. These classes can be found in the subfolder py4godot/cppclasses. Here is Node3D as an example class:

#ifndef NODE3D_  
#define NODE3D_  
#include "py4godot/cpputils/Wrapper.h"  
#include "py4godot/cpputils/VariantTypeWrapper.h"  
#include "py4godot/cppcore/Variant.h"  
#include "py4godot/cppenums/cppenums.h"  
#include<memory>  
  
  
#include "py4godot/cppclasses/generated4_core.h"  
#include "py4godot/cppclasses/Node/Node.h"  
  
namespace godot{  
  
class LIBRARY_API  Node3D:public Node{  
   public:  
    bool already_deleted = false;  
void   Node3D_py_destroy();  
  ~Node3D();  
  
  static std::shared_ptr<Node3D> constructor();  
  
  
  static Node3D new_static(GDExtensionObjectPtr owner);  
  void set_gdowner_Node3D(GDExtensionObjectPtr owner);  
  
  static std::shared_ptr<Node3D> cast(Wrapper* pwrapper);  
  
  
  
   void set_transform( Transform3D& local);  
     void py_set_transform( std::shared_ptr<Transform3D> local);  
  
   Transform3D* buffer_Node3D_get_transform;  
   Transform3D get_transform();  
     std::shared_ptr<Transform3D> py_get_transform();  
  
static std::shared_ptr<Node3D> construct_Node3D(){  
  return Node3D::constructor();  
}  
static std::shared_ptr<Node3D> cast_to_Node3D(Wrapper* pwrapper){  
  return Node3D::cast(pwrapper);  
}  
  
}
}
#endif

Now let's dive into the includes:

#ifndef NODE3D_  
#define NODE3D_  
#include "py4godot/cpputils/Wrapper.h"  
#include "py4godot/cpputils/VariantTypeWrapper.h"  
#include "py4godot/cppcore/Variant.h"  
#include "py4godot/cppenums/cppenums.h"  
#include<memory>  
  
  
#include "py4godot/cppclasses/generated4_core.h"  
#include "py4godot/cppclasses/Node/Node.h"

And actually, there is not very much to it. We Include classes like Wrapper, because they hold our godot pointer (more on this later), VariantTypeWrapper for core classes (Vector3, Vector2, Array, etc.) and enums. On top of that, we need memory for shared_ptrs we use in godot. On top of that, we include our godot dependencies generated4_core.h for the core classes and Node.h for the class Node, which Node3D inherits from.

Now let's look at the class itself

namespace godot{  
  
class LIBRARY_API  Node3D:public Node{  
   public:  
    bool already_deleted = false;  
void   Node3D_py_destroy();  
  ~Node3D();  
  
  static std::shared_ptr<Node3D> constructor();  
  
  
  static Node3D new_static(GDExtensionObjectPtr owner);  
  void set_gdowner_Node3D(GDExtensionObjectPtr owner);  
  
  static std::shared_ptr<Node3D> cast(Wrapper* pwrapper);  
  
  
  
   void set_transform( Transform3D& local);  
     void py_set_transform( std::shared_ptr<Transform3D> local);  
  
   Transform3D* buffer_Node3D_get_transform;  
   Transform3D get_transform();  
     std::shared_ptr<Transform3D> py_get_transform();  
  
static std::shared_ptr<Node3D> construct_Node3D(){  
  return Node3D::constructor();  
}  
static std::shared_ptr<Node3D> cast_to_Node3D(Wrapper* pwrapper){  
  return Node3D::cast(pwrapper);  
}  
  
}
}
#endif

And just for your interest, here is the macro LIBRARY_API in the macros.h:

#ifdef _WIN32  
#    ifdef LIBRARY_EXPORTS  
#        define LIBRARY_API __declspec(dllexport)  
#        define FUNCTIONS_API  extern "C" /*__declspec(dllexport)*/  
#    else  
#        define LIBRARY_API __declspec(dllimport)  
#        define FUNCTIONS_API extern "C" /*__declspec(dllimport)*/  
#    endif  
#else  
#    ifdef LIBRARY_EXPORTS  
#        define LIBRARY_API __attribute__((visibility("default")))  
#        define FUNCTIONS_API  extern "C" /*__declspec(dllexport)*/  
#    else  
#        define LIBRARY_API  
#        define FUNCTIONS_API extern "C" /*__declspec(dllimport)*/  
#    endif  
#endif

So as we can see, the LIBRARY_EXPORTS is nothing spactacular. Just some library export stuff for windows and other platforms. An then let's have a look at the class Node3D. We have a constructor for creating a new Node3D new_static is for creating our Node3D from a pointer. We have the cast, which we also use in the Cython equivalent to cast to another type. We have our methods set_transform and py_set_transform. py_set_transform is just our interface we use from Python, to make connecting to C++ a little easier. set_transform has the logic. The same is for get_transform and py_get_transform. Finally, we have construct_Node3D and cast_to_Node3D which are just some helper methods we can use instead of using the class. Which makes using the methods in Python a bit simpler.