"Write once, run anywhere" is a very well-known slogan created to illustrate Java's cross-platform functionality. Most programmers would like to run their code anywhere. However, CPU architectures are impossible to keep up with, new programming languages emerge every day, frameworks come and go, and if you want to interact with the OS, code reusability is out of the question.
But maybe, if we narrowed down our scope to mobile devices, there could be a chance! There is an apparent trend towards mobile these days, so developing a mobile cross-platform library would definitely benefit some developers.
Let's say we narrow it down even further, to iOS and Android, which currently hold 93.9% of the market. This leaves us with 7 target CPU architectures/ABIs (armv7, armv7s and arm64 for iOS, and armeabi, armeabi-v7a, x86 and mips for Android), and 2 programming Languages (Objective-C for iOS and Java for Android). As for frameworks and OSs, support for the last 2 iOS versions should be sufficient, as the new iOS versions have a high adoption rate, but when it comes to Android, we need to support all versions since Froyo (or Gingerbread) if we want to have good coverage. As you can see, it's not an easy task, but we made it work.
What we wanted to achieve is summarized in this diagram; one library shared in two platforms, with some platform-specific glue code in order to make it work. As Skyscanner is heavily relying on the Internet, some networking functionality was essential.
Normally, in iOS, a library can be imported either through Objective-C source code, or through prebuilt static library binaries, accompanied by the respective header files. In Android, apart from Java source code, a library can also be imported through
.class (bytecode) files and static/shared library binaries. However, since these options were restrictive, the research that took place here went a step further and explored alternative ways of importing code in Android and iOS.
So, how do we start? What options are there? Are there any tools to make our life easier?
Option 1 - Mobile cross-platform development tools
If you're a mobile developer, you must have heard of numerous mobile cross-platform development tools, like PhoneGap, Appcelerator Titanium, and Xamarin. Some of these tools should allow us to develop a library, right?
Well, the main problems with this approach are that these tools usually:
- output to an end-product (
.apk), not a library, and
- embed a runtime environment to run the cross-platform code, so it's very difficult to interface code running in this environment from the native code.
Here is a list with the tools investigated and some of the reasons over why they didn't work:
- Adobe AIR - The runtime environment here is Adobe Integrated Runtime. Dismissed due to the difficulties of interfacing code in AIR.
- Xamarin - It can only output to intermediate Xamarin libraries, not native ones.
- Appcelerator Titanium - Building native libraries is not officially supported, but it could possibly work if we wrote Titanium extensions which would allow interfacing with native code. Too much trouble, with questionable results, and not sure if it will remain functional in the next Titanium updates…
- Corona - Corona people claim that it is supported for Android, while it's a "coming feature" for iOS.
- MoSync - Same as Corona
- Kony - Not supported
- Trigger.io - Not supported
- OpenFL - Not supported
- DragonRad - Out of date, doesn't seem to support it
Hence, failure, nothing worked :(
But wait a second, isn't C/C++ code accessible from both iOS and Android?
Option 2 - C++
Developing the library in C++ was one of the two solutions that did work.
In Android, the Native Development Kit (NDK) and the Java Native Interface (JNI) framework allow running/interfacing C/C++ code from Java. The NDK is responsible for compiling the C++ code for each Android target (armeabi, armeabi-v7a, x86 and mips), while JNI allows communication between the two languages. Using JNI can be quite verbose; programmers must adhere to naming conventions, and two levels of wrappers are required, in Java and C++. On the one hand, the Java wrappers provide a Java API for our C++ library, by exposing all the C++ classes and methods (with the
native keyword) in Java. On the other hand, the C++ wrappers provide the bridge between the Java wrappers and the C++ library, and translate objects between the two languages.
In iOS, things are much simpler. There are no naming conventions, and just one level of wrappers is required, using Objective-C++. Objective-C++ is a language variant which allows having both Objective-C and C++ code in a single source file. Consequently, all the object translations are happening in this single level of wrappers. You can see the slightly-modified diagram of the Android/iOS apps below:
Including 3rd party libraries is not unusual, as the programmer does not have access to the JRE/Android and Cocoa Touch frameworks. In this case, the 3rd party libraries can be included either as source code, or as prebuilt binaries (either find them, or build them). One of the specifications of the library was performing networking operations (HTTP requests), which is not supported by the Standard Template Library (STL), so we integrated
libcurl into the cross-platform library.
libcurl could not be included as source code, as a configure script had to be executed. Luckily, the prebuilt binaries were found for iOS. In Android, we used the NDK toolchains/compilers and built
libcurl for each Android target. Building libraries for all 7 targets (3 for iOS, 4 for Android) can be time-consuming, but part of this process can be automated with scripts.
This solution worked quite well; C++ is a popular language, there is a vast number of 3rd party libraries that can be used, and all the tools used (Android NDK, JNI, Objective-C++) are official solutions, supported by Google and Apple. The only drawback to this approach is that in Android, if we want to keep references of C++ objects in Java wrapper objects, we have to manually garbage collect the C++ objects (manually call
delete cppObject) before the Java objects are released. However, if there is no reason to retain C++ objects, they can be copied to Java ones, and destroyed immediately after copying.
More details (along with some code snippets) about this solution will be published in a separate post in the next weeks.
Option 3 - Code porting
Another option considered was to maintain one codebase, and use appropriate tools to translate the code to the platforms needed. This option has its drawbacks as well:
- The generated code will not be as efficient as code written by a native developer.
- Bugs are likely to be introduced, and have to be manually fixed.
- Imported binaries are unlikely to be translated, as most such tools translate only source code.
There are several mobile code porting tools, however, none of them provided the required functionality:
- J2ObjC - Tool for translating Java code to Objective-C, supported by Google. It seems like a high-quality tool (compared to the rest). Currently, it has a limited amount of Java classes that are translated to Objective-C, but it is a work in progress. Unfortunately, it doesn't translate Java’s HTTP requests at the moment, but if we implemented this functionality separately for each platform, it could possibly work. The project is alive since September 2012.
- ObjC2J - Tool that translates Objective-C code to Java. This could have been a good solution as well, but unfortunately, it is not mature enough, contains many bugs, and outputs non-compilable code.
- XMLVM - Tool that allows cross-compiling JVM Bytecode to Objective-C. Neither this tool appeared to be good enough, it is complex to use, and requires lots of legacy jars to be downloaded/imported.
- Apportable - Tool that takes care of porting an iOS app to Android. Unfortunately, it did not meet our criteria, as it translates whole apps, not libraries, and outputs to
.apk(Android application package) files.
- Avian - Lightweight Java Virtual Machine which could be embedded in an iOS app bundle and run Java code. This solution did not provide the desired functionality, as it would be very difficult to have the iOS UI code interfacing the library's Java code running in the VM.
- in the box - Porting of Dalvik VM and Android Gingerbread (2.3) APIs on top of iOS. This solution was dismissed, as the project is no longer active.
WebViews), and the
WebView APIs are usually exposed to app developers. Let's see..
- executing scripts
- calling functions
- evaluating global variables and returned results
- performing callbacks (to the native code)
Let's investigate each platform separately.
In Android, the
WebView can execute script strings. Callbacks to Java are implemented by annotating (with
In iOS, the
UIWebView can execute script strings. Unlike Android, it can evaluate global variables and function calls (with
Some projects that seemed very promising and are worth revisiting in the future:
- Appcelerator Hyperloop