BlogProjectsAbout

🇺🇦 #StandWithUkraine

On Feb. 24, 2022 Russia declared an unprovoked war on Ukraine and launched a full-scale invasion. Russia is currently bombing peaceful Ukrainian cities, including schools and hospitals and attacking civilians who are fleeing conflict zones.

Please support Ukraine by lobbying your governments, protesting peacefully, and donating money to support the people of Ukraine. Below are links to trustworthy organizations that are helping to defend Ukraine in this unprovoked war:

Swift + Kotlin = ❤️


Last year, we at Readdle launched Spark for Android. After just one year, the app has reached one million installs on Google Play. I believe this makes it the most popular Swift application currently available on Google Play.

I joined Readdle in 2016 and worked on the initial release of Spark for Android for three years. Readdle developed a specialized Swift toolchain to create the Android version, which was detailed in a Medium article.

In this article, I want to describe the approach of integrating Swift and Kotlin code together.

Toolchain

The Swift compiler works very similarly to the NDK clang compiler (they are both built on top of the LLVM project). With proper silgen naming conventions, it can compile Swift code into ABI-idiomatic C binary code. From the JVM’s perspective, there is no difference between a dynamic library compiled from .c files and one compiled from .swift files. This makes it possible to write all native code for the Android platform using only the Swift language (including JNI bridges).

The LLVM Project is a collection of modular and reusable compiler and toolchain technologies. This fork is used to manage Apple’s stable releases of Clang as well as to support the Swift project. 1

Kotlin code compiles into JVM bytecode, which is not so different from the bytecode that can be compiled from Java.

Note: Because Android compiles Kotlin to ART-friendly bytecode in a similar manner as the Java programming language, you can apply the guidance on this page to both the Kotlin and Java programming languages in terms of JNI architecture and its associated costs. 2

From the standpoint of binding languages (Java <–> C or Kotlin <–> Swift), there is not much difference. At this point, the article could be concluded: Swift was successfully bound to Kotlin, goal achieved. But I want to go further.

My goal was not only to bind code written in these two languages but to make this binding automatic. A good acceptance criterion would be the automatic generation of Kotlin headers and JNI bridges for all Swift modules that were added to the Android project.

My proposal was to establish rules for binding specific Swift types to the appropriate Kotlin environment types. In the end, we categorized them into three groups:

  • References
  • Values
  • Protocols

But before I describe each group, let’s first talk about memory management in both runtime environments.

ARC and Tracing GC

On a regular basis, I interview experienced Android developers and ask them to describe how Traicing GC works and how it differs from Reference Counting. Then, I usually ask about a strong reference cycle. And this is the moment.

First, we should clarify what RC is.

In computer science, reference counting is a programming technique of storing the number of references, pointers, or handles to a resource, such as an object, a block of memory, disk space, and others. In garbage collection algorithms, reference counts may be used to deallocate objects that are no longer needed. 3

Ok, what about ARC?

Automatic Reference Counting is a memory management feature of the Clang compiler providing automatic reference counting for the Objective-C and Swift programming languages. At compile time, it inserts retain and release messages into the object code, which increase and decrease the reference count at runtime, marking for deallocation those objects when the number of references to them reaches zero. 4

Sounds good, but there are cases where ARC can’t handle memory management entirely automatically:

However, it’s possible to write code in which an instance of a class never gets to a point where it has zero strong references. This can happen if two class instances hold a strong reference to each other, such that each instance keeps the other alive. This is known as a strong reference cycle. 5

There is a common approach to avoid issues like this, as described by Apple.

Tracing GC works in a different way:

In computer programming, tracing garbage collection is a form of automatic memory management that consists of determining which objects should be deallocated (“garbage collected”) by tracing which objects are reachable by a chain of references from certain “root” objects, and considering the rest as “garbage” and collecting them. Tracing garbage collection is the most common type of garbage collection – so much so that “garbage collection” often refers to tracing garbage collection, rather than other methods such as reference counting – and there are a large number of algorithms used in its implementation. 6

Ok, and what about cycles? Cycles are handled by Tracing GC: all unreachable objects will be removed regardless of their cycle dependencies.

With this knowledge, we can move on to the first group: References.

Reference

Swift Reference is any public class that is imported into the Kotlin runtime environment. A Swift reference can be represented in the Kotlin environment as an instance of a Kotlin class that keeps a strong reference to the Swift instance.

Reference

How can a Kotlin class keep a strong reference? Here, we should recall how reference counting (RC) works: to create a strong reference to a Swift class, you need to manually retain (+1 to counter) and store the memory reference in a Kotlin long field (for 64-bit architecture). Of course, every retain should be balanced by a release (-1 to counter).

And here’s the tricky part. Unfortunately, there are no deallocators in Tracing GC, at least not in the classic sense. Java and Kotlin classes have the finalize method, which is called when the GC destroys objects. However, it is not a recommended way to clean up resources. 7

There are two approaches for native memory deallocation in the Android Open Source Project:

  1. Manual releasing: This approach is used in AOSP for MediaPlayer, MediaRecorder, and MediaMuxer.

  2. Automatic releasing: In the AOSP project, this is used in BigInteger. Before Android 9, BigInteger used finalize for releasing native handles. However, in Android 9+, BigInteger uses the NativeAllocationRegistry. Unfortunately, this API is for internal use only.

Note: If you are interested in this topic, I recommend the session How to Manage Native C++ Memory in Android (Google I/O '17).

I believe that the most straightforward approach for now is manual releasing of References (this may change in the future).

Here are some code examples for both languages from the Swift Weather project:

Swift Reference

public class WeatherRepository {
    public init(delegate: WeatherRepositoryDelegate)
    public func loadSavedLocations()
    public func addLocationToSaved(location: Location)
    public func removeSavedLocation(location: Location)
    public func searchLocations(query: String?)
}

Kotlin Reference

class WeatherRepository private constructor() {
    companion object {
        external fun init(delegate: WeatherRepositoryDelegate): WeatherRepository
    }
    // Swift JNI private native pointer
    private val nativePointer = 0L
    external fun loadSavedLocations()
    external fun addLocationToSaved(location: Location)
    external fun removeSavedLocation(location: Location)
    external fun searchLocations(query: String?)
    // Swift JNI release method
    external fun release()
}

Static functions and variables can be accessed with static external funs.

Value

Swift Values are public structs that are imported into the Kotlin runtime environment. Unlike classes, structs cannot be passed as references; they always operate with copy-on-write behavior. Therefore, the only way to pass them to the Kotlin environment is by making a copy. To work properly with the Swift API, copying should be supported in both directions: Swift -> Kotlin and Kotlin -> Swift.

Reference

One possible implementation is using the Codable protocol for encoding/decoding into Kotlin data classes. This approach was implemented in the JavaCoder library, which can encode/decode Swift structs to Kotlin data classes with the appropriate field names. As a result, Swift Values work with copy-on-read behavior (similar to C structs).

The current implementation of JavaCoder supports the following types from the standard library:

A Swift Value can include another Swift Value as a field. Additionally, JavaCoder supports Enums and OptionSets.

Here are some code examples for both languages from the Swift Weather project:

Swift Value:

public struct Weather: Codable, Hashable {
    public let state: WeatherState
    public let date: Date
    public let minTemp: Float
    public let maxTemp: Float
    public let temp: Float
    public let windSpeed: Float
    public let windDirection: Float
    public let airPressure: Float
    public let humidity: Float
    public let visibility: Float
    public let predictability: Float
}

Kotlin Value:

data class Weather(
    val state: WeatherState,
    val date: Date,
    val minTemp: Float,
    val maxTemp: Float,
    val temp: Float,
    val windSpeed: Float,
    val windDirection: Float,
    val airPressure: Float,
    val humidity: Float,
    val visibility: Float,
    val predictability: Float
)

Protocol

Swift Protocol refers to a protocol or a block that is imported into Kotlin’s environment.

Protocols are used for passing Kotlin reference instances to the Swift runtime environment. Typically, this involves the implementation of Swift protocols or Swift blocks.

Reference

In the Kotlin environment, it can be represented as an interface or a functional interface (fun interface). For such types, the we should generate a hidden Swift class that implements the corresponding protocol and creates a JNI Global Reference for the Kotlin instance. Once the object’s RC counter reaches zero, it deletes the JNI Global Reference and deinitializes.

A similar approach can be applied to Swift blocks.

Swift Protocol:

public protocol WeatherRepositoryDelegate {
    func onSearchSuggestionChanged(locations: [Location])
    func onSavedLocationChanged(locations: [Location])
    func onWeatherChanged(woeId: Int64, weather: Weather)
    func onError(errorDescription: String)
}

Kotlin Protocol:

interface WeatherRepositoryDelegate {
    fun onSearchSuggestionChanged(locations: ArrayList<Location>)
    fun onSavedLocationChanged(locations: ArrayList<Location>)
    fun onWeatherChanged(woeId: Long, weather: Weather)
    fun onError(errorDescription: String)
}

Swift Block:

public typealias SwiftBlock = (String) -> Void

Kotlin Block:

fun interface SwiftBlock {
    fun invoke(string: String)
}

Summary

With these three concepts (Swift Reference, Swift Value, Swift Protocol), our bridging technology can cover almost any Swift library API. Of course, it doesn’t support everything. For example, it doesn’t support templates or structs without the Codable protocol. In such cases, I recommend writing a small wrapper layer to optimize your API for Android.

Another question I hear very often is: what about performance? Does JNI have an impact on the performance of the app? In most cases, no. JNI is quite fast. There are general recommendations from Google on how to write code with JNI. All of these recommendations are applicable to Swift as well.

This is the roadmap for fully automated Kotlin-Swift binding:

  1. Create a tool to generate JNI code from Kotlin headers ✅: Implemented with the Annotation Processor (this is how Spark for Android works at the moment).
  2. Create a tool to generate Kotlin headers automatically 🚧
  3. Merge both tools for better performance and consistency 🎯

A few months ago, I started a small project to demonstrate using Swift on different platforms — Swift Weather App. At this point, it is a fairly simple project, but I believe it’s a great starting point for your journey into cross-platform Swift development.

Hi!

I’m Andrew. Here I write about programming for mobile platforms

I am also the creator of developer tools such as LM Playground, Swift Android Toolchain and Bonjour Browser. If you find my tools and insights valuable, consider supporting my work on buymeacoffee.