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 first of three parts, which lists all the explored solutions. I’ll follow up with posts on the C++ and JavaScript engine parts.
"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 (
.app
/.ipa
or.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:
- "WebView" tools – These are tools that are using a WebView as the runtime environment and JavaScript/HTML5 for coding. They were immediately dismissed, as it would be very difficult to interface code running in the WebView (however, this was also attempted, as you’ll see later). Some of these tools are PhoneGap, RhoMobile, Sencha Touch, appMobi, Telerik.
- 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.
- Hyperloop – Tool that translates JavaScript to native source code. Currently, it only supports iOS and is not stable, but their plan is to expand to all popular platforms. The project is alive since August 2013.
- 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.
Option 4 – JavaScript In A WebView
JavaScript is a language that is gaining popularity very rapidly in the last few years, originally used for client-side scripting, but now also used in server-side applications (node.js), and it’s part of several of the mobile cross-platform tools investigated above. Maybe it could be the cross-platform language that solves our problems?
Well, all mobile platforms can execute JavaScript scripts in web-browser views (WebView
s), and the WebView
APIs are usually exposed to app developers. Let’s see..
At the very minimum, the functionality we need from JavaScript is:
- 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 @JavascriptInterface
) certain methods of a Java class to be callable by JavaScript, and adding references of the instances of this class to the WebView
‘s JavaScript global scope (with the addJavascriptInterface()
method). However, evaluating variables or function calls, is not as straightforward, as there is no way to directly evaluate a script. The only workaround to this is to pass callback functions to JavaScript, so that when the result is evaluated, the callback function gets called, passing the result to a Java method as an argument… This is shown in this SO answer.
In iOS, the UIWebView
can execute script strings. Unlike Android, it can evaluate global variables and function calls (with stringByEvaluatingJavaScriptFromString:
), but they are returned as a string, so appropriate conversions have to be made when the result is not a string. However, callbacks are not as straightforward as in Android, as there is no such mechanism in UIWebView
. The only (horrible) workaround to calling Objective-C from JavaScript is to try to open a new URL with a custom callback scheme (e.g. skycallback://
) in JavaScript, capture this event in Objective-C, parse the URL, and if the scheme has the name of the callback scheme, either parse the string of the URL’s resource path, or evaluate a global variable where the result is stored. This is shown in this SO answer.
As you see, the interactions between JavaScript and native code are very complicated, different for each platform, likely to result in bugs, and as the code grows in size, it will inevitably become unmaintainable. Consequently, this option was abandoned.
Option 5 – JavaScript In A JavaScript Engine
Having a JavaScript library running in a standalone JavaScript engine also worked.
Interacting with a JavaScript engine is much more straightforward compared to the WebView
, but unfortunately, a pure JavaScript engine lacks networking functionality. The XMLHttpRequest
object, which is responsible for HTTP requests in JavaScript, is not available, as it’s part of the web-browser and not the strict JavaScript specification. As a result, a different architecture was applied, by delegating the networking functionality to the platform-specific (glue) code. This complicates things, but we were particularly interested in developing a cross-platform JavaScript library. Here is how it worked:
In iOS, the JavaScriptCore engine was used, through the awesome (!!!) JavaScriptCore framework. This framework was introduced in iOS 7, and it’s very easy to integrate into the app within seconds, just as you would do with any Cocoa Touch framework. Its API is very simple and the glue code needed is concise.
In Android, again, things are a bit more complicated as there is no JavaScript engine, so we have to embed one manually. Two JavaScript engines were successfully embedded, Rhino and V8. Rhino is written in Java, so it was easy to embed, and it just adds 2.6MB to the app size. It’s developed by the Mozilla Foundation, but its development is inactive for some time now. V8 was a bit more difficult to embed, as it’s written in C++. Hence, the Android NDK and JNI had to be used in order to interface it from Java, adding one more layer of translations (Java<->C++<->JavaScript instead of Java<->JavaScript). In addition the app size increased by 7.1MB, something that might not be negligible for some apps. However, its development is very active.
The cross-platform library was developed in JavaScript with the HTTP request performed by a stub. This stub acts as the placeholder which is overridden after the library is loaded to the JavaScript engine. It gets replaced by a method which calls a native (platform-specific) method where the request is implemented.
The full description of the "JavaScript In A JavaScript Engine" solution can be found in the 3rd part of this sequence of posts, which will be published in the following weeks.
Conclusions
While several tools and techniques were explored, only two of them worked. On the one hand, the C++ solution is a widely used, reliable, and flexible solution, but has a significant drawback of manual garbage collection in Java (for which a workaround was proposed). On the other hand, the JavaScript solution is easier to implement, but the missing functionality overcomplicates the architecture, and is dependent either on Rhino which is not actively developed, or on V8 which has a significant impact on the app size. If you use one of these approaches, take these drawbacks into consideration and proceed with care.
Some projects that seemed very promising and are worth revisiting in the future:
- Corona
- MoSync
- J2ObjC
- Appcelerator Hyperloop
- Nashorn (JavaScript engine written in Java by Oracle)