Skip to main content

Suspend Functions

Overview

The problem with suspend functions is that Objective-C has no equivalent feature. Therefore, suspend functions have to be exposed as Obj-C callback functions, which can be called from Swift using the async/await syntax. However, this is just Swift's syntax sugar. Swift still handles these functions as callback functions that do not support cancellation.

SKIE solves this limitation by generating actual Swift async functions and provides a custom runtime that bridges the concurrency contexts of both platforms. Therefore, from Swift's point of view, Kotlin suspend functions are indistinguishable from user-writen Swift async functions. In other words, a Swift developer can expect the same behavior from Kotlin suspend functions as from regular Swift async functions.

On the other hand, from Kotlin's perspective, the suspend function behaves as if it was called from another Kotlin suspend function. For example, if a suspend function is canceled from Kotlin, the Swift caller receives CancellationError, as would be the case with regular Swift async functions.

Another limitation is that these suspend functions can only be called from the main thread. This limitation can be solved without SKIE by passing a special configuration flag to the compiler. However, SKIE adds this support out of the box. Therefore, with SKIE, you can call the suspend functions from any thread in Swift.

Example

As an example, let's say we have this suspend function:

Kotlin
class ChatRoom {
suspend fun send(message: String) {
// Send a message
}
}

with SKIE this function behaves exactly the same as any other Swift async function:

Swift with SKIE
let chatRoom = ChatRoom()

let task = Task.detached { // Runs on a background thread
try? await chatRoom.send(message: "some message")
}

// Cancellation of the task also cancels the Kotlin coroutine
task.cancel()

Limitations

The suspend functions have two main limitations:

  • Member functions and extensions of generic classes must be called using a different syntax.
  • Overriding suspend functions cannot be fully supported and requires overriding a different function in the Swift code.

Generic Classes

Due to Swift/Objective-C interop limitations, it's impossible to generate the same Swift wrappers for member functions and extensions of generic classes. As a workaround, SKIE generates these functions as members of a different class. This class can be created by calling a generated skie(_:) function.

For example, given:

Kotlin
class A<T> {

suspend fun foo(): Int = 0
}

The correct way to call the foo() method is:

Swift with SKIE
let a = A<NSString>()

try await skie(a).foo()

Overriding Suspend Functions

The generated wrappers are regular Swift extensions and, as such, cannot be overridden. The original suspend functions are still available and can be overridden. Calling the generated wrapper will result in calling the overridden function. However, there are some limitations.

The main problem with this approach is that any call from the overridden function to other async functions will not correctly support cancellation. By overriding the original suspend function, you disconnect the cancellation bridge created by SKIE.

Another thing to be aware of is that SKIE renames the original suspend function by adding the __ prefix. This is done to avoid name collisions with the generated wrapper.

For example:

Kotlin
open class A {

open suspend fun foo(): Int = 0
}

can be overridden in Swift like this:

Swift with SKIE
class B: A {

override func __foo() async throws -> KotlinInt {
return KotlinInt(1)
}
}

Migration and Compatibility

The primary sources of incompatibility are the limitations mentioned above. These will create compile-time errors in your Swift code, which you will need to fix manually. Doing so should be relatively easy, as it means adding the __ prefix to overridden method names and wrapping receivers of some function calls by calling the function skie().

Other problems may arise because SKIE technically changes the semantics of your code by enabling cancellation support. You might run into unexpected changes in runtime behavior if your code relies on the cancellation support not working.

An additional source of runtime problems might be caused by different thread ing behavior between Kotlin and Swift. Without SKIE, the suspend functions run on the same thread from which they were called both from Kotlin and Swift. In practice, they usually run on the main thread because they can, by default, be called only from the main thread in Swift.

However, SKIE hands over the thread selection to Swift when the suspend function is called directly from Swift. Starting from Swift 5.7, the default behavior is to execute async functions on a background thread, and it's up to the called function to change the thread if needed.

Conversely, Kotlin Coroutines do not switch threads unless explicitly requested, which creates a difference in behavior between Kotlin and Swift. This difference can cause problems if your code relies on the thread on which the suspend function is executed, usually the main thread. The solution is explicitly switching the thread in the suspend function or changing the code to remove the dependency on the specific thread.