A simple way to handle callbacks with Android and swig
Why can't we just use one programming language?
Google wants us to implement our mobile apps in Java or Kotlin. If we do that, we get all the benefits of the Android platform at our fingertips. And Google traps us and our users in their walleted garden. Apple wants us to implement our mobile apps in Objective C or Swift - for the same reason. I don't think it's a coincident that all major platforms use not only proprietary API's for the user interface and system functions, but even different languages. Microsoft is no different, if you develop for Windows you must use C# (or suffer severe pain).
Despite the well intended gardening from Google, Apple (and Microsoft), their approach is not successful. Any app, exclusively available only for IOS, Android(or Windows), will fail to dominate it's niche. An app-company locked to one platform cannot win. (HTML5 and javascript has failed fundamentally to solve this problem). So all companies and even one man app shops with ambitions, will aim to launch their app for both mobile platforms, and then often for macOS and Windows too.
It's expensive (and ridiculous) to support different code bases for doing the same thing. Therefore, a common solution for many companies is to write most of the app in C++, and just implement the user interface and specific system interfaces in the language provided by Google, Apple and Microsoft. QT is in my opinion a better solution, but it's expensive to license QT for commercial use, and it's hard to make a QT app look and behave like a "native" IOS or Android app.
So, a lot of people look to C++ to solve their problem. C++ is fast (much faster than Java, Kotlin or C#), reliable, more mature than Objective C, and today quite modern and totally able to solve most problems.
Simplified Wrapper and Interface Generator - or just SWIG
The biggest problem with the C++ approach is that C++ libraries cannot be called directly from Java/Kotlin, C# or Objective C. So a typical mainstream app today, have three layers.
Let's take a closer look at how this is done with Android.
The wrapper layer must be aware of the details on how to translate between C++ and the local language, in this case Java for Android. The irony lurking in the details here is that Java cannot exist alone. Android's Java/Kotlin runtime, ART, is written in C++! So even when Google forces us to use Java or Kotlin for our apps, they use C++ to run it. Still, they have not provided a simple way to integrate the two layers.
Writing the wrapper by hand is possible, but it's probably not something you want to do. Even the simplest things looks scary, and it's very, very hard to get it right on a project with some complexity.
Example, handwritten c code that can be called to from Java
1#include <string.h>
2#include <jni.h>
3
4/* This is a trivial JNI example where we use a native method
5 * to return a new VM String. See the corresponding Java source
6 * file located at:
7 *
8 * hello-jni/app/src/main/java/com/example/hellojni/HelloJni.java
9 */
10JNIEXPORT jstring JNICALL
11Java_com_example_hellojni_HelloJni_stringFromJNI( JNIEnv* env,
12 jobject thiz )
13{
14#if defined(__arm__)
15 #if defined(__ARM_ARCH_7A__)
16 #if defined(__ARM_NEON__)
17 #if defined(__ARM_PCS_VFP)
18 #define ABI "armeabi-v7a/NEON (hard-float)"
19 #else
20 #define ABI "armeabi-v7a/NEON"
21 #endif
22 #else
23 #if defined(__ARM_PCS_VFP)
24 #define ABI "armeabi-v7a (hard-float)"
25 #else
26 #define ABI "armeabi-v7a"
27 #endif
28 #endif
29 #else
30 #define ABI "armeabi"
31 #endif
32#elif defined(__i386__)
33#define ABI "x86"
34#elif defined(__x86_64__)
35#define ABI "x86_64"
36#elif defined(__mips64) /* mips64el-* toolchain defines __mips__ too */
37#define ABI "mips64"
38#elif defined(__mips__)
39#define ABI "mips"
40#elif defined(__aarch64__)
41#define ABI "arm64-v8a"
42#else
43#define ABI "unknown"
44#endif
45
46 return (*env)->NewStringUTF(env, "Hello from JNI ! Compiled with ABI " ABI ".");
47}
All projects I have worked on use an ancient code generator, swig to generate the wrapper layer.
Swig is a monster. Last time I used it, I estimated that it would take me 2 - 4 weeks of reading and experimenting to get a reasonable understanding of how to use it correctly. And I am one who already understands most of the the principles it builds upon. Allowing Java developers to use swig to wrap "some library", by copying and pasting 10 year old snippets they don't understand from Stackoverflow is crazy. Especially now, when everything is Agile, and we don't have time to truly comprehend anything.
Just getting the memory management models in C++ and Java to work together is hard, and swig can only make some rough guesses about what C++ objects to "own" (and release) and what C++ objects to leave alone. For example, if Java get an object from a factory in C++, who will release it? If you call a C++ method with some Java object, how can the Java garbage collector later on know if it is referenced or not in the C++ library?
Interface file and simple wrappers
Doing simple things with swig is relative simple. For example, If we have this C++ class:
1// Foo.h
2class Foo {
3public:
4 static int getAnswer();
5 std::string quoteMe(const std::string& quote);
6};
We can create a very simple swig interface file to tell swig what to do.
/* Foo.i */
%module Foo_Wrapper
/* Required include files */
%{
#include "Foo.h"
%}
/* C++ Standard library wrappers */
%include "std_string.i"
/* Our native headers */
%include "Foo.h"
This will let us use code like:
1public class MainActivity extends AppCompatActivity {
2
3 static {
4 System.loadLibrary("Foo_Wrapper");
5 }
6
7 Foo foo = new Foo();
8
9
10 @Override
11 protected void onCreate(Bundle savedInstanceState) {
12
13 ...
14
15 // Example of a call to a native method
16 TextView tv = (TextView) findViewById(R.id.editText);
17 tv.setText("The answer to * is " + String.valueOf(Foo.getAnswer()));
18
19 // Example, send a text string and display the result.
20 // quoteMe() is a normal instance method that return a value.
21 tv.append(Foo.quoteMe("\nThanks for all the fish"));
22 }
23}
24
The System.loadLibrary() method will load a shared library with our wrapper, and our Foo class, compiled for the architecture we run Android on.
Callbacks
So, in this case we create one instance of Foo, we keep it referenced as a variable in the main activity class (to avoid any garbage collection or memory leak issues), and conversion between simple types line integers and (a little more complex) strings works.
What about callbacks? After C++11, I use std::function<> and lambdas a lot to deal with callbacks in asynchronous code.
What if we want to call, for example, a REST API library written in C++ from Java?
Let us re-factor our previous example into a REST library that can POST Json payloads.
1#include <string>
2#include <functional>
3
4namespace restapi {
5
6 class NativeRestApi {
7 public:
8
9 using completion_t = std::function<void (int httpCode, const std::string& body)>;
10
11 static int getAnswer();
12
13 std::string quoteMe(const std::string& quote);
14
15 void sendPostRequest(const std::string& url,
16 const std::string jsonObject,
17 completion_t completion);
18
19 };
20
21} // namespace
22
This is a pretty standard pattern today, with modern C++.
So, how do we call sendPostRequest() from Java, and pass it a completion function that can call back to Java when the request is completed?
Swig has no idea what to do with our C++ completion_t type.
But swig has a concept about directors.
A director is a virtual class you define in C++, where swig creates Java overrides of the virtual methods. In our example, we call onComplete() in C++, and in Java, there will be a Java class overriding it, and it's onComplete() method will be called. It's not exactly what I would have preferred - I like lambdas - but it's good enough.
In a project I work on, I ran into this exact problem last week. Swig has the concept of directors, and Internet is full of examples on how you can use it, verbosely. You just create a bunch of new files in your project, copy & paste some 10 year old blocks of code, and we are set, right?
Well, not me.
I decided that I wanted exactly 0 new files for my wrapper, except for the swig interface file. So after two days reading and experimenting, I was able to find a distilled solution, that only require a small helper class, defined in the swig interface file itself.
Updated swig interface file:
%module(directors="1") NativeRestApi_Wrapper
/* Required include files */
%{
#include "NativeRestApi.h"
%}
/* C++ Standard library wrappers */
%include "std_string.i"
/* Our native headers */
%include "NativeRestApi.h"
/* C++ std::function<> callback support */
%feature("director") RequestCompletion;
%inline %{
class RequestCompletion {
public:
RequestCompletion() = default;
virtual ~RequestCompletion() = default;
/* This method will be called to in Java World by the overidden Java implementation */
virtual void onComplete(int httpCode, const std::string& body) = 0;
/* This method will be used from Java World to create a std::function<> object to the
* callback in our native API
*/
restapi::NativeRestApi::completion_t createWrapper() {
return [this](int httpCode, const std::string& body) -> void {
onComplete(httpCode, body);
};
}
};
%}
There are a few things to notice.
- The %module definition need the (directors="1") parameter to enable this feature.
- We must add "%feature("director") RequestCompletion;" before we define the completion class.
- The callback method is pure virtual.
- The code-block for RequestCompletion is %inline.
The RequestCompletion class has two responsibilities:
- It creates a C++ completion_t instance that Java can obtain, and then pass to sendPostRequest. That means that we don't need any more abstraction to provide the callback to C++. We can call sendPostRequest directly in Java.
- It is an instance of a director hybrid class, that we will derive a Java class from and provide just an override of onComplete().
The Java version of RequestCompletion:
1class Completion extends RequestCompletion {
2
3 Completion() {
4 super();
5 }
6
7 @Override
8 public void onComplete(int httpCode, String body) {
9 ; // Some logic to handle the event;
10 }
11}
And, finally, some Java-code to POST some Json
1class someCodeToPostJson {
2 NativeRestApi restApi = new NativeRestApi();
3 Completion completion = new Completion();
4
5 void postJson(String json) {
6
7 // Initiate an async 'rest' request with a completion
8 restApi.sendPostRequest("https://api.example.com/v1/testme",
9 json, completion.createWrapper());
10 }
11}
That's all!
There is one potential problem, though: Memory management. Swig is smart enough to understand that something is going on with createWrapper() and sendPostRequest(), so it prevents the garbage collector from cleaning any instance of Completion that has been used. This is kind of swig, because if it did not, the C++ code would never have known when the callback was valid, and when it would crash our app. It does however create a potential memory leak. The simplest way to deal with this is to keep an instance of Completion available as a member variable or static member variable, in stead of rapidly creating new instances.
How to use swig with Android Studio
I found a really nice article about this here: Android NDK in Android Studio with SWIG.
In short, what I did was to create a new project in Android Studio 3, checked the C++ box, and then exceptions and rtt (I like those), and then I deleted the C++ files that Android Studio made for me, and made my own. I also modified the cmakelists file to handle swig, and added one section to the app's gradle.build script.
app/build.gradle
1project.afterEvaluate {
2 javaPreCompileDebug.dependsOn externalNativeBuildDebug
3}
4
Without this block, the build fails before the C++ wrapper layer is created.
A Real App
I have made a very simple app that shows how this all works together. The main view is just Android Studios skeleton code, with a multi-line TextView where I append the output of the interactions of the NativeRestApi methods. In my implementation, I start a std::thread for each request to sendPostRequest, sleep for 3 seconds, and then call the callback with the "result".
The C++ header and swig interface are shown above this section:
C++ implementation of NativeRestApi:
1#include <chrono>
2#include <thread>
3
4#include "NativeRestApi.h"
5
6namespace restapi {
7
8 int NativeRestApi::getAnswer() {
9 return 42;
10 }
11
12 std::string NativeRestApi::quoteMe(const std::string "e) {
13 return std::string("'") + quote + "'";
14 }
15
16 void NativeRestApi::sendPostRequest(const std::string &url,
17 const std::string jsonObject,
18 NativeRestApi::completion_t completion) {
19
20 std::thread{([completion{move(completion)}] {
21 std::this_thread::sleep_for(std::chrono::seconds(3));
22 if (completion) {
23 completion(200, R"({"object" : "something", "success" : true})");
24 }
25
26 })}.detach();
27 }
28
29} // namespace
The CMakeLists.txt file:
1cmake_minimum_required(VERSION 3.4.1)
2
3set(SWIG_I_FILE "src/main/cpp/NativeRestApi.i")
4set(JAVA_GEN_PACKAGE "eu.lastviking.core")
5string(REPLACE "." "/" JAVA_GEN_SUBDIR ${JAVA_GEN_PACKAGE})
6set(JAVA_GEN_DIR ${Project_SOURCE_DIR}/src/main/java/${JAVA_GEN_SUBDIR})
7
8include_directories(src/main/cpp)
9
10find_package(SWIG REQUIRED)
11include(${SWIG_USE_FILE})
12
13# Remove old java files, in case we don't need to generate some of them anymore
14file(REMOVE_RECURSE ${JAVA_GEN_DIR})
15
16# Ensure file recognized as C++ (otherwise, exported as C file)
17set_property(SOURCE src/main/cpp/NativeRestApi.i PROPERTY CPLUSPLUS ON)
18
19# Setup SWIG flags and locations
20set(CMAKE_SWIG_FLAGS -c++ -package ${JAVA_GEN_PACKAGE})
21set(CMAKE_SWIG_OUTDIR ${JAVA_GEN_DIR})
22
23# Export a wrapper file to Java, and link with the created C++ library
24swig_add_module(NativeRestApi_Wrapper java ${SWIG_I_FILE})
25swig_link_libraries(NativeRestApi_Wrapper NativeRestApi)
26
27add_library( # Sets the name of the library.
28 NativeRestApi
29
30 # Sets the library as a shared library.
31 SHARED
32
33 # Provides a relative path to your source file(s).
34 src/main/cpp/NativeRestApi.cpp
35 )
The Java code
1package eu.lastviking.testswig;
2
3import android.os.Bundle;
4import android.widget.TextView;
5import android.support.design.widget.FloatingActionButton;
6import android.support.design.widget.Snackbar;
7import android.support.v7.app.AppCompatActivity;
8import android.support.v7.widget.Toolbar;
9import android.view.View;
10import android.view.Menu;
11import android.view.MenuItem;
12
13import eu.lastviking.core.NativeRestApi;
14import eu.lastviking.core.RequestCompletion;
15
16public class MainActivity extends AppCompatActivity {
17
18 static {
19 System.loadLibrary("NativeRestApi_Wrapper");
20 }
21
22 NativeRestApi restApi = new NativeRestApi();
23 Completion completion = new Completion();
24
25 class Completion extends RequestCompletion {
26
27 Completion() {
28 super();
29 }
30
31 @Override
32 public void onComplete(int httpCode, String body) {
33 runOnUiThread(() -> {
34 TextView tv = (TextView) findViewById(R.id.editText);
35 tv.append("\nRest result: "
36 + String.valueOf(httpCode)
37 + " "
38 + body);
39 });
40 }
41 }
42
43 @Override
44 protected void onCreate(Bundle savedInstanceState) {
45 super.onCreate(savedInstanceState);
46 setContentView(R.layout.activity_main);
47 Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
48 setSupportActionBar(toolbar);
49
50 FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);
51 fab.setOnClickListener(new View.OnClickListener() {
52 @Override
53 public void onClick(View view) {
54 // Initiate an async 'rest' request with a completion
55 restApi.sendPostRequest("https://api.example.com/v1/testme",
56 "{}", completion.createWrapper());
57 }
58 });
59
60 // Example of a call to a native method
61 TextView tv = (TextView) findViewById(R.id.editText);
62 tv.setText("The answer to * is " + String.valueOf(NativeRestApi.getAnswer()));
63
64 // Example, send a text string and display the result.
65 // quoteMe() is a normal instance method that return a value.
66 tv.append(restApi.quoteMe("\nThanks for all the fish"));
67
68 // Initiate an async 'rest' request with a completion
69 restApi.sendPostRequest("https://api.example.com/v1/testme",
70 "{}", completion.createWrapper());
71 }
72
73 @Override
74 public boolean onCreateOptionsMenu(Menu menu) {
75 // Inflate the menu; this adds items to the action bar if it is present.
76 getMenuInflater().inflate(R.menu.menu_main, menu);
77 return true;
78 }
79
80 @Override
81 public boolean onOptionsItemSelected(MenuItem item) {
82 // Handle action bar item clicks here. The action bar will
83 // automatically handle clicks on the Home/Up button, so long
84 // as you specify a parent activity in AndroidManifest.xml.
85 int id = item.getItemId();
86
87 //noinspection SimplifiableIfStatement
88 if (id == R.id.action_settings) {
89 return true;
90 }
91
92 return super.onOptionsItemSelected(item);
93 }
94}
95
The complete source code is available on github: SwigDirectorTest