Here, I am including the experience I had while exploring solutions for developing a mobile cross-platform library, i.e. a single codebase that could be part of mobile apps running under different platforms. It covers my journey from mobile cross-platform developments tools (PhoneGap, Titanium, and the likes), code porting tools, and WebViews that weren’t up to the task, to C++ and JavaScript engines that did work. There aren’t many resources out there explaining how to approach this problem, so we thought it could be helpful if we shared this experience. This is the second of three parts, which documents the steps taken to develop the cross-platform library in C++. In part one, you can find the other candidate solutions that were investigated. In part three, you can find the JavaScript solution.
When the cross-platform development tools failed to provide the functionality we needed (check part one), we decided to try a lower-level solution that is supported by both platforms; C++. In Android, we’re able to interface C++ code through the Android Native Development Kit (NDK) and the Java Native Interface (JNI) framework. As for iOS, this is possible with Objective-C++, a language variant that allows source files to include both Objective-C and C++. Hooray!!
Cross-platform library
One of the first problems that we encountered when developing the cross-platform library was STL’s lack of functionality. The Standard Template Library (STL) in C++ provides some minimum functionality, essential for most C++ projects, but unfortunately, it is not as powerful as the JRE/Android and Cocoa Touch frameworks. This problem became apparent when we tried to perform HTTP requests, something that is not part of STL. The solutions to this kind of (missing functionality) problems are the following:
- Delegate missing functionality to the native code, i.e. develop everything you can in your cross-platform library and when that extra functionality is needed, call native methods that will perform what you need and pass the result back to the library. Not so clean…
- Find C/C++ libraries with their source, and either include the source to your cross-platform library, or build the binaries (and include them) for every target platform and CPU architecture/ABI (armv7, armv7s and arm64 for iOS, and armeabi, armeabi-v7a, x86 and mips for Android).
- Find existing prebuilt library binaries for the platforms. Very rare for mobile platforms, but occasionally, you might get lucky (like I did)!
It was decided to use libcurl
for the requests due to its portability and robustness. Luckily, the iOS binaries were found through this link, which also contains information on how to build such libraries in iOS. Unfortunately, it wasn’t that easy for Android, as no binaries were provided, and we needed to configure and build it for every target CPU ABI. This can be a painful process as the following steps must be taken:
Build the Android NDK toolchains for the target Android version and each target architecture. This is possible through the
${NDK_HOME}/build/tools/make-standalone-toolchain.sh
script provided with the NDK. The NDK toolchains are essential for building libraries, as they provide the cross-compilers, sysroots, and everything that is needed for building. There are some prebuilt toolchains included in${NDK_HOME}/toolchains
, however, they don’t provide support for STL and exceptions. Here is the script I wrote for automating the toolchain building:01: _# buildToolchain.sh_
02:03: #!/bin/sh04:05: # the NDK directory06: NDKHOME=$107: # the directory to store the toolchains_08: BASE_TOOLCHAINDIR=$209: # the platform (target version) for which the toolchain 10: # should be generated. android-9 used here11: PLATFORM=$312: # target architecture: armeabi, armeabi-v7a, x86, or mips13: ARCH=$414: # the host system, e.g. linux-x86_64, darwin-x866415: SYSTEM=$516:17: export ANDROID_NDK_ROOT=${NDK}18:19: TOOLCHAIN_DIR=${BASE_TOOLCHAIN_DIR}/${ARCH}20:21: mkdir -p ${TOOLCHAIN_DIR}22:23: if [ "${ARCH}" = "mips" ] || [ "${ARCH}" = "x86" ]; then24: ARCH_SWITCH=${ARCH}25: fi26:27: ${NDK_HOME}/build/tools/make-standalone-toolchain.sh 28: –platform=${PLATFORM} 29: –install-dir=${TOOLCHAIN_DIR} 30: –arch=${ARCH_SWITCH} 31: –system=${SYSTEM}
Build the 3rd-party libraries for every target architecture (armeabi, armeabi-v7a, x86 and mips) using the NDK toolchains. Alternatively, if we don’t care much about coverage, building the libraries for just the armeabi target would be sufficient, as it covers most devices. Here is the script I used for building
libcurl
:01: _# buildLibCurl.sh_
02:03: #!/bin/sh04:05: # directory with all toolchains06: BASE_TOOLCHAINDIR=$107: # directory with libcurl’s source_08: BASE_SOURCEDIR=$209: # target architecture10: ARCH=$311:12: # find appropriate toolchain_13: TOOLCHAIN_DIR=${BASE_TOOLCHAINDIR}/${ARCH}14:15: # get the toolchain’s sysroot_16: SYSROOT=${TOOLCHAINDIR}/sysroot17:18: # arrange the compiler flags according to the architecture19: # more info can be found in ${NDKHOME}/docs20: HOST=""21: CFLAGS=""22: LDFLAGS=""23: if [ "${ARCH}" = "armeabi" ]; then24: HOST=arm-linux-androideabi25: CFLAGS="-mthumb"26: else27: if [ "${ARCH}" = "armeabi-v7a" ]; then28: HOST=arm-linux-androideabi29: CFLAGS="-march=armv7-a -mfloat-abi=softfp -mfpu=vfpv3-d16"30: LDFLAGS="-march=armv7-a -Wl,–fix-cortex-a8"31: else32: if [ "${ARCH}" = "mips" ]; then33: HOST=mipsel-linux-android34: else
35: if [ "${ARCH}" = "x86" ]; then36: HOST=i686-linux-android37: fi38: fi39: fi40: fi41:42: export PATH=${TOOLCHAINDIR}/bin/:$PATH43: # set the compilers_44: export CC="${HOST}-gcc –sysroot=${SYSROOT}"45: export CXX="${HOST}-g++ –sysroot=${SYSROOT}"46: export AR="${HOST}-ar"47:48: cd ${BASE_SOURCE_DIR} && rm -rf build && mkdir build49: ./configure –prefix=$(pwd)/build 50: –host=${HOST} 51: CFLAGS="${CFLAGS}" 52: LDFLAGS="${LDFLAGS}"53:54: make && make install
After building all the 3rd-party dependencies (libcurl
), coding started! I don’t really need to expand on this, everyone can develop their own library logic. My tiny library exposed one function in its API, getItineraries(const char *from, const char *to, time_t date):std::vector
. Calling this function involved querying a Skyscanner Web API with an HTTP request, fetching the response, parsing it, creating the necessary itinerary
and itinerary_leg
C++ objects, putting them in a vector, sorting them by price, and returning the vector object. There isn’t much logic in there, but it’s enough to prove that developing a mobile cross-platform library is feasible.
In the next sections, we’re going to expand on implementing the Android and iOS glue code. We decided to write the wrappers manually, but alternatively, we could have used SWIG for wrapper generation. SWIG is a powerful tool for interfacing C/C++ code, but it can be a bit complicated to get started with. It would definitely benefit larger-scale projects.
Android
After developing the cross-platform library, we continued with the Android app. This involved the JNI wrappers and the Android UI code. The JNI wrappers consisted of 2 levels; the Java wrappers and the C++ wrappers. The Java wrappers were responsible for providing a Java API to our (C++) library, and the C++ wrappers took care of bridging the Java wrappers with the library by conforming to the JNI naming specifications and object translations.
JNI
In my library, 3 Java wrapper classes were implemented; one for the library’s API (the getItineraries()
method) and one for each of the C++ data classes (itinerary
and itinerary_leg
). Each of the methods that are delegated to the C++ wrapper needs to be declared with the native
keyword. These methods do not have a body, and instead, they are implemented in the C++ wrappers. JNI is able to find the corresponding C++ functions in the wrappers through their unique name which consists of the Java package, class and method name, separated by underscores.
1: _// SkyscannerSdkWrapper.java_2: 3: **package** net.skyscanner.mobilesdk.wrappers;4: 5: **public** **class** SkyscannerSdkWrapper {6: 7: **public** **native** void **test**();8: 9: }01: _// skyscanner_sdk_jni.cpp_02: 03: **#include** 04: _// includes the Android logging functions_05: **#include** 06: 07: _// preventing C++ name mangling in order to work with JNI_08: **#ifdef** __cplusplus09: **extern** "C" {10: **#endif**11: 12: _// corresponds to the net.skyscanner.mobilesdk.wrappers.SkyscannerSdkWrapper.test()_13: _// method_14: void15: **Java_net_skyscanner_mobilesdk_wrappers_SkyscannerSdkWrapper_test**(JNIEnv *env,16: jobject obj) {17: _// prints "test" to the LogCat output with "skyscanner_sdk_jni" as the tag_18: **__android_log_print**(ANDROID_LOG_DEBUG, "skyscanner_sdk_jni", "%s", "test");19: }20: 21: 22: **#ifdef** __cplusplus23: }24: **#endif**
The Java wrappers are placed inside the ${ANDROID_PROJECT_HOME}/src/
directory along with the rest of the source code, while the C++ wrappers should be in ${ANDROID_PROJECT_HOME}/jni/
.
So, how do the proxy objects work? How can the Java wrapper objects communicate with the original C++ objects? The architecture followed here was to allocate the C++ objects in the heap and store their pointers in the corresponding Java wrapper objects. This way, whenever a C++ object’s field or function needs to be evaluated or called in Java:
- the respective Java wrapper object’s method is called with the proper Java arguments,
- the Java wrapper method calls the C++ wrapper’s corresponding function, and
the C++ wrapper
- converts the Java method arguments to C++ ones,
- finds the C++ object through the pointer stored in the Java wrapper object,
- calls the C++ object’s method with the C++ arguments, and
if the method is non-void,
- it gets the C++ result,
- converts it to a Java primitive or
new
Java object, and - returns it to Java.
01: // ItineraryWrapper.java02:03: package net.skyscanner.mobilesdk.wrappers;04:05: public class ItineraryWrapper {06:
07: // the pointer to the original C++ itinerary object in the heap08: private long nativeHandle;09:10: public ItineraryWrapper(long nativeHandle) {11: this.nativeHandle = nativeHandle;12: }13:
14: public native ItineraryLegWrapper getOutboundLeg();15:
16: }01: _// itineraryjni.cpp02:03: #include "itinerary.h"04: #include "itineraryleg.h"05:06: // …07:08: // returns the ItineraryLegWrapper object itineraryleg object09: jobject Java_net_skyscanner_mobilesdk_wrappers_ItineraryWrapper_getOutboundLeg(10: JNIEnv env, jobject obj) {11:12: // getting the C++ pointer from the Java object13:14: // find the ItineraryWrapperClass15: jclass itinerary_wrapperjclass = env->GetObjectClass(obj);16: // find the nativeHandle field ID_17: jfieldID native_handle_field_id = env->GetFieldID(itinerary_wrapperjclass,18: "nativeHandle", "J"); // J is the type signature for long_19: itinerary it = env->GetLongField(obj, native_handle_fieldid);20:21: // get the pointer to the itineraryleg object22: const itinerary_leg leg = &it->get_outbound_leg();23:24: _// create an ItineraryLegWrapper Java object with the itineraryleg C++ pointer25: // to be returned to Java26:27: // find the ItineraryLegWrapper class28: const char itinerary_leg_jclass_name =29: "net/skyscanner/mobilesdk/wrappers/ItineraryLegWrapper";30:31: jclass itinerary_leg_wrapper_jclass = env->FindClass(32: itinerary_leg_jclassname);33: // find the constructor_34: jmethodID itinerary_leg_wrapper_jconstructor = env->GetMethodID(35: itinerary_leg_wrapperjclass, "
",36: "(J)V");37: // create the Java object_38: jobject leg_jobject = env->NewObject(itinerary_leg_wrapper_jclass,39: itinerary_leg_wrapper_jconstructor, (jlong) leg);40:41: return legjobject;42: }43:
44:
45: // …_
Here is a more detailed tutorial on JNI.
There is a major issue with this approach, as you probably have guessed. What about garbage collection? Java is a language with automatic garbage collection, while C++ objects located in the heap should be explicitly deleted (well, C++11 has smart pointers, but they were not investigated here). So what happens when Java objects get garbage-collected? Will the original C++ object be garbage collected or will there be a memory leak?
A solution to this problem would be to use Java’s finalize()
method which is called when an object gets garbage-collected. Unfortunately, there is no guarantee that this method will be called, so we couldn’t rely on this. The safe, but not so clean, approach that we followed was implementing a dispose()
method on each Java wrapper class, which takes care of deallocating the original C++ object. This method has to be called explicitly by the Android app developer before the Java wrapper object gets garbage-collected. If he fails to do so, memory leaks will occur.
01: _// ItineraryWrapper.java_02: 03: **package** net.skyscanner.mobilesdk.wrappers;04: 05: **public** **class** ItineraryWrapper {06: 07: _// ..._08: 09: **public** **native** void **dispose**();10: 11: }01: _// itinerary_jni.cpp_02: 03: _// ..._04: 05: void06: **Java_net_skyscanner_mobilesdk_wrappers_ItineraryWrapper_dispose**(JNIEnv *env,07: jobject obj) {08: _// similarly to getOutboundLeg() above_09: jclass itinerary_wrapper_jclass = env->**GetObjectClass**(obj);10: jfieldID native_handle_field_id env->**GetFieldID**(itinerary_wrapper_jclass,11: "nativeHandle", "J");12: itinerary *it = env->**GetLongField**(obj, native_handle_field_id);13: 14: **delete** it;15: }16: 17: _// ..._
Undoubtedly, this manual garbage-collecting approach will appear strange to Java developers, and it can be dangerous if developers are not careful. There is a workaround to this, but it comes at a cost. Instead of developing Java wrappers for C++ classes, we could translate the exposed C++ classes into Java ones, and when a C++ object would need to be passed to Java, the C++ object’s fields could be copied into a Java object. As a result, C++ objects would no longer be necessary, and they could be freed right after they are copied.
There are three main drawbacks to this workaround:
- Objects lose their flexibility. If the objects will need to be manipulated again in C++, two more translations (Java->C++ and C++->Java) will need to be performed.
- Allocations and deallocations that take place during copying have an impact on the library’s performance.
- Copying the C++ classes into Java ones will result in increased glue code size and possible inconsistencies.
NDK
Let’s continue with more NDK! Apart from the standalone toolchains (for building 3rd-party libraries), the NDK also provides the ${NDK_HOME}/ndk-build
script which builds our app’s native code dependencies, using the Application.mk
and Android.mk
Makefiles. They are located in the JNI directory (${PROJECT_HOME}/jni/
) and contain information about target architectures, native/external modules, compiler flags, source files, etc.
1: _# Application.mk_2: 3: _# Build for all 4 target ABIs_4: APP_ABI := all 5: _# Targeting 2.3 (Gingerbread)_6: APP_PLATFORM := android-97: _# STL to include _8: APP_STL := stlport_shared01: _# Android.mk (for the C++ wrappers)_02: 03: _# set the local path_04: LOCAL_PATH := $(call my-dir)05: 06: _# clear previously set variables_07: include $(CLEAR_VARS)08: 09: _# name of this module_10: LOCAL_MODULE := mobile_sdk_jni11: _# source files to include_12: LOCAL_SRC_FILES := itinerary_jni.cpp itinerary_leg_jni.cpp 13: skyscanner_sdk_jni.cpp utilities_jni.cpp14: 15: _# add logging support_16: LOCAL_LDLIBS := -llog17: _# this module depends on the mobile_sdk (the cross-platform _18: _# library) module (imported at the end of this file)_19: LOCAL_SHARED_LIBRARIES := mobile_sdk20: 21: _# include the GNU Makefile script that is in charge of _22: _# collecting all the information defined in LOCAL_XXX _23: _# and determine what to build, and how to do it exactly_24: include $(BUILD_SHARED_LIBRARY)25: 26: _# import the module located in the _mobile_sdk_/ directory in the _27: _# NDK_MODULE_PATH environment variable. _28: _# the module in this case is mobile_sdk_29: $(call import-module,mobile_sdk)
Android.mk
is required at ${PROJECT_HOME}/jni/
, and there can be several Android.mk
files, one at each directory where there is C/C++ source code or binaries to be included. However, Application.mk
is optional and we can have only one such file (in ${PROJECT_HOME}/jni/
) per project. For example, in our project, there were one Application.mk
and three Android.mk
files; one for Android’s JNI C++ wrappers, one for the library’s source code, and one for libcurl
’s binaries.
After preparing the wrappers and Makefiles, executing ${NDK_HOME}/ndk-build
at ${PROJECT_HOME}/
builds the C++ dependencies, packages them into shared libraries (one for each CPU ABI), and places them at the ${PROJECT_HOME}/libs/${TARGET_ARCH_ABI}/
directory.
The last step is to make sure that the C/C++ libraries are loaded at runtime through System.loadLibrary()
, and finally, implement our Android UI code.
01: _// SkyscannerSdkWrapper.java_02: 03: **package** net.skyscanner.mobilesdk.wrappers;04: 05: **public** **class** SkyscannerSdkWrapper {06: 07: **static** {08: _// STL_09: System.**loadLibrary**("stlport_shared");10: _// the mobile_sdk (cross-platform library + libcurl) module_11: System.**loadLibrary**("mobile_sdk");12: _// the C++ wrappers (JNI)_13: System.**loadLibrary**("mobile_sdk_jni");14: }15: 16: _// ..._17: 18: }
Documentation about the NDK can be found under the ${NDK_HOME}/docs
directory.
iOS
In iOS, things were much simpler. As previously mentioned, the prebuilt libcurl binary was found, but it shouldn’t have been too hard building it. The Objective-C-C++ bridge is achieved with Objective-C++, a language variant that allows the two languages to co-exist in a source file. In contrast to JNI’s verbose (and strange) specifications, the only step taken here was to add the .mm
extension to the files containing the wrapper code, so that the compiler can understand that they are Objective-C++ files. You don’t have to do anything else! Even though I’m an Android fan, I was quite impressed by this.
Consequently, the wrappers were written in Objective-C++. The same architecture was followed as in Android, i.e. 3 Objective-C++ wrappers were implemented, with each wrapper object holding the pointer to the original C++ object which resides in the heap.
01: _// ItineraryWrapper.mm_02: 03: **#import** "ItineraryWrapper.h"04: **#import** "ItineraryLegWrapper.h"05: 06: **#include** 07: **#include** "itinerary_leg.h"08: 09: @interface ItineraryWrapper ()10: 11: _// the pointer to the original C++ itinerary object in the heap_12: @property itinerary *nativeHandle;13: 14: @end 15: 16: @implementation ItineraryWrapper17: 18: _// the constructor_19: - (id)**initWithItinerary**:(itinerary *)it {20: **if** (self = [super **init**]) {21: _nativeHandle = it;22: }23: **return** self;24: }25: 26: @end
Fortunately, garbage collection was much easier. Objective-C objects have a dealloc
method which is called when they are deallocated; it is similar to Java’s finalize()
method, but in Objective-C, it is guaranteed that it will be called upon deallocation. Therefore, the C++ destructor was called inside dealloc
, and no special instructions need to be given to app developers.
01: _// ItineraryWrapper.mm_02: 03: _// ..._04: 05: @implementation ItineraryWrapper06: 07: _// ..._08: 09: _// called automatically when the Objective-C++ object is destroyed_10: - (void)**dealloc** {11: _// free the original C++ object_12: **delete** self.nativeHandle;13: }14: 15: @end
After the wrappers were implemented, a static library (iOS doesn’t support shared libraries) was built, containing our library along with Objective-C++ wrappers. The library and the headers were imported in the iOS project, the UI was implemented, and the iOS app was done! Much easier than Android..
That’s it! I hope this will help those of you trying to create mobile cross-platform libraries, reusing C++ libraries in Android/iOS, or even the ones getting started with Android NDK, JNI, or Objective-C++.
Good luck!
android, c++, cross-platform, ios, library, mobile, Technology