GDNative and CMake code-first approach
Similar to an article I wrote not so long ago I wanted to explore GDNative even further. In the process, I automated the boring parts of the process using python and created a free open-source template that you can use now at your disposal on Github.
You can find my previous article on GDNative here. And if you want to skip ahead and use the template directly – you can access it through this link.
Table of Contents
- About python and automation
- What do we need to be automated for GDNative?
- How to structure the project
- How generating files work
- Conclusion
About python and automation
In programming and any kind of development, there are always ways of automation. There is a lot of boring stuff in our workflow. Some of them are skippable and some are not. For example, creating files is part of the process but if the files always follow some kind of structure and setup it is best to automate it. It will save you time and effort to work on better stuff like your game or functionality. My latest article was on CI/CD which is all about automating the build and deployment of your game.
In the future, I will give even more advice on automating the boring stuff around the projects – like code generation from shaders or other custom game files. I’ve already touched upon some tool building before and even how to run python scripts using CMake. Take a look at that article because it will have a lot of common with this one.
Python is a really useful language because the scripts are available and will work pretty much on any platform with python installed. They are easy to write and read. This is also why Godot uses a similar scripting language and why the Godot build system is entirely based on python. I like python but I also like CMake and nobody ever said that the two of them are not compatible. This is why I choose a combination of the two to generate my project files and some of my source files.
What do we need to be automated for GDNative?
If you’ve read my previous article on GDNative or have worked with it for a while you will know that one of the most boring stuff is creating the files. If you create a new class you would have a few steps that you need to repeat every time:
- Create header and source files
- Write the contents of the headers and source files (include Godot headers, macros, inheritance)
- Add the class to the Godot library initialization function
- Add the files to CMake
- Create a file for the Godot project (.gdns).
- Name them with the class name accordingly.
- Link the Godot files to the native library file (.gdnlib)
And that is for every class. That can get boring and tedious to do every time. To make that easy I wrote a script that takes only two values – the new class name and the class name from Godot that it inherits from. Then the script will create a generated include file that will contain two macros for the class definition. Then if not already existing it will also auto-create a header and a source file (that you can later modify). It will also create a generated macro for registering the class in the nativescript_init function. Then it will create the required “.gdns” file inside the Godot project folder.
How to structure the project
I will keep it high level here. If you’re interested or can’t follow through check either the template repository linked at the beginning of this article or one of my other articles on how to structure a CMake project.
The project structure is simple. Your code setup comes first with the CMakeLists.txt in the root folder. There are subfolders for splitting your project into manageable chunks. I also split the CMake configurations in a CMake directory to keep it manageable. Then comes the script directory which is responsible for handling the automation stuff. Emsdk directory is also there if compiling for the web is important to you. The last thing is that I also made a script to download the Godot version for the current system that is compatible with the current project. Godot-cpp files are also included as a subdirectory.
The Godot project itself is in a folder called project. It will get all the generated compiled libraries in a subfolder that would be in the reach of the Godot res:// protocol.
Because everything is managed under git I’ve gitignored all unrelevant parts.
Generation
For the generation itself, I’ve decided to follow the structure that godot-cpp uses. That is to have the include directory and inside it have a gen directory for all generated files. I also keep the templates for file generation under “scripts/templates”.
How generating files work
I will provide sections of the script here piece by piece. The first thing I want to note is that I aimed at having only core python packages in the script. I am aware that there are more convenient utility packages to use for some of the things.
At the high level for generating a new class, I would only need its name and the name of the class it inherits from. I wanted to make it as easy as possible to specify those in a file so I used the “.ini” format. This format has sections in the form of “[section]” and key-value pairs under them in the form of “class_name = parent_name”. I put this into each subproject folder. Then what I need to do is run a python generation script on every build. The following demonstrates my CMake setup:
execute_process(
COMMAND ${PYTHON_EXECUTABLE} ./scripts/get_generated_sources.py "${CMAKE_CURRENT_SOURCE_DIR}/gdnative_scripts.ini" "${CMAKE_CURRENT_SOURCE_DIR}"
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
OUTPUT_VARIABLE "${PROJECT_NAME}_GENERATED_SOURCES"
)
message(STATUS "${${PROJECT_NAME}_GENERATED_SOURCES}")
add_custom_target("gen_gdnative_interface_${PROJECT_NAME}" ALL
SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/gdnative_scripts.ini
DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/gdnative_scripts.ini
BYPRODUCTS ${${PROJECT_NAME}_GENERATED_SOURCES}
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
COMMAND ${PYTHON_EXECUTABLE} ./scripts/generate_gdnative_project_files.py "${CMAKE_CURRENT_SOURCE_DIR}/gdnative_scripts.ini" "${CMAKE_CURRENT_SOURCE_DIR}" ${PROJECT_NAME}
COMMENT "Generating GDNative interface"
)
You will notice there are two scripts out there. The first one is to print all the files that will get generated so that we can specify them in the byproducts of our build target. Execute process will be run on CMake generation. Custom target will be run on the build. I decided to give three values to the python script – the “.ini” file, the current directory (for generation), and the project name.
In the python file itself, I parse the “.ini” file and get some templates. I use the standard python templating library which will replace all values in a file that are defined like this “${variable}” with the variable provided through a dictionary. There are no functions for templating available so I decided to also pass upper and lower case variables when needed. Then for each file, I generate headers that would contain the opening and closing macro for the class:
#ifndef ${upper_project_name}_${upper_class_name}_GEN_H
#define ${upper_project_name}_${upper_class_name}_GEN_H
#define CLASS_${upper_class_name}_START class ${class_name} : public ${parent_name}\
{\
private:\
GODOT_CLASS(${class_name}, ${parent_name})\
public:\
void _init() {};
#define CLASS_${upper_class_name}_END };
#endif //${upper_project_name}_${upper_class_name}_GEN_H
That is then used in a header file that uses the following template for generation:
#ifndef ${upper_project_name}_${upper_class_name}_H
#define ${upper_project_name}_${upper_class_name}_H
#include <gen/${lower_class_name}.gen.h>
#include <Godot.hpp>
#include <${parent_name}.hpp>
namespace godot
{
CLASS_${upper_class_name}_START
public:
${class_name}();
virtual ~${class_name}();
static void _register_methods();
void _ready();
void _process();
CLASS_${upper_class_name}_END
}
#endif //${upper_project_name}_${upper_class_name}_H
It is a bit ugly but it is the best way to work with that. This is the closest way to achieve C#’s partial classes. I can define whatever I want in the generated header’s macro and it will become part of the class.
The next important step is to add the class to register itself with Godot on GDNative initialization. To do this I create a macro for having all the class being registered I concatenate all the files listed in the init and generate the code “register_class<${classname}>();”. I also add the include files for them because else the macro won’t just work. I put all of them in a macro like this:
#ifndef ${upper_project_name}_LIBRARY_GEN_H
#define ${upper_project_name}_LIBRARY_GEN_H
#define ${upper_project_name}_CLASS_REGISTRATION ${commands}
${includes}
#endif //${upper_project_name}_LIBRARY_GEN_H
This is then to be used in the native_script_init funciton:
extern "C" void GDN_EXPORT ${lower_project_name}_nativescript_init(void* handle)
{
Godot::nativescript_init(handle);
${upper_project_name}_CLASS_REGISTRATION
}
Compared to the previous steps, generating the “.gdns” files is almost a piece of cake. It just needs to specify the class name and path to the “.gdnlib” file:
[gd_resource type="NativeScript" load_steps=2 format=2]
[ext_resource path="res://${lower_project_name}/${lower_project_name}_native.gdnlib" type="GDNativeLibrary" id=1]
[resource]
resource_name = "${lower_class_name}"
class_name = "${class_name}"
library = ExtResource( 1 )
The python script for doing all this looks kind of like this:
for line in config["normal"]:
gen_file_path = gen_root / f"{line.lower()}.gen.h"
gdns_file_path = project_root / f"{line.lower()}.gdns"
# Replace values for inside the tempaltes
model = {
"class_name" : line,
"parent_name" : config["normal"][line],
"upper_class_name" : line.upper(),
"lower_class_name" : line.lower(),
"upper_project_name" : sys.argv[3].upper(),
"lower_project_name": sys.argv[3].lower(),
}
# Used to register the class later in
normal_registration_commands.append(f"register_class<{line}>();")
# Generate safely the .gen.h file
gen_file_path.touch()
gen_file_path.write_text(gen_template_string.safe_substitute(model))
# Generate the GDNS file
gdns_file_path.touch()
gdns_file_path.write_text(gdns_template_string.safe_substitute(model))
# Used later to include in the native init
includes.append(f"#include <{line.lower()}.h>\n")
# Generate these only if they aren't there
include_file_path = include_root / f"{line.lower()}.h"
if not include_file_path.exists():
include_file_path.touch()
include_file_path.write_text(header_template_string.safe_substitute(model))
src_file_path = src_root / f"{line.lower()}.cpp"
if not src_file_path.exists():
src_file_path.touch()
src_file_path.write_text(source_template_string.safe_substitute(model))
commands = "".join(normal_registration_commands)
includes = "".join(includes)
model = {
"upper_project_name": sys.argv[3].upper(),
"lower_project_name": sys.argv[3].lower(),
"commands": commands,
"includes": includes,
}
gen_library_file_path = gen_root / "library.gen.h"
gen_library_file_path.touch()
gen_library_file_path.write_text(library_gen_template_string.safe_substitute(model))
library_file_path = src_root / "library.cpp"
if not library_file_path.exists():
library_file_path.touch()
library_file_path.write_text(library_template_string.safe_substitute(model))
Conclusion
I really wanted to touch on the subject of code generation. It is useful to know for every programmer and makes your life way easier. It will also close the gap a bit for all C/C++ developers that want to work with Godot engine on a lower and more performant level.
Check my template repository and do not hesitate to write a comment or contact me directly if you need help with the setup.
If you want to leanr how to configure these kinds of projects easily you can also check out my CMake course on Udemy.
Leave a comment