Calling Kotlin from C++

At Highrise we’ve been building the future of our platform in a way that supports Android. We’ve put together a stack that is pretty challenging but really fun to build. It’s based on a C++ core and platform specific projects in Swift (for iOS) and Kotlin (for Android) that uses that core. On Swift you need to have an Objective-C++ wrapper for Swift to call functionality in the C++ core, but it’s pretty straightforward and things like wrapping a block in an std::function works pretty well. Android however runs on the JVM and requires using JNI to communicate with C++. This took away the niceness of Kotlin lambdas for asynchronous completion away from us. Or did it?

I first tried to pass a kotlin lambda to the core and wasn’t able to make it work. My fallback was to create a convention of well-defined callbacks. For example, a call to ‘loginUser’ would result, eventually, in a call to ‘callbackLoginUser’. This required a lot of overhead because of how JNI works. For each callback-prefixed function I had to build an interface that hard-coded the java parameter code, the method, and the class.

 static JniMethodInfo callbackLoginUserMethod() {
     JniMethodInfo callbackInfo;
     JniHelper::getMethodInfo(callbackInfo,
                              "com/app/android/Login",
                              "callbackLoginUser",
                              "(ZLjava/lang/String;)V");
     return callbackInfo;
 }

 static void callbackLoginUser(jobject object, 
 					    	   bool succeeded, 
 					    	   std::string errorMessage) {
     auto info = callbackLoginUserMethod();
     auto errorString = info.env->NewStringUTF(errorMessage.c_str());
     info.env->CallVoidMethod(object, info.methodID, succeeded, errorString);
     info.env->DeleteGlobalRef(object);
 }

These are basically helper methods to hide the boilerplate JNI code behind a simple function call. It works and is easy to reason about since we created a common convention. That said, I couldn’t stop thinking about being able to use kotlin lambdas. Of course, the idea came to me on the drive home.

First thing I thought was since everything in Java (and Kotlin) are objects, a lambda must also be a class that gets generated in the JVM. After a little study, it does!

 val completion: (Boolean, String) -> Unit = { success, value ->
 	println("$value -> $success")
 }

For example that code becomes a class is created with a name like: com.app.android.Login$loginUser$completion$1. And an invoke function with the right params is created: invoke(boolean, java.lang.String). With this knowledge I just had to figure out how to actually call the invoke function, which was pretty clear with all the boilerplate JNI I had written.

First I needed to acquire the name of the class. JNI functions are basically C functions that you need to manage, so the lambda shows up on the C++ side as a jobject. The matching JNI function will receive a jobject instance of the lambda.

 // In Login.kt, identify the kotlin external function that will call to C++
 private external fun nativeLoginUser(completion: ((Boolean, String) -> Unit))
 // In Login.cpp, define the C function that will be matched with the above
 // external function.
 JNIEXPORT
 void Java_com_app_android_Login_nativeLoginUser(JNIEnv *environment, 
 												 jobject thiz, 
 												 jobject completion) {
 	//…
 }

The first task will be to identify the generated class of this completion object.

 JNIEXPORT
 void Java_com_app_android_Login_nativeLoginUser(JNIEnv *environment, 
 												 jobject thiz, 
 												 jobject completion) {
	jclass klass = environment->GetObjectClass(completion);
    jmethodID classMethodId = environment->GetMethodID(klass, 
    												   "getClass", 
    												   "()Ljava/lang/Class;");
    jobject klassObj = environment->CallObjectMethod(completion, classMethodId);

    auto klassObject = environment->GetObjectClass(klassObj);
    auto nameMethodId = environment->GetMethodID(klassObject, 
    	                                         "getName", 
    	                                         "()Ljava/lang/String;");
    jstring classString = (jstring)environment->CallObjectMethod(klassObj, 
    														     nameMethodId);

    auto className = environment->GetStringUTFChars(classString, NULL);

    //…
 }

Now that we know what class we are communicating with, we’ll need to find the invoke method. This part is still a bit hard-coded, unfortunately. You have to provide a param code that matches tha actual method signature. Something like “(ZLjava/lang/String;)V”. This translates Z to Boolean, L to java/lang/String, and V to Void. This matches the signature of the lambda (except for that Unit type, which I don’t fully understand). Using the JniHelper class that comes with Cocos2d-X I can get that method information. Finally I can call the method on the specified object with the vargs of the arguments to send.

 JNIEXPORT
 void Java_com_app_android_Login_nativeLoginUser(JNIEnv *environment, 
 												 jobject thiz, 
 												 jobject completion) {
	// Get `className` (above)

    JniMethodInfo invokeMethod;
    JniHelper::getMethodInfo(invokeMethod,
                             className,
                             "invoke",
                             "(ZLjava/lang/String;)V");

    jstring message = JniHelper::getEnv()->NewStringUTF("Success!");
    invokeMethod.env->CallVoidMethod(completion, 
    								 invokeMethod.methodID, 
    								 true, 
    								 message);
    environment->ReleaseStringUTFChars(classNameString, className);
 }

Finally, in Kotlin I can simply add a new function that calls the native/external C++ function with the lambda.

 private external fun nativeLoginUser(completion: ((Boolean, String) -> Unit))
 fun loginUser() {
    nativeLoginUser({ success, value ->
        if (success) {
            println("Yes!")
        }
        println(value)
    })
 }

There is a lot I didn’t cover here like releasing objects properly and getting a global reference to objects that you may need to wait for asynchronous callback. That’s all JNI specific stuff and it, unfortunately, takes a lot of trial and error to work out. I’m also not totally happy with the way the param code is hard coded. I think I could probably generate the param string from the Kotlin side and pass that with the block. It’s not ideal but still better than the convention-based method. Have you done this? I’d love to hear your experiences!