Mixing SDK and NDK

Android app development is split into two, very distinct worlds.

On the one side, there's the Android SDK, which is what the bulk of Android apps is being developed in. The SDK is based on the Java Runtime and the standard Java APIs, and it provides a very high-level development experience. Traditionally, the Java language or Kotlin would be used to develop in this space.

And then there's the Android NDK, which sits at a much lower level and allows to write code directly for the native CPUs (e.g. ARM or x86). This code works against lower-level APIs provided by Android and the underlying Linux operating system that Android is based on, and traditionally one would use a low-level language such as C to write code at this level.

The Java Native Interface, or JNI, allows the two worlds to interact, making it possible for SDK-level JVM code to call NDK-level native functions, and vice versa.

Elements makes it really easy to develop apps that mix SDK and NDK, in several ways:

  1. A shared language for SDK and NDK
  2. Easy bundling, with Project References
  3. Automatic generation of JNI imports
  4. Mixed Mode Debugging

A Shared Language for SDK and NDK

The first part is the most obvious and trivial. Since Elements decouples language form platform, whatever the language of choice is, you can use it to develop both the JVM-based SDK portion of your app and the native NDK part. No need to fall back to a low-level language like C for the native extension.

Easy Bundling of NDK Extensions, with Project References

Once you have an SDK-based app and one or more native extensions in your project, you can bundle the extension(s) into your fina .apk simply adding a conventional Project Reference to them, for example by dragging the extension project onto the app project in Fire or Water.

Even though the two projects are of a completely different type, the EBuild build chain takes care of establishing the appropriate relationship and adding the final NDK binaries into the "JNI" subfolder of your final .apk.

Automatic Generation of JNI Imports

Establishing a project reference to your NDK extension also automatically generates JNI imports for any APIs you expose from our native project. All you need to do is mark your native methods with the JNIExport aspect, as such:

[JNIExport(ClassName := 'com.example.myandroidapp.MainActivity')]
method HelloFromNDK(env: ^JNIEnv; this: jobject): jstring;
begin
  result := env^^.NewStringUTF(env, 'Hello from NDK!');
end;
[JNIExport(ClassName = "com.example.myandroidapp.MainActivity")]
public jstring HelloFromNDK(^JNIEnv env, jobject thiz)
{
  return (**env).NewStringUTF(env, "Hello from NDK!");
}
@JNIExport(ClassName = "com.example.myandroidapp.MainActivity")
public func HelloFromNDK(_ env: ^JNIEnv, _ this: jobject) -> jstring {
  return (**env).NewStringUTF(env, "Hello from NDK!");
}
@JNIExport(ClassName = "com.example.myandroidapp.MainActivity")
public jstring HelloFromNDK(^JNIEnv env, jobject thiz) {
  return (**env).NewStringUTF(env, "Hello from NDK!");
}

As part of the build, the compiler will generate a source file with import stubs for any such APIs, and inject that into your main Android SDK project. That source file will contain Partial Classes (or Extensions, in Swift parlance) matching the namespace and class name you specified.

All you need to do (in Oxygene, C# or Java) is to mark your own implementation of the Activity as partial (__partial in Java, and no action is needed in Swift), and the new methods implemented in your NDK extension will automatically be available to your code:

namespace com.example.myandroidapp;

type
  MainActivity = public partial class(Activity)
  public

    method onCreate(savedInstanceState: Bundle); override;
    begin
      inherited;
      // Set our view from the "main" layout resource
      ContentView := R.layout.main;
      HelloFromNDK;
    end;

  end;

end;
namespace com.example.myandroidapp
{
    public partial class MainActivity : Activity
    {
        public override void onCreate(Bundle savedInstanceState)
        {
            base(savedInstanceState);
            // Set our view from the "main" layout resource
            ContentView = R.layout.main;
            HelloFromNDK();
        }
    }
}
public class MainActivity : Activity {
    override func onCreate(_ savedInstanceState: Bundle) {
        super(savedInstanceState)
        // Set our view from the "main" layout resource
        ContentView = R.layout.main
        HelloFromNDK()
    }
}
package com.example.myandroidapp;

public partial class MainActivity : Activity {
    public override void onCreate(Bundle savedInstanceState) {
        base(savedInstanceState);
        // Set our view from the "main" layout resource
        ContentView = R.layout.main;
        HelloFromNDK();
    }
}

Of course you can use any arbitrary class name in the JNIExport aspect, it does not have to match an existing type in your SDK project. If you do that, rather than becoming available as part of your Activity (or whatever other class), the imported APIs will be on a separate class you can just instantiate.

To see the generated imports, search your build log for "JNI.pas" to get the full path to the file that gets generated and injected into your project. You can also just invoke "Go to Definition" (^⌥D in Fire, Ctrl+Alt+D in Water) on a call to one of the methods, to open the file, as the IDE will treat it as regular part of your project. (The same, by the way, is also true of the R.java file generated by the build that define the R class that gives access to all your resources.)

Mixed Mode Debugging

Finally, Elements allows to debug both your SDK app and its embedded NDK extensions at the same time. You can set breakpoints in both Java and native code, and explore both sides of your app and how they interact.

All of this is controlled by two settings, but Fire and Water, our IDEs, automate the process for you so you don't even have to worry about them yourself.

First, there's the "Support Native Debugging" option in your NDK project. It's enabled by default for the Debug configuration in new projects, and it instructs the build chain to deploy the LLDB debugger library as part of your native library (and have it, in turn, bundled into your .apk). This is what allows the debugger to attach to the NDK portion of your app later.

Secondly, there's the "Debug Engine" option in your SDK project. It defaults to "Java", for JVM-only debugging, but as soon as you add a Project Reference to an NDK extension to your app, it will switch to "Both" (again, only for the Debug configuration), instructing the Elements Debugger to start both JVM and native debug sessions when you launch your app.

This, of course, works both in the Emulator and on the device.

See Also

Note that the video and blog post above were created before the automatic generation of JNI Imports was available, so it still mentions having to define the import manually.