How to use C++ for Cross-Platform Development

My current line of work revolves around an app that runs on four platforms – Android, iOS, macOS and Windows. It’s core code base is written in C++ and it uses Objective-C (on iOS and macOS) and Java (on Android) to access native APIs. I wanted to share some tips and tricks that I’ve accumulated over time on how to make the ends meet.

This article will mainly concentrate on languages and so called backend part of the application, if you are also interested in cross-platform UI development, then that is a whole another story and I won’t be covering it here. I can only mention some good frameworks to start with, like Qt, React Native, Flutter to name a few (the latter two being further away from C++, but they have C++ bindings).

This article also comes with code examples which you can check out on my GitHub.

Table of contents:

Why C++? Why not JavaScript all the way?

Well, first of all because it’s available on all these platforms plus-minus ouf-of-the-box, so it gives us opportunity to consolidate the business logic and avoid code duplication.

Second it’s truly a Swiss army knife of languages – it gives you speeds of low level languages, and safety and flexibility of high level ones. It allows you to program in OOP or functional style wherever it’s applicable.

<rant><dis>

Third – why not JavaScript (i.e. React and others)? – I hate web development, I’ve spent 12 years of my life doing it and for me it has been the churn of buzzwords and trendy frameworks that die after a year or so. And of course any such high level of abstraction comes with high degree of performance penalty, especially on mobile platforms.

</dis></rant>

Tools

First of all operating system – if you want to develop anything Apple related there’s no other (reasonable) way – you’ll need to get a Mac. So in general my choice is always to use macOS as it gives you opportunity to cover three platforms in one – iOS, macOS and Android, and then use Parallels Desktop with Windows for Windows development. And if you want to throw in Linux in the mix – you can use the same Parallels and install Ubuntu in a separate virtual machine.

Next you’ll need IDE – I chose to go native – Android Studio, Visual Studio and Xcode – they give you all the tools to debug and instrument your app the way the ecosystem developers intended.

Eventually it would be a mess to keep track of four (or five) different IDE/build system projects and all the configuration they could host, so to manage the build systems my weapon of choice is CMake. It’s a build system project generator which means it’s not a build system per-se it just knows how to generate projects for and run other build systems. It outputs native IDE projects which is amazing as it gives you full benefits of native development, but keeps your source and build configuration centralized.

A few tips on CMake:

  • Use APPLE, IOS, WIN32 and ANDROID variables to differentiate between platforms. APPLE in this case covers both macOS and iOS.
  • Avoid temptation to use file(GLOB) as it prevents CMake to detect changes in source structure and prevents it from auto-regenerating the project (which is nice feature to have).
  • You can use single header (.hpp file) for all platforms, but different compilation units (.cpp or .mm files – .mm in this case is Objective-C++ which I’ll cover further down). This gives cleaner C++ code without the unnecessary preprocessor clutter and in case of Objective-C++ it can use the Objective-C syntax mixed with C++.

For example:

set(SOURCE_FILES
    "MyClass.hpp"
    "MyClass.cpp" # Covers shared implementation parts
)
if(IOS)
    # Covers iOS implementation
    list(APPEND SOURCE_FILES "ios/MyClass.mm")
elseif(APPLE)
    # Covers macOS implementation
    list(APPEND SOURCE_FILES "macos/MyClass.mm")
elseif(ANDROID)
    # Covers Android implementation
    list(APPEND SOURCE_FILES "android/MyClass.cpp")
elseif(WIN32)
    # Covers Windows implementation
    list(APPEND SOURCE_FILES "windows/MyClass.cpp")
endif()

Android

Android’s eco system is the weirdest of them all – it uses completely different build system from the rest – Gradle – for which CMake does not generate a project, instead Gradle uses CMake as an underlying build system, but actually it uses CMake to generate and build Ninja projects – yet another build system.

Adding CMake to gradle is quite easy, all you need to do is to add externalNativeBuild clause and point it to root CMakeLists.txt file like this:

externalNativeBuild {
    cmake {
        path 'CMakeLists.txt'
        buildStagingDirectory "build_android_cmake"
    }
}

Then there’s the language for native development which on Android is Java and lately Kotlin – both of them can be mixed among themselves as they both run on JVM (Java Virtual Machine). This is where the fun part begins – to interop it with C++ you have to use JNI (Java Native Interface) which in turn is C API.

Also don’t forget to load your library from Java. You can do that by simply adding a static initialized to any Java class, like Activity or Application class:

    static {
        // Explicitly load our libraries before we start doing everything else
        System.loadLibrary("MyLib"); // name of the library which file name would be libMyLib.so
    }

Few tips to be aware of:

  • jobjects passed to function as arguments are local references, that means that they will be destroyed after the function finished, so if you wish to keep references you need to convert them to global references using NewGlobalRef(JNIEnv*, jobject) method.
  • Don’t forget to explicitly delete your global references, otherwise you’ll run into JVM memory leaks – this is where RAII comes in handy – just call DeleteGlobalRef(JNIEnv*, jobject) method in your destructor for every global reference your C++ object has created.
  • Be careful about threads – if you change threads in C++ and wish to call anything from JNI, you have to attach the thread using jint AttachCurrentThread(JavaVM* vm, void** p_env, void* thr_args) method.
  • Same goes the other way around – don’t forget to add @Synchronize attribute to any Java method that might be called from different thread from C++.

Calling C++ from Java

This is, in some ways, the easiest part – you need to expose your C function (or static C++ method) to JNI and that’s basically it. For example you can declare a Java method native:

package com.example;

class MyClass {
    public native void myMethod();
    # or
    public static native void myMethod();
}

And then to map a C function to it you simply use special naming convention for your C function (Java_<package_name>_<class_name>_<method_name>) with C++ name mangling disabled (i.e. extern “C”):

extern "C" JNIEXPORT jlong JNICALL Java_com_example_MyClass_myMethod(JNIEnv* env, jobject thisptr);

This will map a C function directly to MyClass.myMethod in package com.example.

Another way would be to register your methods in JNI_onLoad callback – a method that is called once when native library is loaded (think of it as a JNI’s “main” or “DllMain” method for the library). This gives opportunity to register any function as native, including static C++ methods.

class MyClass
{
public:
    static void myMethod();
};

// Declare JNI method map
namespace
{
    const JNINativeMethod methodsArray[] =
    {
        { "myMethod", "()V", reinterpret_cast<void*>(&MyClass::myMethod) }
    }
}

JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* /* reserved */)
{
    JNIEnv* env;
    if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK)
    {
        return JNI_ERR;
    }
    // Find the class definition in the current environment
    jclass javaClass = env->FindClass("com/example/MyClass");
    if (!javaClass)
    {
        return JNI_ERR;
    }
    // Register method map for the class
    if (env->RegisterNatives(javaClass, methodsArray, sizeof(methodsArray) / sizeof(methodsArray[0])) < 0)
    {
        return JNI_ERR;
    }
    return JNI_VERSION_1_6;
}

Calling Java from C++

This is a bit trickier as I mentioned above – you have to be careful about which thread you are working in, so you have to keep track of the JVM pointer to get the environment for current thread – this can be achieved by storing the JVM pointer in JNI_OnLoad function.
Apart from thread safety all you need to do is find the class, method ID and then perform the call:

// Find the class
auto javaClass = jniEnv->FindClass("com/example/MyClass");
if (!javaClass)
{
    throw std::runtime_error("Failed to find MyClass in JNI environment");
}
// Find method ID
auto methodId = jniEnv->GetMethodID(javaClass, "someMethod", "()V");
if (!methodId)
{
    throw std::runtime_error("Failed to find someMethod in MyClass");
}
// Call this method on the object (not the class)
jniEnv->CallVoidMethod(jniObject, methodId);

Of course you can get the method of your global object reference without knowing it’s full packaged name using GetObjectClass(JNIEnv*, jobject) function.

Object instances from Java

Now the trickiest part is managing dynamically allocated objects on both sides – having a C++ object instance match Java object instance and vice-versa. To achieve that you’ll need to keep global reference to Java object in your C++ object and a raw pointer address in Java class, and you have to define which of those objects will be the master (responsible for creation and destruction of the other object).

An example of master class in Java:

package com.example;

class MyClass {
    // This member holds a raw ptr to the native C++ object
    private long nativePtr;
    // This method constructs the C++ object and returns
    // raw pointer that get's stored in nativePtr member
    private native long constructNative();
    // This method destroys the C++ object
    private native void destructNative();

    MyClass() {
        nativePtr = constructNative();
    }
    // Java does not have destructors, so you have to have and
    // explicit Dispose method for this
    void Dispose() {
        destructNative();
    }

    private void otherMethod() {
        Log.d("Example", "Java method called");
    }
    private native void someMethod();
}

An example class in C++:

#include <iostream>
#include <jni/jni.h>

class MyClass
{
public:
    MyClass(JNIEnv* initJniEnv, jobject initJniObject)
        : jniEnv(initJniEnv)
        // Immediatelly create a global reference so we don't loose it
        , jniObject(initJniEnv->NewGlobalRef(initJniObject))
    {}
    MyClass(const MyClass&) = delete;
    MyClass& operator=(const MyClass&) = delete;
    ~MyClass()
    {
        // Release the global reference
        jniEnv->DeleteGlobalRef(jniObject);
    }
    void someMethod()
    {
        std::cout << "C++ method called\n";
    }
private:
    JNIEnv* jniEnv { nullptr };
    jobject jniObject;
};

// This method will return the raw C++ pointer stored
// in the MyClass.nativePtr member of the Java object
MyClass* getPtr(JNIEnv* jniEnv, jobject ptrThis)
{
    // First we need to get the class definition of the object
    jclass javaClass = jniEnv->GetObjectClass(ptrThis);
    if (!javaClass)
    {
        jniEnv->FatalError("GetObjectClass failed");
    }
    // Now we find the field ID
    jfieldID nativePtrId = jniEnv->GetFieldID(javaClass, "nativePtr", "J");
    if (! nativePtrId)
    {
        jniEnv->FatalError("GetFieldID failed");
    }
    // And now we read the pointer value, cast it and return it
    jlong nativePtr = jniEnv->GetLongField(ptrThis, nativePtrId);
    return reinterpret_cast<MyClass*>(nativePtr);
}

extern "C" JNIEXPORT jlong JNICALL Java_com_example_MyClass_constructNative(JNIEnv* jniEnv, jobject ptrThis)
{
    // Create global reference
    MyClass* nativeObject = new MyClass(jniEnv, ptrThis);
    return reinterpret_cast<jlong>(nativeObject);
}

extern "C" JNIEXPORT void JNICALL Java_com_example_MyClass_destructNative(JNIEnv* jniEnv, jobject ptrThis)
{
    MyClass* nativeObject = getPtr(jniEnv, ptrThis);
    delete nativeObject;
}

extern "C" JNIEXPORT void JNICALL Java_com_example_MyClass_someMethod(JNIEnv* jniEnv, jobject ptrThis)
    MyClass* nativeObject = getPtr(jniEnv, ptrThis);
    nativeObject->someMethod();
}

Object instances from C++

Another way of doing this is when you have C++ class as the master, you still keep pointers and reference at both ends for ease of inter-communication, but you need to create and destroy Java object from C++, so you need to make calls into JNI from C++.

To do so you’ll need to keep at least JNIEnv pointer stored globally somewhere, and the easiest way is to get it is by declaring JNI_OnLoad function.

A shortened down example of C++ class definition:

// You need a construcotr that constructs the Java object
MyClass:: MyClass()
{
    // Find the class definition
    auto javaClass = jniEnv->FindClass("com/example/MyClass");
    if (!javaClass)
    {
        throw std::runtime_error("Failed to find MyClass in JNI environment");
    }
    // Find constructor method's ID
    auto constructorId = jniEnv->GetMethodID(javaClass, "<init>", "(J)V");
    if (!constructorId)
    {
        throw std::runtime_error("Failed to find MyCrossPlatformClass2 constructor");
    }
    // Create object and pass this pointer as it's first argument for storage
    auto localObject = jniEnv->NewObject(javaClass, constructorId, reinterpret_cast<jlong>(this));
    if (!localObject)
    {
        throw std::runtime_error("Failed to create MyCrossPlatformClass2");
    }
    // Last but not least we need to create global reference so we can keep owning the Java companion class
    jniObject = jniEnv->NewGlobalRef(localObject);
}

// Of course don't forget to have a destructor that releases
// the global reference
MyClass::~ MyClass()
{
    // Release the global reference
    jniEnv->DeleteGlobalRef(jniObject);
}

An example class in Java:

package com.example;

class MyClass {
    // This member holds a raw ptr to the native C++ object
    private long nativePtr;

    // Constructor receives the C++ pointer at initialization time
    MyClass(long initNativePtr) {
        nativePtr = initNativePtr;
    }
}

iOS and macOS

Both platforms are from the same ecosystem the only difference for now is the target architecture, SDKs and a few Frameworks, but the lines are being blurred every day (Catalyst, M1, etc.), so soon it might be a single platform. Both platforms are built using Xcode for which CMake has a nice generator interface. To generate Xcode projects simply call this command line:

# Generate iOS project
cmake -G Xcode -S . -B build_ios -DCMAKE_SYSTEM_NAME=iOS
# Generate macOS project
cmake -G Xcode -S . -B build_macos

*OS native language for almost 20 years has been Objective-C (all the native frameworks are written in it), but lately Swift has taken on the stage and it seems it won’t go anywhere soon.

Objective-C, as it is a C-based language with some syntactic sugar, is really easy to mix with C++, Swift on the other hand is not – unfortunately Swift native functions and objects are not visible to C++ and neither is C++ objects to Swift. I’ll talk about Swift a bit further down.

The tricky part in Objective-C is the memory management, especially with ARC (Automatic Reference Counting) enabled – by default the compiler will continue keeping track of the references even in .cpp files, but if you use some APIs that tend to reinterpret_cast to void* – all is lost then and you might end up with garbage objects (I’m looking at you Qt!).

Overall you only need to use .mm extension to turn the compilation unit into an Objective-C++ file which allows you to write both C++ and Objective-C in the same file, for example – call an Objective-C message within the C++ class method:

void MyClass::someMethod(std::size_t someValue)
{
    [myObjcObject setValue:(NSUInteger)someValue];
}

And of course vice-versa – call C++ method from within the Objective-C method:

-(void)myMethod:(NSUInteger)newValue
{
    _cppObject->otherMetod(static_cast<std::size_t>(newValue));
}

Hosting Objective-C object within C++ class

The tricky part here is to differentiate where the header file that defines the class will be used – this is where the __OBJC__ macro comes in handy. First we add a forward declaration to our Objective-C delegate so it can be used in both Objective-C and C++:

#ifdef __OBJC__
// A standard Objective-C forward declaration when compiling within an Obective-C compilation unit
@class MyDelegate;
#else
#   include <objc/objc.h>
// Declare a type alias when compiling in C++
using MyDelegate = objc_object;
#endif

After that we can add an ARC managed pointer to our C++ class declaration:

class MyClass
{
public:
    MyClass();
    void platformNativeMethod() const;
    void cppMethod() const noexcept;
private:
    // This also keeps track of the reference
    MyDelegate* objCObject;
};

And then just create the object from within Objective-C++:

MyClass::MyClass()
{
    objCObject = [[MyDelegate alloc] init];
}

The ARC will take care of cleaning up when C++ object get’s destroyed.

Hosting C++ object within Objective-C class

This is by far the easiest part, a good example would be a typical Apple delegate pattern with a delegate that proxies all the Objective-C message calls back to C++ class, similarly like in previous example we add an Objective-C delegate as a member to our C++ class, but we also add our C++ class to the delegate like this:

@implementation MyDelegate {
    MyClass* _cppObject;
}

-(instancetype)initWithCppObject:(MyClass*)cppObject
{
    self = [super init];
    if (self)
    {
        _cppObject = cppObject;
    }
    return self;
}

@end

And in C++ constructor pass this to it:

MyClass::MyClass()
{
    objCObject = [[MyDelegate alloc] initWithCppObject:this];
}

You can also create C++ objects within the Objective-C object, just make sure they are properly destroyed either by using RAII smart pointers like std::unique_ptr or by deleting them in the [MyDelegate dalloc] method.

A few words about Objective-C runtime

There is one more way to work with Objective-C from C++ and it’s by calling Objective-C runtime directly using it’s C API (similar to JNI). All you need to know is a few functions, like:

  • objc_msgSend – allows you to call any Objective-C method (send a message)
  • sel_registerName – to get the SEL pointer from message name (remember that message name includes colons) to use in other methods

A few words about Swift

In case of Swift you need to use Objective-C or C to bridge it with C++. An example of making a class and method accessible through Objective-C runtime is marking them with @objc attribute:

@objc
class MyClass {
    @objc
    func swiftMethod() {
        // ...
    }
}

The other way around is somewhat easier – you just need an Objective-C wrapper (just like the delegate example above) that would proxy all the method calls to C++.

There is also undocumented @_cdecl attribute (soon to be standardized) exposing Swift functions as C functions which then can be called from C++. And of course you can call C functions from Swift, so wrapping your C++ method calls in plain C functions would also work.

Windows

On Windows I’d suggest you stick with Visual Studio, you can get a free Community Edition to do some development. To generate Visual Studio project from CMake, simply call this command:

# Generate Windows project
cmake -G "Visual Studio 15 2017 Win64" -S . -B build_windows

This is the easiest part so far as C/C++ are the native languages of the system, although Microsoft has been pushing more on C# and web technologies lately. The tricky part here is that all the previously mentioned platforms have already switched to Clang while Microsoft still uses it’s proprietary C++ compiler by default (you have options to switch to Clang, but as of this writing – it’s not there yet) which means you have to be careful about language features you choose as they might not be implemented yet.

A few words about C#

As I wasn’t planning on describing C# and C++ interop in this article, I won’t be showing any examples, but I wanted to mention the P/Invoke that allows bridging C/C++ to C# classes and makes it possible to call C/C++ functions from C#.

Conclusions

It’s not an easy task at first – you have to pull together three different idioms and wrap them, but after initial boilerplate and keeping stuff in mind you can achieve almost everything.

By far the most complicated is Android and JNI as there are so many abstractions in the way, but once you get to know them, you can actually write everything in C++ without ever touching Java.

The easiest (apart from the obvious – Windows) would be the Objective-C, as it’s a C family language and interops with C++ super easy.

A few things to remember:

  • Always check how each platform/language/ecosystem performs memory management and think ahead – be even more careful than with raw heap allocated C pointers.
  • Always check your thread affinity – especially in JNI, don’t start calling JNIEnv acquired from one thread into another. But it’s also important in Objective-C when working with GCD.

Join the Conversation

9 Comments

  1. These titbits are nice but rather disconnected. If you have some cross platform sample code and build files demonstrated above, it would be great if you could check them into a git repo.

    1. You can se a working example attached at the top of this article in my GitHub.

  2. You really shouldn’t be doing raw JNI by hand. There are great many invisible intricacies there that make it very hard to get right. Consider something like (shameless plug) https://github.com/smartsheet-mobile/smjni
    to make it easier.
    On Mac/iOS you sometimes can avoid dealing with ObjC objects and header duality they impose by using Core Foundation classes that have 0-toll bridging to them. E.g. CFStringRef instead of NSString etc. Obviously this only works for the fundamental Cocoa types but often this is all you need.

  3. Using C++ on Android was really painful. I’m relieved that it’s easier on iOS. I’ll try porting my game engine once I got an Mac computer.

  4. What are some notable tips and tricks you’ve gathered for developing and maintaining an application that runs on multiple platforms (Android, iOS, macOS, and Windows) with a core code base in C++ and utilizing Objective-C and Java for native API access?
    Visit us telkom university

Leave a comment

Leave a Reply to gusc Cancel reply

Your email address will not be published.