-
Notifications
You must be signed in to change notification settings - Fork 8
How does py4godot work?
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.
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
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
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.py
and generation_files/generate_classes_pxd.py
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
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.
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.
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_ptr
s 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.