Apple Worm and Cannon - Godot game 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 GDNativeLibrary configuration UI 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

  1. 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
  1. 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
  1. Changed modules/gdnative/include/gdnative/gdnative.h to include my new android.h header file
...
/////// Android
#include <gdnative/android.h>
...
  1. 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://stackoverflow.com/questions/46869901/how-to-get-the-android-context-instance-when-calling-jni-method

https://godotengine.org/article/dlscript-here

https://godotengine.org/article/look-gdnative-architecture

Next Post

Add a comment

Comments

Hi Mike, I didn't play with the C bindings, but the undefined reference that you've mentioned `nativescript_init()` is defined in `src/core/GodotGlobal.cpp`. That means you need to compile these sources also and link to those. With cmake you would normally do something like this ``` file( GLOB src_files "src/core/*.cpp" ) add_library( godot-cpp-lib SHARED ${src_files} ) ``` And then you would link it to the gdnative code that you've wrote like this ``` target_link_libraries( my-gdnative-lib godot-cpp-lib other-libs...) ```
Written on Sun, 05 Aug 2018 19:09:12 by Roman
Would you be willing to help out with an example how to use godot native with C ? I am trying to achieve it for last couple days and I cant get through ndk-build stage. I am failing at linking stage, not even having any 3rd party code, just trying to compile basic example with GodotScript. I have used many websites but this one is closest to what i have https://gamedevadventures.posthaven.com/using-c-plus-plus-and-gdnative-in-godot-part-1, expect i use cmake not scons to build example. I am ok to run linux version with godotNative, i can add shapes to window and print log messages but on android i get errors when compiling. i get a lot of these, seems like godot is not linked at all /home/mike/GamesProjects/kulkiGodot/plugin/tmp/jni/src/gdlibrary.cpp:20: undefined reference to `godot::Godot::nativescript_init(void*)'
Written on Sat, 04 Aug 2018 18:32:29 by Mike
I've uploaded the project to github - https://github.com/sixo/gdnative_example. Hope that helps.
Written on Fri, 29 Jun 2018 09:48:53 by Roman
Can you be able to tell me what your folder structure looks like? It would be great if this is uploaded via github
Written on Fri, 29 Jun 2018 00:58:42 by vince
Glad to hear you liked my tutorial. That's strange. I tried to change the class name and it worked. What exact error message did you get? Maybe you could try to click on the .gdns file from within Godot editor and change the class name directly through the GUI. The important thing is to click on the save icon below the "Inspector" label (Ctrl S won't work). One additional thing that comes to my mind is to check if you copied the newly compiled .so file to your Godot's project directory. In my case it happened that I forgot to copy the compiled .so after changes to cpp files and I was wondering why things aren't working. Hope this helps.
Written on Thu, 31 May 2018 20:10:57 by Roman
Very good tutorial! If I did it the same way, it works. But if I want to use other class names than AndroidGDNative it does not work anymore. I changed the class name in cpp file and in android_gdnative.gdns. Nevertheless, it does not seem to work.
Written on Thu, 31 May 2018 13:58:25 by Josh