Document coroutines codegen: intrinsics part 2
This commit is contained in:
committed by
Ilmir Usmanov
parent
611c1b1a38
commit
8b604b8ce1
@@ -939,3 +939,125 @@ suspend fun main() {
|
||||
alsoReturnsInt()
|
||||
}
|
||||
```
|
||||
|
||||
#### SafeContinuation
|
||||
|
||||
Of course, there is a reason for `SafeContinuation`. Let's consider the following example:
|
||||
```kotlin
|
||||
fun builder(c: suspend () -> Unit) {
|
||||
c.startCoroutine(object: Continuation<Unit> {
|
||||
override val context = EmptyCoroutineContext
|
||||
override fun resumeWith(result: Result<Unit>) {
|
||||
result.getOrThrow()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun main() {
|
||||
builder {
|
||||
suspendCoroutineUninterceptedOrReturn {
|
||||
it.resumeWithException(IllegalStateException("Boo"))
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
One might assume, that we will get `IllegalStateException`, but this in not what happens here:
|
||||
```text
|
||||
Exception in thread "main" kotlin.KotlinNullPointerException
|
||||
at kotlin.coroutines.jvm.internal.ContinuationImpl.releaseIntercepted(ContinuationImpl.kt:118)
|
||||
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:39)
|
||||
at kotlin.coroutines.ContinuationKt.startCoroutine(Continuation.kt:114)
|
||||
```
|
||||
That is an example of undefined behavior.
|
||||
|
||||
So, what happens here and why it causes the KNPE? When we call `resumeWithException`, inside `BaseContinuationImpl.resumeWith` we call
|
||||
`releaseIntercepted`, where we set `intercepted` field to `CompletedContinuation`:
|
||||
```kotlin
|
||||
protected override fun releaseIntercepted() {
|
||||
val intercepted = intercepted
|
||||
if (intercepted != null && intercepted !== this) {
|
||||
context[ContinuationInterceptor]!!.releaseInterceptedContinuation(intercepted)
|
||||
}
|
||||
this.intercepted = CompletedContinuation // just in case
|
||||
}
|
||||
```
|
||||
Then, when we throw the exception by calling `getOrThrow`, `BaseContinuationImpl.resumeWith` catches it (see the section about resume with
|
||||
exception), and calls `releaseIntercepted` again, but since there is no continuation interceptor in `context`, we get the KNPE.
|
||||
|
||||
That is what essentially `SafeContinuation` prevents. It catches an exception inside its `resumeWith` method and saves it until
|
||||
`suspendCoroutine` calls `getOrThrow`. Also, `getOrThrow` returns `COROUTINE_SUSPENDED` for not-yet-finished coroutines. In other words,
|
||||
when a wrapped coroutine suspends, `getOrThrow` tells `suspendCoroutine` to suspend.
|
||||
|
||||
### startCoroutine
|
||||
|
||||
We have already covered how a coroutine suspends, what happens when it resumes and how the compiler handles it. However, we have never
|
||||
looked at how one can create or start a coroutine. In all previous examples, one could notice a call to `startCoroutine`. There are two
|
||||
versions of the function: one is to start a suspend lambda without parameters and the other one - to start a coroutine with either one
|
||||
parameter or a receiver. It is defined as follows:
|
||||
```kotlin
|
||||
public fun <T> (suspend () -> T).startCoroutine(completion: Continuation<T>) {
|
||||
createCoroutineUnintercepted(completion).intercepted().resume(Unit)
|
||||
}
|
||||
```
|
||||
So, it
|
||||
1. creates a coroutine
|
||||
2. intercepts it
|
||||
3. starts it
|
||||
|
||||
Once again, `createCoroutineUnintercepted` has two versions - one without parameters and the other one with exactly one parameter. All it
|
||||
does is calling suspending lambda's `create` function. After the interception, we resume the coroutine with a dummy value. As I explained
|
||||
in the resume with the value section, the state-machine ignores the value in its first state. Thus, it is the perfect way to start a
|
||||
coroutine without calling `invokeSuspend`. However, the way we start callable references is different. Since they are tail-call, in other
|
||||
words, do not have a
|
||||
continuation inside an object, we wrap them in a hand-written one.
|
||||
|
||||
#### create
|
||||
|
||||
`create` is generated by the compiler and it
|
||||
1. creates a copy of the lambda by calling a constructor with captured variables
|
||||
2. puts `create`'s arguments into parameter fields.
|
||||
|
||||
For example, if we have a lambda like
|
||||
```kotlin
|
||||
fun main() {
|
||||
val i = 1
|
||||
val lambda: suspend (Int) -> Int = { i + it }
|
||||
}
|
||||
```
|
||||
the generated `create` will look like
|
||||
```kotlin
|
||||
public fun create(value: Any?, completion: Continuation): Continuation {
|
||||
val resutl = main$lambda$1(this.$i, completion)
|
||||
result.I$0 = value as Int
|
||||
}
|
||||
```
|
||||
note that the constructor, in addition to captured parameters, accepts a completion object.
|
||||
|
||||
In Old JVM BE, `create` is generated for every suspend lambda even when we do not need the function. I.e., even for suspending lambdas with
|
||||
more than one parameter. There are only two versions of `createCoroutineUnintercepted`, and there are no other places where we call
|
||||
`create` (apart from compiler-generated `invoke`s). Thus, in JVM_IR BE, we fixed the slip-up, and it generates the `create` function only
|
||||
for functions with zero on one parameter.
|
||||
|
||||
##### Lambda Parameters
|
||||
|
||||
We need to put the arguments of the suspend lambda into fields since there can be only one argument of `invokeSuspend` - `$result`.
|
||||
The compiler moves the lambda body into `invokeSuspend`. Thus, `invokeSuspend` does all the computation. We reuse fields for spilled
|
||||
variables for parameters as well. For example, if we have a lambda with type `suspend Int.(Long, Any) -> Unit`, then `I$0` hold value of
|
||||
extension receiver,' `J$0` - the first argument, `L$1` - the second one.
|
||||
|
||||
This way, we can reuse spilled variables cleanup logic for parameters. If we used separate fields for parameters, we would need to manually
|
||||
push `null` to them as we do for spilled variable fields if we do not need them anymore.
|
||||
|
||||
#### invoke
|
||||
|
||||
`invoke` is basically `startCoroutine` without an interception. In `invoke`, we call `create` and resume a new instance with dummy value by
|
||||
calling `invokeSuspend`. We cannot just call `invokeSuspend` without calling the constructor first is that it would not create a
|
||||
continuation needed for the completion chain, as explained in the continuation-passing style section. Also, recursive suspend lambda calls
|
||||
would reset `label`'s value.
|
||||
|
||||
FIXME: We do not need to create an additional copy of the lambda if we can verify that we do not pass them as completion to themselves.
|
||||
However, this includes not only recursive lambdas. We can pass the lambda to a tail-call suspending function and call it there. In this
|
||||
case, the continuation object is the same, and we have the same problems as if there was a recursion.
|
||||
|
||||
Of course, in JVM_IR, we do not have a `create` function in case when the lambda has more than one parameter, `invoke` creates a new
|
||||
instance of the lambda with copies of all captured variables and then puts the parameters of the lambda to fields.
|
||||
Reference in New Issue
Block a user