Kotlin/Native iOS
3. Coroutines and Immutability of K/N
Coroutines
What is coroutines? This official guide is very helpful for understanding what is coroutines and how to use coroutines.
This chapter’s topic is to use coroutines in K/N and its current status.
Updating Gradle
To use Coroutines in K/N, update dependencies insharedNative/build.gradle
.
kolinx-coroutines-core-common
and kotlinx-coroutines-core-native
are new. One more, in settings.gradle
add a line enableFeaturePreview('GRADLE_METADATA')
.
include ':app'
include ':sharedNative'
enableFeaturePreview('GRADLE_METADATA')
Then, Sync Now
.
Use Coroutines from iOS
Now, you can use coroutines APIs in common module. In actual.kt
, define this CoroutineDispatcher
.
private class MainDispatcher: CoroutineDispatcher() {
override fun dispatch(context: CoroutineContext, block: Runnable) {
dispatch_async(dispatch_get_main_queue()) { block.run() }
}
}
Coroutine dispatcher determines what thread the corresponding coroutine uses for its execution.
Next, define CoroutineScope
. We need a scope that runs on iOS event loop.
internal class MainScope: CoroutineScope {
private val dispatcher = MainDispatcher()
private val job = Job()
override val coroutineContext: CoroutineContext
get() = dispatcher + job
}
The scope is used to generate a coroutine. I don’t want it visible from iOS so the MainScope
class is defined with internal
. But the access level is depended on you.
CoroutineScope is a scope for new coroutines. This means every coroutine builder inherits the scope’s
coroutineContext
. Thanks to this both context elements and cancellation are automatically propagated. For example, the scope’s cancellation invokes cancellation of coroutines launched inside the scope.
Let’s try calling coroutines. Define showHelloCoroutine
function in actual.kt
and helloCoroutine
suspend function in common.kt
. The launch
function is an extended function of CoroutineScope
class. This is one of coroutine builders.
fun showHelloCoroutine() {
MainScope().launch {
helloCoroutine()
}
}---above: actual.kt--------below: common.kt------internal suspend fun helloCoroutine() {
println("Hello Coroutines!")
}
We can call it like the following.
ActualKt.showHelloCoroutine()// Hello Coroutines!
Here I separated to two functions. One is like a coroutine wrapper function and the other is a suspend function.
In most cases, suspend functions have business logic and we want to share them. So here, I defined helloCoroutine
suspend function in common.kt
to use from Android.
On the other hand, suspend functions cannot use directly from iOS yet so I prepared smth like a wrapper function for the suspend function.
Suspend functions are not exported to a framework in this time. But there is an issue about this.
HTTP Request with Ktor Client.
The above example is a little boring, just printing on console. We will implement HTTP request with Ktor, here.
Updating Gradle
(Updated on January 16, 2019)
Simple Request
Create a API class in common.kt
class Api {
private val client = HttpClient()
internal suspend fun request(urlString: String): String {
val result: String = client.call(urlString) {
method = HttpMethod.Get
}.response.readText()
return result
}
}
The HttpClient class is included in Ktor library. The request suspend function calls client.call
with Get
http method.
Next, define a wrapper function for this suspend function as an extended function of Api class in actual.kt
.
fun Api.request(completion: (String) -> Unit) {
MainScope().launch {
val result = request("https://tools.ietf.org/rfc/rfc8216.txt")
completion(result)
}
}
This passes https://tools.ietf.org/rfc/rfc8216.txt as a parameter. What is registered for RFC8216 ? You will see soon.
Let’s try call it in ViewController.swift
Api().request {
print($0)
return .init()
}
Kotlin lambda which returns Unit is exported as a closure which return KotlinUnit
, not Void. The last return is required because of this.
Anyway, this will be performed asynchronously and print HTTP Live Streaming document. We could use http request from iOS 🎉
Currently, Coroutines support only for main thread. But, supporting for multi-threaded coroutines is issued .
K/N immutability
Frozen
In K/N, immutability is a runtime property. This can be applied using freeze()
function in kotlin.native.concurrent
. We can check the frozen status using isFrozen
.
package kotlin.native.concurrent
public val kotlin.Any?.isFrozen: kotlin.Boolean /* compiled code */
This is a part of kotlin.native.concurrent
package. isFrozen
property is defined here. Any?
type has isFrozen
property apparently.
K/N ensures the important invariant, mutable XOR global
. This means the object is either immutable or accessible from the single thread.
Some objects are frozen by default, like
- Primitive types such as kotlin.String, kotlin.Int
object
singletons- enum class
These status can be checked easily, like the following. In actual.kt
,
primitive int isFrozen: true
primitive string isFrozen: true
class object isFrozen: false
class object with freeze() isFrozen: true
object singleton isFrozen: true
enum class isFrozen: true
If a mutating operation is applied to a frozen object, InvalidMutabilityException
is thrown.
IncorrectDereferenceException
A class object is not frozen by default. And K/N ensures mutable XOR global
.
What happens if we access a not frozen object? Try this, defining the following function in actual.kt
.
fun passNotFrozenObject(completion: (Hello) -> Unit) {
val hello = Hello()
completion(hello)
}
In ViewController.swift
.
ActualKt.passNotFrozenObject { hello in
DispatchQueue.global(qos: .background).async {
print(hello)
}
return .init()
}
This code crashes with
Uncaught Kotlin exception: kotlin.native.IncorrectDereferenceException: illegal attempt to access non-shared org.kotlin.mpp.mobile.Hello@19f3d08 from other thread
IncorrectDereferenceException
is thrown when the object is accessed from another thread.
Fix this crash by freeze(), like
fun passNotFrozenObject(completion: (Hello) -> Unit) {
val hello = Hello().freeze()
completion(hello)
}
This will work and the following will be shown on console
org.kotlin.mpp.mobile.Hello@1e470c8
These reference may be helpful for our understanding
- https://github.com/JetBrains/kotlin-native/blob/cfe2d2b7ce4aef41b45747d7c4d2d6238fd1808d/runtime/src/main/cpp/Memory.cpp#L467
- https://github.com/JetBrains/kotlin-native/blob/328413337b9dcab1dba6dd4d3cf5975d617e60d1/runtime/src/main/cpp/Memory.h#L30
@ThreadLocal and @SharedImmutable
When you use top level global variables of non-primitive types, ThreadLocal or SharedImmutable annotation sometimes may be helpful.
Try with simple example. Define this top level variable in actual.kt
.
val hello = Hello()
And access it from different thread in ViewController.swift.
print(Thread.current)
print(ActualKt.hello)
DispatchQueue.global(qos: .background).async {
print(Thread.current)
print(ActualKt.hello)
}// output on console
<NSThread: 0x600002e9e8c0>{number = 1, name = main}
org.kotlin.mpp.mobile.Hello@3ba2e28
<NSThread: 0x600002e16640>{number = 3, name = (null)}
Uncaught Kotlin exception: kotlin.native.IncorrectDereferenceException:
A class object is not frozen so this throws exception. How about using this?
val hello = Hello().freeze()
There is no change even though this hello
object is frozen !!
In fact, for top level variables, @SharedImmutable
or @ThreadLocal
annotation is needed.
- SharedImmutable: make the object frozen (immutable) and accessible from another thread
- ThreadLocal: make the object state thread local and mutable (the changed state is not reflected to other threads).
@SharedImmutable
val hello = Hello()
Then, the following lines are shown on console successfully.
<NSThread: 0x600002c468c0>{number = 1, name = main}
org.kotlin.mpp.mobile.Hello@39032e8
<NSThread: 0x600002c92080>{number = 3, name = (null)}
org.kotlin.mpp.mobile.Hello@39032e8
These annotation may go away in upcoming releases.
Summary
I introduced coroutines and immutability of K/N.
I want to show an example using reactive programming and architecture framework in next chapter. (coming soon)