Cross-Platform C++ Code Design for Mobile Apps

Igor Pener
Aug 9, 2017 · 5 min read

When building a new app, most people want to target multiple platforms —and that’s great! So prior to the development, you usually have to choose a framework like Xamarin, React Native, or game engine like Unitiy, Unreal Engine, CryEngine, etc. which allows to write code once and then magically recompile/generate it for all platforms. In theory this is awesome but practically there are always components that are platform-specific. Examples being localization, in-app purchases, notifications, sharing and messaging extensions, etc. This list is actually growing with every major iOS and Android release.
Choosing a cross-platform framework may certainly be the right choice for many developer teams but it is also a big dependency and can especially increase memory by quite a bit (e.g. when running the JavaScript VM). Luckily, there is already a native programming languange running on all platforms — C++. So let’s see how we can use it to come up with a cross-platform design without using any frameworks. Since we had just over a year to refine it while developing Shnips for iOS and Android at techOS, we’re confident that it’s generic, scalable and easily testable.

The Goal

Our goal will be to extract all logic into a shared C++ core — AppController — and have one interface to a class dealing with platform-specific calls — IAppDelegate.

// AppController.hpp
class IAppDelegate;

class AppController {
public:
AppController(IAppDelegate *delegate);

void callFromAppDelegate();

private:
IAppDelegate *_delegate;
};

This controller takes a pointer to IAppDelegate, the interface declaring the platform-specifc calls we want to do.

// IAppDelegate.hpp

class IAppDelegate {
public:
virtual void doStuff() = 0;
};

Now, we need to be able to call platform-specific methods from the shared core as well as execute C++ methods from the platform-specific code. The following section illustrates how to implement this two-way communication on iOS. Things get slightly more complicated on Android because the interaction between C++ and Java requires to schedule calls on a thread which the NDK is then executing. For those who are interested in Windows Phone programming, you can follow the same concept and create a similar bridge between C++ and C# or simply use Microsoft’s C++/CX.

iOS — Objective-C++

For cross-platform development, it’s better to use Objective-C than Swift because you can directly interact with Objective-C and C++ code using, wait for it, Objective-C++. Just rename the Objective-C file from .m to .mm and add -ObjC to Other Linker Flags in the Build Settings tab under the Linking section in Xcode. If you choose to use Swift, you will need an additional wrapper from Swift to Objective-C.
Now, to support calls from C++ to Objective-C, we need to add the AppDelegateBridge, which implements IAppDelegate and does nothing else but calling through to the real implementation defined in AppDelegate.

// AppDelegateBridge.h
#include "IAppDelegate.hpp"
#include "AppDelegate.h"

class AppDelegateBridge : public IAppDelegate {
public:
AppDelegateBridge(AppDelegate *delegate) :
_delegate(delegate) {
}

void doStuff() override {
[_delegate doStuff];
}

private:
AppDelegate *_delegate = nullptr;
};

All we have to do now is actually implement the desired behaviour in the AppDelegate class, which is also responsible for allocating and deallocating AppController. Calling C++ methods from native code works out of the box.

// AppDelegate.h
#import <UIKit/UIKit.h>

@interface AppDelegate : UIResponder <UIApplicationDelegate>
- (void)doStuff;
@end
// iOSAppDelegate.mm
#import "AppDelegateBridge.h"

@interface AppDelegate () {
AppController *_appController;
}
@end

@implementation AppDelegate
- (BOOL)application:(UIApplication *)app didFinishLaunchingWithOptions:(NSDictionary *)options {
_appController = new AppController(new AppDelegateBridge(self));
return YES;
}

- (void)doStuff {
// ...
}
@end

Don’t forget to deallocate the _appController instance. Apart from that we’re done on iOS. You can easily extend the functionality by adding virtual methods to IAppDelegate, calling through in AppDelegateBridge, and implement them in AppDelegate.

Android — Java

On Android, the AppDelegateBridge has a few more add-ons because Java runs in a virtual machine which makes the communication more cumbersome.

// AppDelegateBridge.hpp
#include <jni.h>
#include "IAppDelegate.hpp"
class AppDelegateBridge : public IAppDelegate {
public:
struct JNI {
JavaVM *vm = nullptr;
jclass obj = nullptr;
jmethodID doStuff;
JNI(); // This method should be called by JNI_OnLoad
void init(JNIEnv *env);
void clear();
};
void doStuff() override; static JNI app_delegate;
};

Here’s the implementation.

// AppDelegateBridge.cpp
#include "AppDelegateBridge.hpp"
AppDelegateBridge::JNI AppDelegateBridge::app_delegate;

AppDelegateBridge::JNI::JNI() {
clear();
}

void AppDelegateBridge::JNI::init(JNIEnv *env) {
const auto ref = env->FindClass(
"ch/techos/shnips/MainActivity"
);
obj = reinterpret_cast<jclass>(env->NewGlobalRef(ref));
doStuff = env->GetStaticMethodID(obj, "doStuff", "()V");
}

void AppDelegateBridge::JNI::clear() {
doStuff = nullptr;
}

void AppDelegateBridge::doStuff() {
JNIEnv *env = nullptr;
auto result = vm->GetEnv(
reinterpret_cast<void**>(&env), JNI_VERSION_1_6
);
if (env && result == JNI_OK && app_delegate.reset) {
env->CallStaticVoidMethod(
app_delegate.obj, app_delegate.doStuff
);
}
}

Notice that the app_delegate instance is static. Here’s the platform specific Java code.

// MainActivity.java
package BundleID;
public class MainActivity extends AppCompatActivity {
public static void doStuff() {
// ...
}
}

Now we can call Java code from C++. However, note that doStuff() will not be executed on the main thread. E.g., if you intend to manipulate the UI, you should create an internal class and extend it from android.os.Handler, override the synchronized handleMessage(Message) method and schedule doStuff() on the main thread.

What’s left is to be able to call C++ code from Java. For this we can simply add method stubs labeled with the native keyword.

// MainActivity.java
package BundleID;
public class MainActivity extends AppCompatActivity {
public static void doStuff() {
// Potentially reschedule on the main thread
}
public static native void callFromMainActivity();
}

If you have set up the NDK and gradle files correctly, this will auto-generate a file called app-core-jni.cpp which would contain the native method declarations and definitions similar to DLL entry points.

// app-core-jni.cpp
#include <jni.h>
#include "AppController.hpp"
#include "AppDelegateBridge.hpp"
AppController *app_controller = nullptr;

extern "C" {
JNIEXPORT
jint JNICALL JNI_OnLoad(JavaVM *vm, void *);
JNIEXPORT
void JNICALL
Java_BundleID_MainActivity_callFromMainActivity(
JNIEnv *env, jclass /*, custom arguments */
);
}
JNIEXPORT
jint JNICALL JNI_OnLoad(JavaVM *vm, void *) {
AppDelegateBridge::app_delegate.vm = vm;
JNIEnv *env = nullptr;
auto result = vm->GetEnv(
reinterpret_cast<void**>(&env), JNI_VERSION_1_6
);
if (!env || result != JNI_OK)
return -1;
AppDelegateBridge::app_delegate.init(env); if (!app_controller) {
app_controller = new AppController(new AppDelegateBridge());
}
return JNI_VERSION_1_6;
}
JNIEXPORT
void JNICALL
Java_BundleID_AppController_callFromMainActivity(
JNIEnv *env, jclass /*, custom arguments */
) {
if (!app_controller) {
app_controller->callFromAppDelegate();
}
}

OK, we’re done here. It’s not the most intuitive or developer friendly way and I sincerely hope Google will improve their NDK to facilitate this process. Don’t forget to deallocate app_controller when the app gets destroyed.

Testing

Note that although the AppController class has a pointer to IAppDelegate and the AppDelegate contains the AppController we can still easily test the C++ core without ever instantiating the AppDelegate class. How? Through dependency injection. We can simply pass a fake version of AppDelegate to the AppController constructor as long as it implements IAppDelegate.

// AppController_tests.cpp
#include "test_runner.hpp"
#include "IAppDelegate.hpp"
#include "AppController.hpp"

class FakeAppDelegate : public IAppDelegate {
public:
void doPlatformSpecificStuff() override {
// ...
}
}

TEST_SUITE(AppController) {
FakeAppDelegate _delegate;

TEST_CASE(toBeTested, "returns 42 when successful") {
AppController controller(&_delegate);
// ...
}
}

Depending on the app/game you develop, it might not always be faster to use a full blown cross-platform framework/game engine. And even if you do, it still has lots of value understanding how inter-operability is achieved. Have fun writing cross-platform apps.

 by the author.

Igor Pener

Written by

Founder / CEO of techOS. Built Shnips, a mobile game for iOS and Android. Computer graphics 👾, AR 👓, AI 🧠.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade