How to run C++ code from a native Android or iOS app

Matthieu Regnauld
10 min readMar 8, 2023

--

When it comes to real-time performances for some specific tasks, using some C++ code in your mobile app might be a good idea and sometimes even necessary. You may also need to run some C++ code because some third-party libraries are only available in C++ and you want to interface with them.

A good example of that is real-time audio processing, that often needs to be done in C++ (check my article about that here).

OK, fine. But how do I implement it?

“I don’t need sleep. I need answers.”

Let’s see a very basic example, first with Android and then with iOS. Starting from now, we assume that for both apps, we use com.juceaudio.juceaudiodemo as the package name (or bundle identifier).

How to call a simple C++ function on Android

In order to call a C++ function from any Kotlin class, we need first to implement quite a few things by following this steps:

STEP 1: In your app/build.gradle file, add the following blocks:

android {
...
defaultConfig {
...

// add this:
externalNativeBuild {
cmake {
version = "3.24.1"
abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64'
arguments "-DANDROID_STL=c++_shared"
}
}
}

// add also this:
externalNativeBuild {
cmake {
path 'CMakeLists.txt'
}
}
...
}

STEP 2: In the app/ directory, create a new file named CMakeLists.txt:

cmake_minimum_required(VERSION 3.18.1)
set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -Ofast")

add_library( # Sets the name of the library.
native-lib

# Sets the library as a shared library.
SHARED

# Provides a relative path to your source file(s).
# This is where you'll add all your C++ source files:
src/main/cpp/native-lib.cpp
)

find_library( # Sets the name of the path variable.
log-lib

# Specifies the name of the NDK library that
# you want CMake to locate.
log)

target_link_libraries( # Specifies the target library.
native-lib

${log-lib})

Starting from now, if there are some compilations error, please do not panic!

STEP 3: In app/src/main/, create a directory named cpp, and then create your first C++ file named native-lib.cpp in it. This file will be the interface between your Kotlin code (or Java if you still live in the past) and your C++ classes:

#include <jni.h>
#include <string>

extern "C" JNIEXPORT jstring JNICALL Java_com_juceaudio_juceaudiodemo_NativeManager_nativeTest(
JNIEnv *env, jclass, jstring originalText)
{
const char *cOriginalText = env->GetStringUTFChars(originalText, JNI_FALSE);
std::string sOriginalText(cOriginalText);
env->ReleaseStringUTFChars(originalText, cOriginalText);

std::string sFinalText = sOriginalText + " " + sOriginalText;
return env->NewStringUTF(sFinalText.c_str());
}

The naming of the function above must respect a specific rule here:

  • each word of the function name is separated by an underscore
  • the name always starts with Java, even in a Kotlin project
  • then followed by the package name of the app
  • then followed by the name you want to give to your function (here, I named it nativeTest)

You’ll also find the first two arguments: JNIEnv *env and jclass.

The first argument is useful for example for converting Kotlin types into their equivalent in C and vice versa.

The second argument can be either jclass or jobject. jclass is a reference to the Kotlin class that will call our nativeTest function (more about that below), which means that on the Kotlin side, our function will be declared as static. jobject on the other side is a reference to the Kotlin instance of the class that will call our nativeTest function, which means that on the Kotlin side, our function will be an instance method.

STEP 4: In app/src/main/kotlin/com/juceaudio/juceaudiodemo/, create a Kotlin class named NativeManager. This class is not mandatory, you can call your C++ functions from pretty much anywhere, but I personally prefer to create a specific class dedicated to calling those functions:

package com.juceaudio.juceaudiodemo

class NativeManager
{
companion object
{
init
{
// the try/catch is useful if you want to mock NativeManager for unit tests:
try
{
System.loadLibrary("native-lib")
}
catch (e: UnsatisfiedLinkError) {}
}

// NDK functions, here declared as static:
@JvmStatic private external fun nativeTest(originalText: String): String
}

fun test(originalText: String): String = nativeTest(originalText)
}

Sometimes, Android Studio can be moody at this point and still show errors. Don’t hesitate then to invalidate the caches and restart it (File > Invalidate Caches…).

STEP 5: This is it! Now you can call your C++ code from anywhere:

fun onClick(view: View)
{
val nativeManager = NativeManager()
val finalMessage = nativeManager.test("Message")

// should print: "Message Message":
Log.d("TAG", finalMessage)
}

How to call a simple C++ function on iOS

In order to call a C++ function from any Swift class, we need first to implement quite a few things by following this steps:

STEP 1: Create a new group named Native, then create two new groups in it : Cpp and ObjC.

STEP 2: In Native/Cpp/, create a file named NativeLib.cpp:

#include "NativeLib.hpp"

std::string NativeLib::nativeTest(std::string originalText)
{
return originalText + " " + originalText;
}

STEP 3: Again, in Native/Cpp/, create a file named NativeLib.hpp:

#ifndef NativeLib_hpp
#define NativeLib_hpp

#include <stdio.h>
#include <string>

class NativeLib
{
public:
std::string nativeTest(std::string originalText);
};

#endif /* NativeLib_hpp */

From now on and for the next two steps, we will unfortunately have to handle a bit of this monstrosity called Objective-C.

STEP 4: In Native/ObjC/, create a file named NativeLibWrapper.h:

#import <Foundation/Foundation.h>

@interface NativeLibWrapper : NSObject

- (NSString*) nativeTest: (NSString*) originalText;

@end

STEP 5: Again, in Native/ObjC/, create a file named NativeLibWrapper.mm (be careful to use the .mm extension!):

#import <Foundation/Foundation.h>
#import "NativeLibWrapper.h"
#import "../Cpp/NativeLib.hpp"

@interface NativeLibWrapper() {
NativeLib nativeLib;
}
@end

@implementation NativeLibWrapper

- (NSString*) nativeTest: (NSString*) originalText {
std::string finalText = nativeLib.nativeTest(
std::string([originalText UTF8String]));
return [NSString stringWithUTF8String:finalText.c_str()];
}

@end

STEP 6: Back to Swift, create a new class named NativeManager:

class NativeManager
{
private let nativeLibWrapper = NativeLibWrapper()

func test(_ originalText: String) -> String
{
return nativeLibWrapper.nativeTest(originalText)
}
}

STEP 7: Finally, at the root of the project (at the same level as Info.plist), create the file (if it doesn’t already exists) [YourProjectName]-Bridging-Header.h, and add to it:

#import "Native/ObjC/NativeLibWrapper.h"

Note that this file already exists if you’re working on a Flutter project, named Runner-Bridging-Header.h. You still need to add the import above, though.

STEP 8: Now the test() function can be called in Swift like this:

let nativeManager = NativeManager()
let finalMessage = nativeManager.test("Message")

// should print: "Message Message":
print(finalMessage)

Great! Now you should be able to call a C++ function from your Kotlin or Swift code.

But we could go a little be further. In a more realistic app, we often need to pass and return objects, which makes things a bit more complex (but not that difficult, don’t worry).

Let’s see now an example, first with Android and then with iOS, where we pass and return an object.

How to pass and return an object to and from a C++ function on Android

The main idea here is to create two classes, ExampleEntity (Kotlin) and Example (C++), that actually represent the same data, and do some mapping from one to the other.

STEP 1: Let’s start by creating the ExampleEntity Kotlin class in app/src/main/kotlin/com/juceaudio/juceaudiodemo/:

package com.juceaudio.juceaudiodemo

import androidx.annotation.Keep

@Keep
class ExampleEntity
{
var testInt: Int = 0
var testFloat: Float = 0F
var testBoolean: Boolean = false

override fun toString(): String
{
return """
testInt: $testInt
testFloat: $testFloat
testBoolean: $testBoolean
""".trimIndent()
}
}

The @Keep annotation is necessary here if you want your app not to crash in release mode: that will keep the field names as they are once compiled, which is necessary for the mapping below.

STEP 2: Now in app/src/main/cpp, create a new Example C++ class, in an Example.h file:

#ifndef ANDROID_EXAMPLE_H
#define ANDROID_EXAMPLE_H

class Example
{
public:
int testInt = 0;
bool testBoolean = false;
float testFloat = 0;
};

#endif //ANDROID_EXAMPLE_H

STEP 3: Back in the native-lib.cpp file, add a new function named nativeObjectTest()(more explanation below):

extern "C" JNIEXPORT jobject JNICALL Java_com_juceaudio_juceaudiodemo_NativeManager_nativeObjectTest(
JNIEnv *env, jclass, jobject exampleEntity)
{
// mapping from Kotlin to C++:
auto *example = new Example();
jclass exampleEntityClass = (*env).GetObjectClass(exampleEntity);

jfieldID jTestInt = (*env).GetFieldID(exampleEntityClass, "testInt", "I");
example->testInt = (*env).GetIntField(exampleEntity, jTestInt);
jfieldID jTestFloat = (*env).GetFieldID(exampleEntityClass, "testFloat", "F");
example->testFloat = (*env).GetFloatField(exampleEntity, jTestFloat);
jfieldID jTestBoolean = (*env).GetFieldID(exampleEntityClass, "testBoolean", "Z");
example->testBoolean = (*env).GetBooleanField(exampleEntity, jTestBoolean);

// some dummy work:
example->testInt *= 2;
example->testFloat *= 3;
example->testBoolean = !example->testBoolean;

// mapping back from C++ to Kotlin:
jclass exampleEntityFinalClass = (*env).FindClass("com/juceaudio/juceaudiodemo/ExampleEntity");
jmethodID methodId = (*env).GetMethodID(exampleEntityFinalClass, "<init>", "()V");
jobject jExampleEntity = (*env).NewObject(exampleEntityFinalClass, methodId);

jfieldID testIntField = (*env).GetFieldID(exampleEntityFinalClass, "testInt", "I");
(*env).SetIntField(jExampleEntity, testIntField, example->testInt);
jfieldID testFloatField = (*env).GetFieldID(exampleEntityFinalClass, "testFloat", "F");
(*env).SetFloatField(jExampleEntity, testFloatField, example->testFloat);
jfieldID testBooleanField = (*env).GetFieldID(exampleEntityFinalClass, "testBoolean", "Z");
(*env).SetBooleanField(jExampleEntity, testBooleanField, (jboolean) example->testBoolean);

return jExampleEntity;
}

That’s actually quite a big one, but don’t you worry, you actually have all you need to know here:

  • First, we pass an ExampleEntity instance as a parameter of nativeObjectTest().
  • The first half of the function is dedicated to the mapping, from the ExampleEntity Kotlin object to the Example C++ object.
  • Then, in the middle, we do some dummy work, just for testing purposes.
  • Finally, in the second half, we map the Example C++ object to a new ExampleEntity Kotlin object (named jExampleEntity in the example above).

The syntax above, although a little verbose, is quite self-explanatory. A few things to mention, though:

  • The “I”, “F”, “Z” that you can see in the code above describe the type of data you want to map (more info here).
  • methodId represents here the constructor of ExampleEntity.

STEP 4: Back in the NativeManager Kotlin class, we can add our nativeObjectTest() function:

package com.juceaudio.juceaudiodemo

class NativeManager
{
companion object
{
init
{
// the try/catch is useful if you want to mock NativeManager for unit tests:
try
{
System.loadLibrary("native-lib")
}
catch (e: UnsatisfiedLinkError) {}
}

// NDK functions, here declared as static:
@JvmStatic private external fun nativeTest(originalText: String): String
@JvmStatic private external fun nativeObjectTest(exampleEntity: ExampleEntity): ExampleEntity
}

fun test(originalText: String): String = nativeTest(originalText)
fun objectTest(exampleEntity: ExampleEntity): ExampleEntity = nativeObjectTest(exampleEntity)
}

STEP 5: Finally, we can check if it works:

val exampleEntity = ExampleEntity()
exampleEntity.testInt = 2
exampleEntity.testFloat = 3F
exampleEntity.testBoolean = false
val finalExampleEntity = nativeManager.objectTest(exampleEntity)

// shoud print:
// testInt: 4
// testFloat: 9.0
// testBoolean: true
Log.d("TAG", finalExampleEntity.toString())

How to pass and return an object to and from a C++ function on iOS

The main idea here is to create three classes, ExampleEntity (Swift) and Example (C++), but also OCExample (Objective-C), that actually represent the same data, and do some mapping from one to the other.

STEP 1: Let’s start by creating the ExampleEntity Swift class:

class ExampleEntity
{
var testInt: Int = 0
var testBoolean: Bool = false
var testFloat: Float = 0.0

init(testInt: Int,
testBoolean: Bool,
testFloat: Float)
{
self.testInt = testInt
self.testBoolean = testBoolean
self.testFloat = testFloat
}

public var description: String
{
return "testInt: \(testInt)\ntestBoolean: \(testBoolean)\ntestFloat: \(testFloat)"
}
}

STEP 2: Now in Native/Cpp/, create the Example C++ class, in an Example.hpp file:

#ifndef Example_hpp
#define Example_hpp

#include <stdio.h>

class Example
{
public:
int testInt = 0;
bool testBoolean = false;
float testFloat = 0;
};

#endif /* Example_hpp */

STEP 3: And in Native/ObjC/, create the OCExample Objective-C class.

First, create the OCExample.h file and put the following in it:

#import <Foundation/Foundation.h>

@interface OCExample : NSObject

@property int testInt;
@property bool testBoolean;
@property float testFloat;

@end

Then, create the OCExample.mm file (again, be careful with the .mm extension) and put the following in it (even though it’s almost empty, it is still necessary):

#import "OCExample.h"

@implementation OCExample

@end

Also, don’t forget to add it in your [YourProjectName]-Bridging-Header.h file:

#import "Native/ObjC/NativeLibWrapper.h"
#import "Native/ObjC/OCExample.h"

STEP 4: In Native/Cpp/, go back to the NativeLib.cpp file and add the nativeObjectTest() function:

#include "NativeLib.hpp"

std::string NativeLib::nativeTest(std::string originalText)
{
return originalText + " " + originalText;
}

Example* NativeLib::nativeObjectTest(Example *example)
{
// some dummy work:
example->testInt *= 2;
example->testFloat *= 3;
example->testBoolean = !example->testBoolean;
return example;
}

STEP 5: Again, go back to the NativeLib.hpp file and add the nativeObjectTest() function (don’t forget to import Example.hpp):

#ifndef NativeLib_hpp
#define NativeLib_hpp

#include <stdio.h>
#include <string>
#include "Example.hpp"

class NativeLib
{
public:
std::string nativeTest(std::string originalText);
Example* nativeObjectTest(Example *example);
};

#endif /* NativeLib_hpp */

STEP 6: In Native/ObjC/, go back to the NativeLibWrapper.h file and add the nativeObjectTest() function and the OCExample.h import:

#import <Foundation/Foundation.h>
#import "OCExample.h"

@interface NativeLibWrapper : NSObject

- (NSString*) nativeTest: (NSString*) originalText;

- (OCExample *) nativeObjectTest: (OCExample *) ocExample;

@end

STEP 7: Again, go back to theNativeLibWrapper.mm file and add the nativeObjectTest() function:

#import <Foundation/Foundation.h>
#import "NativeLibWrapper.h"
#import "../Cpp/NativeLib.hpp"
#import "../Cpp/Example.hpp"

@interface NativeLibWrapper() {
NativeLib nativeLib;
}
@end

@implementation NativeLibWrapper

- (NSString*) nativeTest: (NSString*) originalText {
std::string finalText = nativeLib.nativeTest(
std::string([originalText UTF8String]));
return [NSString stringWithUTF8String:finalText.c_str()];
}

- (OCExample *) nativeObjectTest: (OCExample *) ocExample {

// mapping from Objective-C to C++:
Example *example = new Example();
example->testInt = ocExample.testInt;
example->testBoolean = ocExample.testBoolean;
example->testFloat = ocExample.testFloat;

// calling the C++ function:
Example *finalExample = nativeLib.nativeObjectTest(example);

// mapping from C++ to Objective-C:
OCExample *finalOCExample = [OCExample new];
finalOCExample.testInt = finalExample->testInt;
finalOCExample.testBoolean = finalExample->testBoolean;
finalOCExample.testFloat = finalExample->testFloat;

return finalOCExample;
}

@end

STEP 8: Back to Swift, open theNativeManager class again and add the objectTest() function:

class NativeManager
{
private let nativeLibWrapper = NativeLibWrapper()

func test(_ originalText: String) -> String
{
return nativeLibWrapper.nativeTest(originalText)
}

func objectTest(exampleEntity: ExampleEntity) -> ExampleEntity
{
// mapping from Swift to Objective-C:
let ocExample = OCExample()
ocExample.testInt = Int32(exampleEntity.testInt)
ocExample.testFloat = exampleEntity.testFloat
ocExample.testBoolean = exampleEntity.testBoolean

let finalOCExample = nativeLibWrapper.nativeObjectTest(ocExample)

// mapping from Objective-C to Swift:
return ExampleEntity(
testInt: Int(finalOCExample!.testInt),
testBoolean: finalOCExample!.testBoolean,
testFloat: finalOCExample!.testFloat)
}
}

Yup, that’s a lot of mapping…

STEP 9: Finally, we can check if it works:

let exampleEntity = ExampleEntity(testInt: 2, testBoolean: false, testFloat: 3)
let finalExampleEntity = self.nativeManager.objectTest(exampleEntity: exampleEntity)

// shoud print:
// testInt: 4
// testBoolean: true
// testFloat: 9.0
print(finalExampleEntity.description)

Conclusion

This is it! Congratulations for making it to the end! If you have any question, feel free to ask.

And if you want more, you can find a fully functional project on Github. It’s a Flutter project, but if you go to the android or ios directories, you should feel at home. That project also illustrate how to work with a C++ external library (check my article here).

--

--