Writing Android NDK Code with Elements

With our latest release of Elements 9.1 last week, we introduced support for native Android NDK apps.

What this means is the you can now use whatever Elements language you like best – be it Oxygene, C#, Swift or even Java – to write not just the "regular" JVM/Dalvik-based part of your app, but also any "native" extensions you might want to write with the NDK.

Excursion: 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 part 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, say OpenGL/ES.

In the the past you would have to step down to C or C++ for that, but no more.

NDK with Elements in Action

Let's take a look at adding an NDK extension to an Android app with Elements. For this you need Elements 9.1, and you can work in Fire or in Visual Studio.

First, lets create a regular Android app using the "Android Application" template. You can use any language you like. and I'll show example code for all four languages below. This part of the app will be JVM based, as most Android apps.

The app already contains a MainActivity, and we'll 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.

Our Java app can operate with NDK extensions is via JNI, and the way this works is my simply declaring a placeholder method on ur Java class that be a stand-in for our native implementations. You do this by adding a method declaration as such this to the MainActivity class:

Oxygene

class method HelloFromNDK: String; external;

C#

public static extern string HelloFromNDK();

Swift

public static __external func HelloFromNDK() -> String

Java

public static native string HelloFromNDK()

The external/extern/native keyword 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 via JNI.

That's it. In our regular code (say in onCreate) we can now call this method to get a string back, and then use this string on the java side – say show it as a toast.

But of course we still have to implement the method.

Let's add a second project to our 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".

Right now, this template creates a Static library, but for the NDK extension, we need a dynamic one, so we go into Project Settings and simply change the Output Type from StaticLibrary to Library.

With that out of the way, we can implement our method, which is as simple as creating a new global method and exporting it under there 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:

Oxygene

{$GLOBALS ON}

[JNIExport(ClassName := 'org.me.androidapp.MainActivity')]
method HelloFromNDK(env: ^JNIEnv; this: jobject): jstring;
begin
  result := env^^.NewStringUTF(env, 'Helloooo-oo!');
end;

C#

#pragma globals on

[JNIExport(ClassName = "org.me.androidapp.MainActivity")]
public jsstring HelloFromNDK(JNIEnv *env, jobject thiz)
{
  return (*env)->NewStringUTF(env, "Mr, Jackpots!");
}

Swift

#pragma globals on

@JNIExport(ClassName = "org.me.androidapp.MainActivity")
public func HelloFromNDK(env: UnsafePointer<JNIEnv>!, this: jobject!) -> jstring! {
  return (*(*env)).NewStringUTF(env, "Jade give two rides!")
}

Java

#pragma globals on

@JNIExport(ClassName = "org.me.androidapp.MainActivity")
public jsstring HelloFromNDK(JNIEnv *env, jobject thiz) {
  return (*(*env)).NewStringUTF(env, "Call for help!");
}

A couple things worth noting:

As should be obvious, we're no longer in Java land for this code. This is code that will compile to CPU-ntive ARM or Intel code, and that uses more C-level APIs such as zero-terminated strings, "glibc" and 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 our 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 we can use to instantiate a Java 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, including in this example using the jstring type to let you return a Java level string that the rest of your app can handle.

Build this project, and we're almost set, there's only two little tings left to do:

  1. In the main Android app project, locate the "Android Native Libraries Folder" project setting, and point it to the output folder of the NDK project (it should be the folder that contains the per-archicture subfolders. and will probably be end in "Bin/Debug/Android"

  2. In your java code, somewhere before you first call the NDK function (for example at the start on onCreate), add the following line of code, to load in the native library:

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!