Using Godot's GDNative on Android
Just recently I started to play with the Godot engine and I really liked it. My first experiment with Godot was a minimalist causal game. It certainly isn't the most advanced 3D graphics game, but it took only a couple of hours to create it and most of the time I spent on creating the graphics assets.
Godot is a complete cross-platform engine similar to Unity 3D with a visual editor and tons of features. However, in comparison to Unity3D, it felt less blown/more lightweight (at least for me). With Godot I finally found a complete engine with the features I needed with open source MIT license. Now I had the power to fix an issue and add a feature without waiting for a vendor to supply an update.
Even though I don't have much experience with graphics/game programming, I've been working on a project in Unity3D. I also like Unity and I feel really productive with the tools it provides, but if it would be 2019 and Godot's C# support would be complete and stable, I would definitely choose Godot over Unity.
For now I really would like to use Godot for my private projects (I will for sure use it for work/clients when C# will be fully supported and stable). My target architecture is mostly Android (and a bit of iOS). Godot provides a relatively easy way of adding platform specific features through a module. However, this usually requires recompiling the complete Godot source code. Recently Godot came up with GDNative. With this new feature it should be possible to plug-in a .so library into the project without touching the source code of Godot. I thought that this is a really nice feature for my customized Android hacks, so I started to play with it.
You can find the code and Godot project that I've used also on github.
Minimal C Example
I followed the tutorial from official docs and created the following code
#define sysv_abi // get rid of some compiler warnings
#include <jni.h>
#include <gdnative_api_struct.gen.h>
#include <android/log.h>
#define LOGI(...) ((void)__android_log_print(ANDROID_LOG_INFO, "gdnative", __VA_ARGS__))
extern "C" {
// Caled when NativeScript instance is created/destroyed
void *android_gdnative_constructor(godot_object *p_instance, void *p_method_data);
void android_gdnative_destructor(godot_object *p_instance, void *p_method_data, void *p_user_data);
// Our custom function
godot_variant android_gdnative_test(godot_object *p_instance, void *p_method_data
, void *p_user_data, int p_num_args, godot_variant **p_args);
// Gives access to various functions from Godot's api
const godot_gdnative_core_api_struct *api = NULL;
const godot_gdnative_ext_nativescript_api_struct *nativescript_api = NULL;
// Called when the native library is loaded
// Here we initialized the previous 2 structs
void GDN_EXPORT godot_gdnative_init(godot_gdnative_init_options *p_options)
{
api = p_options->api_struct;
LOGI("godot_gdnative_init");
// now find our extensions
for (int i = 0; i < api->num_extensions; i++)
{
// LOGI("api type=%d", api->extensions[i]->type);
switch (api->extensions[i]->type)
{
case GDNATIVE_EXT_NATIVESCRIPT:
{
nativescript_api = (godot_gdnative_ext_nativescript_api_struct *)api->extensions[i];
};
break;
default: break;
}
}
}
// Called when native lib is unloaded
void GDN_EXPORT godot_gdnative_terminate(godot_gdnative_terminate_options *p_options)
{
api = NULL;
nativescript_api = NULL;
LOGI("godot_gdnative_terminate");
}
// Used to register our custom classes and methods
void GDN_EXPORT godot_nativescript_init(void *p_handle)
{
LOGI("godot_nativescript_init");
godot_instance_create_func create = { NULL, NULL, NULL };
create.create_func = &android_gdnative_constructor;
godot_instance_destroy_func destroy = { NULL, NULL, NULL };
destroy.destroy_func = &android_gdnative_destructor;
nativescript_api->godot_nativescript_register_class(p_handle, "AndroidGDNative", "Node2D",
create, destroy);
godot_instance_method test = { NULL, NULL, NULL };
test.method = &android_gdnative_test;
godot_method_attributes attributes = { GODOT_METHOD_RPC_MODE_DISABLED };
nativescript_api->godot_nativescript_register_method(p_handle, "AndroidGDNative", "test",
attributes, test);
}
// Our custom data that can be passed automatically between our custom functions,
// constructor, and destructor
struct UserData {
char data[256];
};
// Called after creating a new instance of our registered AndroidGDNative class in GDScript
// Once our class is loaded (load()/preload()), calling new() from GDScript will execute
// this method
void *android_gdnative_constructor(godot_object *p_instance, void *p_method_data)
{
LOGI("android_gdnative_constructor");
return api->godot_alloc(sizeof(UserData));
}
void android_gdnative_destructor(godot_object *p_instance, void *p_method_data, void *p_user_data)
{
LOGI("android_gdnative_destructor");
api->godot_free(p_user_data);
}
// Passes string from native code to GDScript
godot_variant android_gdnative_test(godot_object *p_instance, void *p_method_data
, void *p_user_data, int p_num_args, godot_variant **p_args)
{
godot_string gs = api->godot_string_chars_to_utf8("Hello from GDNative");
godot_variant ret;
api->godot_variant_new_string(&ret, &gs);
api->godot_string_destroy(&gs);
return ret;
}
}
I was using GDNative headers directly from sources from version 3.0.1 and there was no godot_string_new_data() as described in the tutorial. I just quickly looked through the available api in gdnative_api_struct.gen.h and found a godot_string_chars_to_utf8 as an alternative.
Compiling With NDK
You can create a new project in Android Studio and use the usual gradle/cmake way of building native libraries. Then you can just take the .so libraries stored somewhere in [module_name]/build/intermediates/cmake.
I decided to use NDK build scripts because in this case I only needed to create one additional Android.mk file to build the C++ code. I created a folder called jni and placed my gdnative_test.cpp file that contained the previously shown code in there. Inside of the same folder I placed an Android.mk file with the following content
# Android.mk
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := android_gdnative
LOCAL_CPPFLAGS := -std=c++14
LOCAL_CPP_FEATURES := rtti exceptions
LOCAL_LDLIBS := -llog
LOCAL_SRC_FILES := \
gdnative_test.cpp
LOCAL_C_INCLUDES := \
D:/dev/godot/godot/modules/gdnative/include
include $(BUILD_SHARED_LIBRARY)
LOCAL_C_INCLUDES variable points to the place where I store the GDNative headers. To build the .so library, change to directory that contains the jni directory, and execute ndk-build (I'm assuming that you have NDK installed and PATH environment variable properly configured).
Using the .so Library in My Godot Project
I followed the official tutorial and created a GDNativeLibrary and a NativeScript. The GDNativeLibrary resource contains some properties and the actual resource path to the .so library. The resource file has a .gdnlib extension. The NativeScript points to the GDNativeLibrary and can be used in GDScript to load our native library. Both files can be configured from Godot's UI. My files had the following content
android_gdnative.gdnlib
[general]
singleton=false
load_once=true
symbol_prefix="godot_"
reloadable=true
[entry]
Android.armeabi-v7a="res://lib/libandroid_gdnative.so"
[dependencies]
Android.armeabi-v7a=[]
android_gdnative.gdns
[gd_resource type="NativeScript" load_steps=2 format=2]
[ext_resource path="res://lib/android_gdnative.gdnlib" type="GDNativeLibrary" id=1]
[resource]
class_name = "AndroidGDNative"
library = ExtResource( 1 )
_sections_unfolded = [ "Resource" ]
Then from GDScript I used my .so library like this
extends Node2D
onready var agdn = preload("res://lib/android_gdnative.gdns").new()
func _ready():
var msg = agdn.test()
print("Native code returned: " + msg)
Using C++ STL
To add C++ STL support, I created an Application.mk file inside of the jni folder
# Application.mk
APP_STL := c++_shared
When you now run ndk-build, your jni/libs folder will additionally contain libc++_shared.so. You need to copy this file into your Godot project folder together with your other .so library and configure a dependency in godot, so that the libraries can be properly packaged and loaded. The library dependency is configured via GDNativeLibrary resource With this change android_gdnative.gdnlib now contains the following lines
[general]
singleton=false
load_once=true
symbol_prefix="godot_"
reloadable=true
[entry]
Android.armeabi-v7a="res://lib/libandroid_gdnative.so"
[dependencies]
Android.armeabi-v7a=[ "res://lib/libc++_shared.so" ]
You can also link to STL statically by setting APP_STL in Application.mk to c++_static, but if your project contains multiple .so libraries that could potentially all use STL, then this is not recommended.
Looking at godot/platform/android/detect.py
it seems that Godot's binary libgodot_android.so
links with gnustl_static
by default. This could cause the issues mentioned previously!
Using JNI From GDNative
Sometimes it is useful to call Java methods through JNI from native C++ code. For this you usually need at least a pointer to the JNIEnv and ideally some jobject that can be used as a context (should inherit from android.content.Context).
I couldn't find a simple way of getting the JNIEnv pointer. I'm new to Godot, so I possibly overlooked something. I decided to change Godot's source code to get the JNIEnv pointer. Because I was already looking at the sources of the gdnative module, I decided to plug in a new function to the gdnative api. I found a static method TreadAndroid::get_env() inside of platform/android/thread_jandroid.h that Godot is already using to retrieve the JNEnv* for the current thread. I wrapped this method into my godot_android_get_env() function and made it part of gdnative api. I'm not writing that much cross-platform C/C++ code, so I apologize upfront for this little hack. The hack was quick and consisted of the following steps
- I Added android.h to modules/gdnative/include/gdnative/
#ifndef GODOT_GDNATIVE_ANDRIOD_H
#define GODOT_GDNATIVE_ANDRIOD_H
#include <gdnative/gdnative.h>
#ifdef __ANDROID__
#include <jni.h>
#else
using JNIEnv = void;
#endif
#ifdef __cplusplus
extern "C" {
#endif
JNIEnv* GDAPI godot_android_get_env();
#ifdef __cplusplus
}
#endif
#endif
- I Created android.cpp for the implementation in modules/gdnative/gdnative/
#include "gdnative/android.h"
#ifdef __ANDROID__
#include "platform/android/thread_jandroid.h"
#endif
#ifdef __cplusplus
extern "C" {
#endif
JNIEnv* GDAPI godot_android_get_env() {
#ifdef __ANDROID__
return ThreadAndroid::get_env();
#else
return nullptr;
#endif
}
#ifdef __cplusplus
}
#endif
- Changed modules/gdnative/include/gdnative/gdnative.h to include my new android.h header file
...
/////// Android
#include <gdnative/android.h>
...
- Registered my godot_android_get_env() function inside of modules/gdnative/gdnative_api.json. This forces the build system to automatically generate the necessary structures inside of gdnative_api_struct.gen.cpp and gdnative_api_struct.gen.h
...
{
"name": "godot_android_get_env",
"return_type": "JNIEnv*",
"arguments": []
},
...
Now I can use JNEnv inside of my C++ library like this
...
// Passes cache directory path from native code to GDScript
godot_variant android_gdnative_test(godot_object *p_instance, void *p_method_data
, void *p_user_data, int p_num_args, godot_variant **p_args)
{
// Get JNIEnv* from my function that extends godot_gdnative_core_api_struct
JNIEnv* env = api->godot_android_get_env();
// Get context - see https://stackoverflow.com/questions/46869901/how-to-get-the-android-context-instance-when-calling-jni-method
jclass activityThread = env->FindClass("android/app/ActivityThread");
jmethodID currentActivityThread = env->GetStaticMethodID(activityThread, "currentActivityThread", "()Landroid/app/ActivityThread;");
jobject at = env->CallStaticObjectMethod(activityThread, currentActivityThread);
jmethodID getApplication = env->GetMethodID(activityThread, "getApplication", "()Landroid/app/Application;");
jobject context = env->CallObjectMethod(at, getApplication);
// Get path to cache directory
jclass contextClass = env->FindClass("android/content/Context");
jclass fileClass = env->FindClass("java/io/File");
jmethodID getCacheDir = env->GetMethodID(contextClass, "getCacheDir", "()Ljava/io/File;");
jmethodID getAbsolutePath = env->GetMethodID(fileClass, "getAbsolutePath", "()Ljava/lang/String;");
jobject file = env->CallObjectMethod(context, getCacheDir);
jstring str = (jstring)env->CallObjectMethod(file, getAbsolutePath);
const char *cacheDir = env->GetStringUTFChars(str, 0);
// Pass cacheDir to GDScript
...
}
...
However, you still won't be able to use some of the JNI stuff because this code won't run on the main UI thread.
Useful links
https://github.com/GodotNativeTools/godot_headers
https://godotengine.org/article/dlscript-here