Write once, run anywhere: when is it worth it?

The dream of writing code once and running it anywhere has been alive since the dawn of programming. Since the beginnings of Java’s VM environments to Electron, React Native and Flutter the dream is to this day alive and well. However, as it is often the case, the original (native) experience is usually better than that offered by the cross platform solution. Java was known for its slow performance and user interface that was very different from those used by native programs. Electron and React Native come with an entire JavaScript engine built in increasing program size and decreasing performance, Flutter doesn’t use native controls but draws them from scratch, inevitably leading to tiny UI inconsistencies.

There is no doubt that these shortcomings are forgivable in the amount of developer hours and budget saved, so all of these are indeed valuable options, otherwise there wouldn’t be a vibrant developer ecosystem surrounding these solutions. What we will explore instead in this article is the business case when a true, write once, run anywhere approach is indeed the best, uncompromised business decision.

The case for delivering uncompromised solution

Uncompromised solution represents a solution that doesn’t compromise the experience of the end user. When the end user is a consumer, that means that the application should offer native look and feel. When the end user is a library consumer, developers should enjoy the same experience a fully native library would offer.

Right now there is no cross platform solution that would make for an uncompromised user experience, so the only thing that can currently be implemented in an uncompromised way in a cross platform way is business logic. This business logic can then be packaged as a cross platform library and reused in native app builds, which use native controls for GUI. Some successful examples of this approach are MS Office, AutoCAD and our very own Zumo

In all these examples there is a large amount of business logic, which would need to be replicated across all supported platforms. Putting this logic into a shared (for example, C++) core and using native GUI controls on each platform, creates a seamless user experience. 

Tools to get the job done

First, comes the programming language selection. We considered interpreted languages (specifically, JavaScript), however there was a significant limitation that JavaScript interpreter would have to be bundled on the platforms that don’t natively expose it (for example, Android) and high performance parts of the program would still have to be compiled into binary form and bundled along with the library (for example, long running tasks such as encryption).

A natural choice is thus to select a language that can be compiled to all the supported platforms, i.e. C++ or Rust. We chose C++ as it seemed a more battle tested solution at the time.

A significant complication of cross platform solution is setting up a cross platform SDK build toolchain. C++ package manager Conan can be used to manage dependencies and build toolchains, specifically it simplifies cross platform builds significantly via target profiles. For more on setting up build pipelines refer to Let's Build Chuck Norris! series by Dimitri Merejkowsky.

Another tool that was of immense help writing bindings between C++ and Android/iOS was Djinni by Dropbox, a tool that made it possible to auto generate Java and Objective-C interfaces to reflect their C++ counterparts. Bindings for WebAssembly on the other hand were written with the help of the Embind binding library for Emscripten toolchain.

Last, but not least continuous integration tools like CircleCI make it possible to build the library for all the target platforms with the click of a button in a reproducible way in the cloud. Easing the pain of having to perform many manual steps in the build and release process.

Underwater rocks

There are several complexities and shortcomings of this approach.

First, it is necessary to write or at least generate bridge code and platform specific wrappers that make it pleasant for the end user to use the library. Different platforms have different idiosyncrasies which have to be accounted for on the wrapper level to guarantee a familiar experience to the end user. 

Developers of such a cross platform SDK need to be familiar with best practices of many platforms which is in practice difficult to achieve without years of experience. This brings us to our second problem, the problem of developer availability. It is difficult to find developers who have such a broad experience or who are interested in acquiring such a broad experience with different platforms and programming languages.

Sometimes it is OK to compromise

That being said, going cross platform via “write once, run anywhere approach” is very rarely the right approach. Due to the difficulties mentioned above, it is worthwhile pursuing this approach only in the case where business logic is complicated and would take a significant amount of time to rewrite for all the supported platforms. If the entire business logic consists of simple API calls it will be easier to write it multiple times than jumping through the hoops of writing it once. In such a case a cross platform unit test suite would suffice.

In other cases, when there is complex mathematical & statistical logic (MS Excel), complex computations (AutoCad) or crypto & blockchain logic (Zumo) this approach works well.

Want to chat more about how we can help you elevate your tech venture with our web development services? Get in touch at sales@dlabs.io.