Multiplatform development: sharing code

Paul Omta Follow Oct 27, 2022 · 6 mins read
Multiplatform development: sharing code
Share this

TomTom’s location technologies such as search and navigation can be added to a mobile app, an online service or in vehicles using the TomTom software development kits (SDKs). In some cases, the service is provided by a TomTom online service. In other cases, the SDK performs the work on the device itself. Our core functionality for on-device work, such as quickly calculating the fastest route, is written in C++. But the SDKs that we have are in Kotlin for Android and Swift for iOS. Ever wonder how we pulled that off after other parties such as Dropbox have banned code sharing between their apps?

I work on this kind of core functionality in C++ and write the interfaces in Kotlin and Swift as well. Within the Navigation SDK department, we have had this language interoperability challenge for as long as TomTom has sold portable navigation devices: since 2004. In this article, I will try to give you an overview of the different approaches we have taken to solve this challenge within TomTom and the improvements we have made on this topic over the years. Spoiler: we’re still sharing code with great success.

Why use multiplatform code?

An often cited reason is developer productivity: write code once and deploy it on all supported platforms. While this looks attractive at a glance, I personally think this is a wrong reason to share code. Perhaps Kotlin Multiplatform Mobile will move the proverbial needle in this space, but using the main contenders C and C++ for this can be hard. Software engineers with familiarity in C or C++ as well as either Kotlin or Swift are hard to come by, as already stated in the previous Dropbox article.

The reason TomTom uses multiplatform code is mainly the requirement to use a systems programming language that gives software engineers the ability to do complicated operations in a performant way, while using as little system resources as possible. C++ Is a great language for this, as one of its design paradigms is: you don’t pay for what you don’t use. And since we have relied on this language for a long time, it makes sense to stick to it and not switch to, for example, Rust. Also because on both the iOS and the Android platforms, one can interface with C++ components. On Android, using Java, this can be done through the Java Native Interface (JNI). On iOS, using Objective-C, one can call directly into C++.

In the past: a custom solution

In a previous incarnation of the Navigation SDK, we used a custom in-house remote procedure call (RPC) protocol to share code between platforms. This had the advantage that everything was under our control. It did present a maintenance burden and it was difficult for engineers working mainly on code for Android or iOS to use this interface. To smooth things out, a separate client library was developed to wrap this RPC framework and present callers with a platform-native interface in Java or Objective-C. This approach worked well in the final product, but it made the interface resistant to change due to this custom approach.

An improvement: using a tool

For the next incarnation of the Navigation SDK, the custom in-house RPC protocol implementation was dropped in favor of using a tool to automatically convert a C++ interface to Java: SWIG. This incarnation did not have an interface for iOS. While it seemed beneficial to automatically generate an interface, problems with this approach started to stack up. While SWIG is great when you are doing simple things such as calling a function and getting something in return, it falls apart in more complicated designs. Lifetime management for listeners, for example.

SWIG Works with interface files. In these files, you tell the tool which C++ header files to convert into your target language (Java in our case). To make things work properly, though, we had to inject custom code into these SWIG interface files. This required engineers to learn the SWIG syntax for doing that, which was non-trivial. The injected code was mainly needed to solve lifetime issues between Java & C++, as both languages have a very different memory model. Another example is that in some cases, we had to add default constructors to C++ objects that made no sense for the type, because SWIG would not perform the conversion properly otherwise.

We did look into applying Djinni as an alternative to SWIG. Since Djinni supports Objective-C as well as Java, this could be an improvement over SWIG. It works differently from SWIG, because it requires writing your interface in the Djinni interface definition language. It then generates the interface on both sides: C++ on the one side and Java or Objective-C on the other. The main issue with Djinni is that one loses control over the C++ interface, as this is generated by Djinni. It forces usage of C++ shared pointers, which is non-idiomatic in modern C++. There is no namespace support for C++ either. Basically, with Djinni you’re stuck with the common subset of the three languages, which hampers design flexibility and quality of the interface.

Current approach: language provided facilities

Newer programming languages require interoperability with existing languages in order for them to be adopted. Large, existing codebases prevent a rewrite into a shiny new programming language as a big-bang effort. So indeed: Kotlin interfaces nicely with Java, Swift interfaces with Objective-C. And those languages in turn interface with C++. This interoperability is there to stay and provides a sturdy foundation to build on. So what we currently do is to use the plain language facilities: the JNI for Kotlin and an Objective-C layer for Swift.

The only missing piece on Android was passing and returning arguments that do not directly correspond to the Java primitive types. For this, data is serialized to a byte array. For serialization, we use Google Protocol Buffers. This is a good choice, as it is not tied to any RPC implementation. A small .proto file needs to be defined for the layout of the data in memory and the protocol buffers compiler generates code for C++ and Java for conversion.

This is, in my personal opinion, the best solution so far. Yes, it is slightly more verbose than using a tool, but everything is under the control of the software engineer. It has turned out that we have needed this control to ensure we produced high quality interfaces for users of our APIs. And this approach allows for the most flexibility and the application of language idioms on both sides of the interface.

Written by Paul Omta
Software Engineer