How to build a shared C++ library for iOS and Android
At SafetyCulture, we want to ship things fast, so our customers can improve workplace safety and quality fast. In order to do that, our mobile engineering team is looking for new alternatives to develop our apps without having to write same code multiple times on different platforms.
There are quite a few options available out there with pros and cons. However it’s not easy to just pick one. When we validate those options, we have following guidelines in mind:
- No compromise on user experience. Regardless which solution we choose, our users should still be able to use high quality app which feels natural and smooth.
- Little to no compromise on developer experience. We will definitely do something different than native development, however we don’t want to feel that we have to make a big commitment on a new stack for just sharing code. It should be flexible enough for us to move to any direction we want in future without throwing all code away.
After spending a few days on research and discussion, we landed our eyes on Djinni, a share code solution by Dropbox which allows you to share main business logic in C++ and still have user interface handcrafted natively to achieve best user experience. You can also expose some platform specific APIs through the bridge for C++ to use, and expose back to user interface if necessary. This is really powerful as we can write a clean, thin UI layer using the same set of APIs on both platforms.
How to Setup Djinni
Setup is easy:
Add Djinni as submodule to your git project:
git submodule add https://github.com/dropbox/djinni.git deps/djinni
git submodule update --init --recursive
Create your .djinni
interface description file, for example:
question = record {
id: string;
title: string;
order: i32;
}page = record {
id: string;
title: string;
order: i32;
questions: list<question>;
}form = record {
id: string;
name: string;
pages: list<page>;
}shared_core = interface +c {
static create(): shared_core;
generate_form(number_of_pages: i32, questions_per_page: i32): form;
prefix_string(input: string) : string;
}
The interface description is pretty straightforward: we have three records which consist a single nested data structure: a form contains multiple pages of multiple questions.
The shared_core
is the name of the interface which we are going to implement in C++. You can also define an interface which can be implemented in Objective-C and Java, we will show you an example later in this article. Let’s just focus on this simple example for now. it’s worth noting that we defined a static create
method for the shared_core
which takes no parameters. This makes more sense when you need to pass in your platform implemented objects in as you will need them in most cases.
The next step is to create a shell script which take the above .djinni
file as input and generate all the bridging code for us. The .sh
file looks like this:
#! /usr/bin/env bash### Configuration
# Djinni IDL file location
djinni_file="demo.djinni"
# C++ namespace for generated src
namespace="demo"
# Objective-C class name prefix for generated src
objc_prefix="SC"
# Java package name for generated src
java_package="com.safetyculture.demo"### Script
# get base directory
base_dir=$(cd "`dirname "0"`" && pwd)
# get java directory from package name
java_dir=$(echo $java_package | tr . /)
# output directories for generated src
cpp_out="$base_dir/generated-src/cpp"
objc_out="$base_dir/generated-src/objc"
jni_out="$base_dir/generated-src/jni"
java_out="$base_dir/generated-src/java/$java_dir"
# clean generated src dirs
rm -rf $cpp_out
rm -rf $jni_out
rm -rf $objc_out
rm -rf $java_out
# execute the djinni command
deps/djinni/src/run \
--java-out $java_out \
--java-package $java_package \
--ident-java-field mFooBar \
--cpp-out $cpp_out \
--cpp-namespace $namespace \
--jni-out $jni_out \
--ident-jni-class NativeFooBar \
--ident-jni-file NativeFooBar \
--objc-out $objc_out \
--objc-type-prefix $objc_prefix \
--objcpp-out $objc_out \
--idl $djinni_file
This script depends on the Djinni submodule we added in the beginning, so make sure you’ve done that. Once you run this script, you will see a new directory called generated-src
. It contains four sub-directories: cpp
, objc
, jni
, java
which have all the code you need for your iOS or Android project.
To compile the Dinjini generated code for an iOS project:
- Create a directory called
platforms/ios
and create your iOS project inside of it. - Add files within
deps/djinni/support-lib/objc
,generated-src/objc
andgenerated-src/cpp
to your iOS project. DO NOT use copy option. - Rename the
main.m
of your iOS project to bemain.mm
; this turns it into a Objective-C++ file.
All done, you should be able to build your project without error.
Now let’s start writing some code. Create a directory called shared
and create two files called shared_core_impl.hpp
and shared_core_impl.cpp
. That’s where we write our shared code in C++. And don’t forget to add these two files to your iOS project as well.
The header file shared_core_impl.hpp
looks like this:
#include "shared_core.hpp"namespace demo {
class SharedCoreImpl : public demo::SharedCore {
public:
SharedCoreImpl();
Form generate_form(int32_t number_of_pages, int32_t questions_per_page);
std::string prefix_string(const std::string & input);
};
}
What this does is we create a new class called SharedCoreImpl
which implements the interface SharedCore
. I won’t post the full implementation code here, but the logic is pretty simple. The generate_form
method takes 2 integer parameters, generates and returns a Form
object with the number of pages and questions provided via parameters. The prefix_string
just puts a hello
in front of any input string and return the new string.
It’s really easy to use your C++ code in an iOS project; here is a short example:
#import "ViewController.h"
#import "SCSharedCore.h"
@interface ViewController ()
@property (nonatomic, strong) SCSharedCore *coreAPI;
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
_coreAPI = [SCSharedCore create];
}- (void)generateForm() {
// generate a form contains 500,0000 questions
SCForm * form = [_coreAPI generateForm:500 questionsPerPage:1000];
}
@end
That’s it! Not too hard, right? The setup process is a bit different on Android, you will need to import both generated jni
and java
code. However the concept is similar: importing automatically generated headers, and add C++ implementation code to your project. That’s why we need to put those C++ files in a shared directory outside of iOS or Android project.
The Architecture
This is the architecture diagram we end up with using Djinni.
We borrowed concept from Redux and Flux where data only flows one direction. View sends actions and then render new state. We will have specific managers written in C++ for each screen or UI flow. When a view is initiated, the view controller is responsible to instantiate and setup the manager object. The view only needs to worry about when to send actions and how to render state. The entire business logic is implemented in manager and hidden from UI layer which can help us to write really clean UI code.
This also makes writing unit tests really easy; since the UI layer now does rendering and send actions separately, we can create mock manager which only feeds different states to view, and test the rendering logic. We can also create another mock manager to verify if correct actions are sent when user interacts with UI.
Platform Interaction
In some cases UI may need to interact with platform APIs. We avoid calling platform APIs from the UI layer directly, instead we expose natively implemented APIs to the manager and surface back to UI layer. WE call this approach U shape.
Here is a good example for a platform interface:
ui_platform_support = interface +o +j {
post_task_in_background_thread(task: task);
post_task_in_main_thread(task: task);
}
Here we take advantage of existing concurrency/threading APIs from each platforms. The diagram below shows how UI is interacting with platform APIs:
Networking
As shown in the first architecture diagram, persistent layer is responsible to decide where data should go. We consider backend API are similar to disk storage which is just another source for us to read and write data. We use gRPC as our network protocol to interact with the backend. There are tons of benefits with using gRPC for mobile apps. We will have another blog post for this in future. However it’s still possible to pass down your platform networking implementation to the persistent layer if you don’t want to write all these HTTP calls using C++.
What We’ve Learnt
C++ is not that scary
If you don’t have previous experience in C++ like us, I’d encourage you to have a try at least. This language has evolved a lot in recent years with lots of modern features. The smart pointer is similar to ARC in iOS, so you don’t have to manually free your memory allocations, whereas you need to use weak reference to avoid retain cycle.
Be aware of performance overhead
There is little to none overhead over the bridge for iOS as C++ data structure is not foreign to Objective-C (Objective-C++). However there is noticeable performance impact on Android as Djinni using the JNI under the hood, so data structure marshalling and un-marshalling between C++ and Java are expensive operations. If your app needs to send large structured data over the bridge and speed matters, you will have to look into options such as sending cross-platform binary data format(e.g. Flatbuffers) instead of structured data.
There are extra steps to setup
If you are not familiar with Makefile
to build your support libraries, you will be likely to invest extra time initially to get everything in place. Since part of your code will be written in C++, some of your 3rd party libraries will need to be setup correctly in order for C++ to use. In our case, we build gRPC, ProtoBuf and Flatbuffers into static libraries in different architectures, combine them into fat binary for iOS using lipo
and add them to our app. However all these things are one time investment. Once you get everything up and running, it’s unlikely you are going to spend the same time over and over again.
That’s it! We are still in process of adopting this new architecture within mobile team. We will keep sharing our experience along the way.
Do you have experience building cross platform mobile apps? Join our engineering team and help us build products that impact lives.