Reaching users is a quintessential way of growing technology products. Time to market efficiency is a vital consideration for a business seeking to validate new ideas.
For consumer apps, that means making versions quickly available on a diversity of major target platforms such as:
- The Web
Shared libraries as a middleware foundation
There are various ways to go about delivering solutions to multiple platforms efficiently. I want to highlight an architecture based on shared libraries, situated in a middleware role between backend services and native clients, as one strategy to consider when seeking the following advantages:
- Code re-use at the data model handling and business logic level.
- Serve a preference for a single programming language or a set of languages.
- Freedom from target platform constraints through effective dependency handling.
- Plays nice with backend resources.
- Supports a workflow conducive to overall productivity.
Requirements of shared libraries
In choosing what technologies to invest in, there are several factors to consider. These can include:
- The capability for implementing most business logic with sufficient expressiveness and desired style.
- Having an API surface sufficient for native platform interfacing with the right balance of embeddability (access to language features and packages) and performance (memory and processing).
- Being free of interoperability restrictions, either technical or political in nature.
While I am considering this architecture in general terms, the target platform tending to have the most restrictions is iOS. Therefore, much of my evaluation is based on how well a chosen foundation can be integrated with that operating system.
Programming language options
When it comes to writing code, a programming language sets the stage for what can be expressed. We all have our individual preferences and they are an important factor to consider for both reasons of style and productivity. In a shared library situation, there are additional concerns of interoperability and package availability to extend base language functionality, as needed.
Some possible languages for generating multiplatform shared libraries are:
- Kotlin (generate with Kotlin/Native)
- C#/F# (generate with .NET Embedding)
- Go (aka golang) (generate with Go on Mobile)
C is one of the most portable languages but without much support for modern programming paradigms. C++ seems capable of doing just about anything but has a style and complexity that is not for everyone. This situation continues to evolve with further iterations of the language. However, in scenarios where undisputed control over system resources is needed, C/C++ are prime choices.
Swift may not be considered as a contender by those outside of the Apple ecosystem. Being aligned with one of the most restrictive platforms is something that may be a major drawback when it comes to filling a multiplatform role. Also, package availability may not be nearly as expansive as in other ecosystems.
Kotlin takes the next step by offering compilation of its code to native libraries via Kotlin/Native. One of its best features, interoperability with Java, may also be one of its worst, in terms of overall potential as a multiplatform language. This is due to decreased incentive for package creators to deliver pure Kotlin editions when there is a Java version available and that Kotlin/Native is not able to import Java.
C#/F# has a heavy association with Windows and that might be a turn-off for those unaware of .NET’s new open-source direction. With .NET Embedding, these languages may provide the right level of expressiveness along with embeddability and utility matched by one of the richest software ecosystems in the world.
Finally, when it comes to finding the right blend of performance and expressivity, Go (golang) and Rust may be the ideal candidates due to having the much of the control of C/C++, by virtue of being systems-level languages, with a more modern, refined (less-encumbered) approach. Between the two, Go seems to be more opinionated in its idioms, while Rust offers support for a greater variety of programming styles.
Ideally, what is wanted is modern expressiveness, high performance, and significant community support on multiple platforms where choosing a language doesn’t require trading capability for convenience or declaring allegiance to a corporate monopoly. In reality, there are numerous trade-offs for any given choice, or set thereof, such as the following concerns:
- Programmer abilities
- Memory requirements
- Computational performance
- Interoperability overhead
- Threading-model compatibility
- Garbage collecting needs
- Types to be exchanged
- Exception handling
- Package import limitations
- CPU optimizations
- Code expressiveness
- Platform restrictions
Evaluating the choices
One way to go about evaluating these options is to implement a project suitable to representing actual usage in each language. To that end, I created a sample project for my candidates where the goal was to produce a multiplatform compatible library capable of performing create, read, update and delete (CRUD) operations on DynamoDB that can be natively called from all of my target platforms.
The hard work to gain the necessary experience with multiple language options for shared libraries is something that each engineer must do for themselves to be able to make a meaningful comparison according to their capabilities and preferences.
In summary, there are additional considerations to take into account such as:
- Management of additional compilation steps (e.g. JIT or AOT)
- Supporting ecosystem (especially the availability of packages/libraries)
- Interoperability with existing environment
- Programming style (not to be underestimated)
Having evaluated C#, Kotlin (with Java), C++, Rust and Go (golang), my tentative conclusion is that all are potentially viable to serve as the language foundation for a shared library approach to multiplatform architecture. There are some limitations that will be deciding factors during actual project execution.
Choosing compiled solutions is still not a binary decision when it comes to adjusting to the nuances of multiplatform interoperability. For example, with .NET, the compiler produces an intermediate compilation that can be fed into a JIT compiler. From there, native code can be generated. In other words, there’s much more going on between the source and the final compiled product. There were also Xamarin dependencies that I found to be an issue when wishing to test with NUnit.
Sometime in the future, .NET Embedding (Embeddinator-4000) may present itself as an ideal multiplatform library generator. This is due in no small part to the large collection of libraries that can extend a C#/F# implementation. However, this technology does not yet seem to be ready for general use, lacking such features as complete debugging and additional work required to manage the Mono framework linked to host apps.1
On the Java side, particularly impacting non-JVM environments like iOS, Java-based libraries are not available to Kotlin/Native. This is a major limitation that negates much of its usefulness in a general purpose shared library role even though it may still be a much improved way to work with Java. Being a first-class Android language is also a strong factor to weigh in its favor.
For the present, C++ is probably still the tried and true favorite with regard to interoperability and performance. But, alas, it is a language weighed down heavily by its roots and requires programming in a way much different from other modern languages.
Go (golang) can be considered an improvement over some of those limitations in C++. I’m still evaluating its interoperation capabilities. Provisionally, the ability to produce iOS frameworks via “Go Mobile” or “Go on Mobile” seems promising and it may become the leader with regard to my preferences for a multiplatform library generator. It may be easier to interoperate with Go than Rust.
There is growing support for multiplatform approaches among most major programming language ecosystems. Aside from C/C++, multiplatform library generators like Kotlin/Native, .NET Embedding and Go Mobile still have ground to cover before being production-quality solutions. If you are ready to take the plunge into a general multiplatform shared library architecture, C++ is probably the safest choice with Rust and Go (golang) being the next most promising options from my perspective.