Creating an Android NDK Extension
As of Elements 9.1, you can now use Elements for Android development not only using the regular Java based Android SDK, but also build CPU-native Android NDK-based extension for your apps as well.
Background: What is the NDK?
The regular API for writing Android apps is based on the Java Runtime (or Google's variations/evolutions of that, such as Dalvik or ART). Essentially, you write code against Java libraries that compiles to Java byte code. Most parts of most apps are written like that. But Google also offers a Native Development Kit, the NDK, for when you need to write code that either is not efficient enough in Java, needs to talk to lower-level APIs, or use for example OpenGL/ES.
In the the past you would have to step down to C or C++ for that, but now you can use Oxygene, C#, Swift or the Java language to write these extensions, same as you do for the main app.
NDK with Elements in Action
Let's take a look at adding an NDK extension to an Android app with Elements.
First, lets create a regular Android app using the "Android Application" template, for example following the regular First Android App Tutorial. You can use any language you like. This part of the app will be JVM based, as most Android apps.
The app you just created already contains a MainActivity
, and we'll now extend that to call out to the NDK extension we'll write, to do something simple – like obtain a string; – and then show the result.
The Java app can operate with NDK extensions via JNI, and the way this works is by simply declaring a placeholder method on your Java class that acts as a stand-in for the native implementation. You do this by adding a method declaration such as this to the MainActivity class:
class method HelloFromNDK: String; external;
public static extern string HelloFromNDK();
public static __external func HelloFromNDK() -> String
public static native string HelloFromNDK()
<External>
Public Function HelloFromNDK() As String
End Function
The external
/extern
/native
keyword (or the <External>
Aspect for Mercury) will tell the compiler that we'll not be providing an implementation for this method (as part of the Java project), but that it will be loaded in externally (in this case via JNI).
That's it. In your regular code (say in onCreate
) you can now call this method to get a string back, and then use this string on the Java side – for example show it as a toast.
But of course we still have to implement the method.
Let's add a second project to your solution, but this time instead of looking under Java/Cooper, switch over to the Island tab or node in the new Project dialog, and choose the "Android NDK Library" template. Again, pick whatever language you like (it doesn't even have to be the same as the main project). Let's call the project "hello-ndk".
In this second project, you can now implement the native method, which is as simple as adding a new global method and exporting it under the right name.
JNI uses specific rules for that, namely the export name must start with "Java_
", followed by the full name of the Java-level class (with the dots replaced by underscores), and finally the method name itself. So the full name would be something like "Java_org_me_androidapp_MainActivity_HelloFromNDK
".
Luckily, Island provides a nifty aspect called JNIExport
that does the proper name mangling for you:
{$GLOBALS ON}
[JNIExport(ClassName := 'org.me.androidapp.MainActivity')]
method HelloFromNDK(env: ^JNIEnv; this: jobject): jstring;
begin
result := env^^.NewStringUTF(env, 'Helloooo-oo!');
end;
#pragma globals on
[JNIExport(ClassName = "org.me.androidapp.MainActivity")]
public jsstring HelloFromNDK(JNIEnv *env, jobject thiz)
{
return (*env)->NewStringUTF(env, "Mr, Jackpots!");
}
@JNIExport(ClassName = "org.me.androidapp.MainActivity")
public func HelloFromNDK(env: UnsafePointer<JNIEnv>!, this: jobject!) -> jstring! {
return (*(*env)).NewStringUTF(env, "Jade give two rides!")
}
#pragma globals on
@JNIExport(ClassName = "org.me.androidapp.MainActivity")
public jsstring HelloFromNDK(JNIEnv *env, jobject thiz) {
return (*(*env)).NewStringUTF(env, "Call for help!");
}
<JNIExport(ClassName := "org.me.androidapp.MainActivity")>
Public Function HelloFromNDK(env as Ptr(Of JNIEnv), this as jobject) as jstring
Return env.Dereference.Dereference.NewStringUTF(env, "How's Annie?")
End Function
A Couple Things Worth Noting
As should be obvious, we're no longer in Java (as JVM/Dalvik) land for this code. This is code that will compile to CPU-native ARM or Intel code, and that uses more C-level APIs such as zero-terminated strings, "glibc" and a lower-level operating system (Android essentially is Linux, at this level) APIs. Of course you do have full access to Island's object model for writing object oriented code here, and you can use Elements RTL, as well.
Since this code will be called from Java, JNI provides some helper types and parameters to help us interact with the Java runtime. This includes the env
object that you can use to instantiate a Java-level string, and the jstring
type that we use to return said string.
But don't be fooled, we're not writing "Java" code at this point. So inside this method (and throughout the rest of the NDK project, you can go as CPU-native and a bit-twiddly as you'd like, the same as you would do in C/C++ code, without any of the (real or perceived) overhead of the Java runtime.
JNI takes care of marshaling data back and forth as your method gets called.
Making the Connection
Build this project, and we're almost set, there's only two little things left to do:
First, we need to link the two projects together, so that the native extension will get bundled within the Java app.
If you are using EBuild, that is easy: simply add a project reference to the NDK Extension to your main app, for example by dragging the NDK project onto the main project in Fore – the build chain will do the rest.
If you are still using MSBuild/xbuild, locate the "Android Native Libraries Folder" project setting in the main app, and manually point it to the output folder of the NDK project (you will want to use the folder that contains the per-archicture subfolders, e.g. "Bin/Debug/Android
".
Second, in your Java code, somewhere before you first call the NDK function (for example at the start of onCreate
), add the following line of code, to load in the native library:
System.loadLibrary('hello-ndk');
System.loadLibrary("hello-ndk");
System.loadLibrary("hello-ndk")
System.loadLibrary("hello-ndk");
System.loadLibrary("hello-ndk")
And thats it. you can now build both apps, deploy and run the Android app, and see your NDK extension in action!