How to run C++ code from a native Android or iOS app
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?
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 ofnativeObjectTest()
. - The first half of the function is dedicated to the mapping, from the
ExampleEntity
Kotlin object to theExample
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 newExampleEntity
Kotlin object (namedjExampleEntity
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 ofExampleEntity
.
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).