Mick
Member
GM Version: GMS1.4+ (some steps might differ for GMS2, but the idea is the same)
Target platform: Android
Download: N/A
Links: N/A
Note: Updated 2023-01-19 for newer Java versions and 64-bit systems in mind.
Intro
I have recently created a few extensions for Android to play chiptunes in various formats. This required the ability to run native C/C++ code. Native extensions for Android are written in Java, but using JNI (Java Native Interface) you can include C/C++ code in your extension. The caveat to this method is that the native functions can't be used in any Java class, they are tightly connected to the package and class name used when compiling the shared libraries (.so). Here is an attempt to explain how to complete this somewhat advanced task. For this tutorial, I assume that you have your environment setup so you can build java classes (JDK) and Android source (NDK) from the command line.
Tutorial
1. Create a folder for your NDK project, when following this example, name it "HelloNativeAndroid".
2. In that folder create two new folders, name one "bin" and the other "jni".
3. The "jni" folder is where you will put your C / C++ files. In this example we will only use one C file, so create a new file and name it "hello.c". Leave it empty for now.
4. Also in this folder, create another file and name it "Android.mk" (this is similar to makefile on Linux). Paste the following content into the newly created file:
You can read more about how to use the Android.mk file here: https://developer.android.com/ndk/guides/android_mk.html
5. Back in the "HelloNativeAndroid" folder, create a new file named "HelloNativeAndroid.java". This will be the java interface to the native C/C++ functions. Paste the following content into the java file:
6. Open a command prompt in this folder and build the java file with the command: "javac -d bin HelloNativeAndroid.java". You should now see a "HelloNativeAndroid.class" file in the folder "HelloNativeAndroid/bin/com/gamemaker/Tutorial.
7. Now we need to create a jni header file using the java file. This can be done with the following command (from the project root folder): "javac -h HelloNativeAndroid.java". You should now see a newly created header file named "com_gamemaker_Tutorial_HelloNativeAndroid.h".
8. You can now copy the function declarations from the created header file and implement them in the hello.c file we created in step 3. Like so:
9. Now build the shared library with the command: "ndk-build APP_STL=c++_static". If successful, you see two newly created folders, "obj" and "libs". The libs folder should have a couple of subfolders for different architectures with the file "libHelloNativeAndroid.so" in each of them. We will copy some of these subfolders later when creating the GMS Android extension.
10. In your GMS project, right click "Extensions" in the resource tree and click "Create Extension". Give it the name "HelloNativeAndroid" and select "Android" under "Select andy additional features...". Click "Next" and for class name, write "JHelloNativeAndroid". We will use the J prefix here to avoid a conflict with the java interface class we created earlier. For this example we don't need any extra permissions, so click "Create" to create the extension. You can read more about creating extensions for Android here: https://help.yoyogames.com/hc/en-us...ting-A-Native-Extension-For-Android-GMS-v1-3-
11. Right-click the extension in the resource tree and select Add Placeholder/Add Generic. Right-click "HelloNativeAndroid.ext" and click properties. Under "copies to", deselect all exports except "Android" and "Adnroid (YYC)".
12. Add two functions (right-click "HelloNativeAndroid.ext" and select "Add Function"). Double-click each function and set the properties like below. Again, we use the J prefix for the external names to avoid a conflict with the native functions.
13. Right-click "HelloNativeAndroid" and click "Open in Explorer". Go to the folder "HelloNativeAndroid/AndroidSource/Java" and create a new file named "JHelloNativeAndroid.java" with the following content:
14. Copy "HelloNativeAndroid.java" from the NDK project folder into the same folder as "JHelloNativeAndroid.java".
GMS2.x/GMS1.4 gradle: Copy and paste the "libs" folder in the NDK project and rename the copy of the folder to "lib". Create a zip file of the "lib" folder so the file paths are saved in the zip file (lib/..). Change the extension of the zip file to jar and copy it to the libs folder of the GMS extension ("HelloNativeAndroid/AndroidSource/libs"). When building with gradle, the .so files are not added to the .apk if they are just copied to the libs folder like on older versions of GMS1.4.
GMS1.4 pre-gradle: Copy the folders "armeabi", "armeabi-v7a", "mips" and "x86" from the NDK project libs folder to the libs folder of the GMS extension ("HelloNativeAndroid/AndroidSource/libs").
15. To test the extension, create a new object and add the following to the Create event:
For native extensions to work in Android, you cannot just press the play button, you need to create an executable and run that. Set Android as target and create the executable (it should run automatically if you have a device setup). If all goes well you should see a message box on the screen saying "Do Androids Dream of Electric Sheep?".
Passing pointers to C/C++
A reliable method to pass pointers (for buffers etc.) to C/C++ is to convert the pointer to string in GMS and pass it as string to the Android extension, then using the function below, you can convert the string to a long integer in the Java class for the extension. You then pass the pointer as a long integer to the native C/C++ function and cast it to a pointer there.
Target platform: Android
Download: N/A
Links: N/A
Note: Updated 2023-01-19 for newer Java versions and 64-bit systems in mind.
Intro
I have recently created a few extensions for Android to play chiptunes in various formats. This required the ability to run native C/C++ code. Native extensions for Android are written in Java, but using JNI (Java Native Interface) you can include C/C++ code in your extension. The caveat to this method is that the native functions can't be used in any Java class, they are tightly connected to the package and class name used when compiling the shared libraries (.so). Here is an attempt to explain how to complete this somewhat advanced task. For this tutorial, I assume that you have your environment setup so you can build java classes (JDK) and Android source (NDK) from the command line.
Tutorial
1. Create a folder for your NDK project, when following this example, name it "HelloNativeAndroid".
2. In that folder create two new folders, name one "bin" and the other "jni".
3. The "jni" folder is where you will put your C / C++ files. In this example we will only use one C file, so create a new file and name it "hello.c". Leave it empty for now.
4. Also in this folder, create another file and name it "Android.mk" (this is similar to makefile on Linux). Paste the following content into the newly created file:
Code:
# Makefile for building hello.c with Android NDK
# Set local path (An Android.mk file must begin by defining the LOCAL_PATH variable)
LOCAL_PATH := $(call my-dir)
# Clears LOCAL_XXX variables
include $(CLEAR_VARS)
# Tell the compiler that include files can be in the parent folder (for the jni header)
LOCAL_C_INCLUDES := $(LOCAL_PATH)/..
LOCAL_CPP_INCLUDES := $(LOCAL_PATH)/..
# Name of shared library
LOCAL_MODULE := HelloNativeAndroid
# Source file(s)
LOCAL_SRC_FILES := hello.c
# Example: include all C source files
# LOCAL_SRC_FILES := $(notdir $(wildcard $(LOCAL_PATH)/*.c))
# The following command will build the library
include $(BUILD_SHARED_LIBRARY)
5. Back in the "HelloNativeAndroid" folder, create a new file named "HelloNativeAndroid.java". This will be the java interface to the native C/C++ functions. Paste the following content into the java file:
Code:
// In your own projects, change the package name to something else
package com.gamemaker.Tutorial;
// The name of the class must match the name of the file
public class HelloNativeAndroid
{
// Declare the native functions (note the native keyword)
public static native double HelloNativeAndroid_Add(double number1, double number2);
public static native String HelloNativeAndroid_Hello();
// Load the shared library (note that you should not include the .so extension)
static {
System.loadLibrary("HelloNativeAndroid");
}
}
7. Now we need to create a jni header file using the java file. This can be done with the following command (from the project root folder): "javac -h HelloNativeAndroid.java". You should now see a newly created header file named "com_gamemaker_Tutorial_HelloNativeAndroid.h".
8. You can now copy the function declarations from the created header file and implement them in the hello.c file we created in step 3. Like so:
Code:
// Include jni.h, the automatically created jni header file and any other header files required by your code
#include <jni.h>
#include "com_gamemaker_Tutorial_HelloNativeAndroid.h"
const char* _hello = "Do Androids Dream of Electric Sheep?";
// Simple function to add two doubles
double Add(double, double);
// HelloNativeAndroid_Add
JNIEXPORT jdouble JNICALL Java_com_gamemaker_Tutorial_HelloNativeAndroid_HelloNativeAndroid_1Add
(JNIEnv *env, jclass cl, jdouble number1, jdouble number2)
{
return Add(number1, number2);
}
// HelloNativeAndroid_Hello
JNIEXPORT jstring JNICALL Java_com_gamemaker_Tutorial_HelloNativeAndroid_HelloNativeAndroid_1Hello
(JNIEnv *env, jclass cl)
{
// To return a C string, we need to convert it to a jstring like so:
jstring jstrBuf = (*env)->NewStringUTF(env, _hello);
// From C++ you would do it like this:
// jstring jstrBuf = env->NewStringUTF(_hello);
// Return the jstring
return jstrBuf;
}
// Implementation of Add function
double Add(double number1, double number2)
{
return number1 + number2;
}
10. In your GMS project, right click "Extensions" in the resource tree and click "Create Extension". Give it the name "HelloNativeAndroid" and select "Android" under "Select andy additional features...". Click "Next" and for class name, write "JHelloNativeAndroid". We will use the J prefix here to avoid a conflict with the java interface class we created earlier. For this example we don't need any extra permissions, so click "Create" to create the extension. You can read more about creating extensions for Android here: https://help.yoyogames.com/hc/en-us...ting-A-Native-Extension-For-Android-GMS-v1-3-
11. Right-click the extension in the resource tree and select Add Placeholder/Add Generic. Right-click "HelloNativeAndroid.ext" and click properties. Under "copies to", deselect all exports except "Android" and "Adnroid (YYC)".
12. Add two functions (right-click "HelloNativeAndroid.ext" and select "Add Function"). Double-click each function and set the properties like below. Again, we use the J prefix for the external names to avoid a conflict with the native functions.
Code:
Name: HelloNativeAndroid_Add
External Name: JHelloNativeAndroid_Add
Help: HelloNativeAndroid_Add(number1, number2)
Return type: double
Arguments: Add two arguments, type should be double for both
Name: HelloNativeAndroid_Hello
External Name: JHelloNativeAndroid_Hello
Help: HelloNativeAndroid_Hello()
Return type: string
Arguments: None
13. Right-click "HelloNativeAndroid" and click "Open in Explorer". Go to the folder "HelloNativeAndroid/AndroidSource/Java" and create a new file named "JHelloNativeAndroid.java" with the following content:
Code:
package ${YYAndroidPackageName};
import android.util.Log;
import java.io.File;
import java.io.FileReader;
import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.lang.String;
import ${YYAndroidPackageName}.R;
import com.yoyogames.runner.RunnerJNILib;
// Import the native functions declared in HelloNativeAndroid.java
import static com.gamemaker.Tutorial.HelloNativeAndroid.HelloNativeAndroid_Add;
import static com.gamemaker.Tutorial.HelloNativeAndroid.HelloNativeAndroid_Hello;
// Class name need to match the one you used when creating the extension in GMS
public class JHelloNativeAndroid
{
public double JHelloNativeAndroid_Add(double number1, double number2)
{
return HelloNativeAndroid_Add(number1, number2);
}
public double JHelloNativeAndroid_Hello()
{
return HelloNativeAndroid_Hello();
}
}
GMS2.x/GMS1.4 gradle: Copy and paste the "libs" folder in the NDK project and rename the copy of the folder to "lib". Create a zip file of the "lib" folder so the file paths are saved in the zip file (lib/..). Change the extension of the zip file to jar and copy it to the libs folder of the GMS extension ("HelloNativeAndroid/AndroidSource/libs"). When building with gradle, the .so files are not added to the .apk if they are just copied to the libs folder like on older versions of GMS1.4.
15. To test the extension, create a new object and add the following to the Create event:
Code:
show_message(HelloNativeAndroid_Hello());
Passing pointers to C/C++
A reliable method to pass pointers (for buffers etc.) to C/C++ is to convert the pointer to string in GMS and pass it as string to the Android extension, then using the function below, you can convert the string to a long integer in the Java class for the extension. You then pass the pointer as a long integer to the native C/C++ function and cast it to a pointer there.
Code:
// This converts a memory address in string format to a long integer
// It will be cast to a pointer in C++
private long StringAddressToLongPointer(String address)
{
if(address.charAt(1) == 'x')
return Long.parseLong(address.substring(2), 16);
else
return Long.parseLong(address, 16);
}
Last edited: