Bytecode behind coroutines in Kotlin
Eugene PetrenkoA simple suspend function, and it’s bytecode.
The new thing in Kotlin 1.1 is coroutines. As we know from the documentation, it is the suspend
keyword that was added to the language. The rest is implemented as libraries.
Let’s take a look at the bytecode side of this feature.
An Empty Suspend function
I have the following code snippet:
suspend fun b() {}
Let’s take a look to the bytecode from this method. For the experiment, I use Kotlin 1.1.1 with IntelliJ IDEA 2017.1. Results may depend on version. I use javap -c
to generate those dumps
public static final java.lang.Object b(kotlin.coroutines.experimental.Continuation<? super kotlin.Unit>);
Code:
0: aload_0
1: ldc #13 // String $continuation
3: invokestatic #19 // Method kotlin/jvm/internal/Intrinsics.checkParameterIsNotNull:(Ljava/lang/Object;Ljava/lang/String;)V
6: getstatic #25 // Field kotlin/Unit.INSTANCE:Lkotlin/Unit;
9: areturn
The interface Continuation
is declared in the Kotlin standard library, see documentation. It contains context
and methods to complete continuation: resume
and resumeWithException
.
A Trivial Suspend function
suspend fun b2() {
a()
c()
}
Here a()
and c()
are calls to ordinary Java methods, which were declared in Kotlin without the suspend
keyword.
public static final java.lang.Object b2(kotlin.coroutines.experimental.Continuation<? super kotlin.Unit>);
Code:
0: aload_0
1: ldc #13 // String $continuation
3: invokestatic #19 // Method kotlin/jvm/internal/Intrinsics.checkParameterIsNotNull:(Ljava/lang/Object;Ljava/lang/String;)V
6: invokestatic #29 // Method a:()V
9: invokestatic #31 // Method c:()V
12: getstatic #25 // Field kotlin/Unit.INSTANCE:Lkotlin/Unit;
15: areturn
As we see from this code, there is nothing special done to the method. The only return value and additional parameter were added.
A suspend function with a suspend call
suspend fun b3() {
a()
b3()
c()
}
In this example, we call b3()
suspend function from itself. Here a()
and c()
are calls to ordinary Java methods, which were declared in Kotlin without suspend
keyword. The generated code now looks way different.
public static final java.lang.Object b3(kotlin.coroutines.experimental.Continuation<? super kotlin.Unit>);
descriptor: (Lkotlin/coroutines/experimental/Continuation;)Ljava/lang/Object;
Code:
0: aload_0
1: ldc #13 // String $continuation
3: invokestatic #19 // Method kotlin/jvm/internal/Intrinsics.checkParameterIsNotNull:(Ljava/lang/Object;Ljava/lang/String;)V
6: new #34 // class streams4/ZKt$b3$1
9: dup
10: aload_0
11: invokespecial #38 // Method streams4/ZKt$b3$1."<init>":(Lkotlin/coroutines/experimental/Continuation;)V
14: getstatic #25 // Field kotlin/Unit.INSTANCE:Lkotlin/Unit;
17: aconst_null
18: invokevirtual #42 // Method streams4/ZKt$b3$1.doResume:(Ljava/lang/Object;Ljava/lang/Throwable;)Ljava/lang/Object;
21: areturn
Instead of having the method in-place, it now generates an inner class for the state-machine to implement the suspend
.
The class streams4/ZKt$b3$1
is generated as follows
final class streams4.ZKt$b3$1 extends kotlin.coroutines.experimental.jvm.internal.CoroutineImpl {
public final java.lang.Object doResume(java.lang.Object, java.lang.Throwable);
descriptor: (Ljava/lang/Object;Ljava/lang/Throwable;)Ljava/lang/Object;
Code:
0: invokestatic #13 // Method kotlin/coroutines/experimental/intrinsics/IntrinsicsKt.getCOROUTINE_SUSPENDED:()Ljava/lang/Object;
3: astore_3
4: aload_0
5: getfield #17 // Field kotlin/coroutines/experimental/jvm/internal/CoroutineImpl.label:I
8: tableswitch { // 0 to 1
0: 32
1: 58
default: 74
}
32: aload_2
33: dup
34: ifnull 38
37: athrow
38: pop
39: invokestatic #23 // Method streams4/ZKt.a:()V
42: aload_0
43: aload_0
44: iconst_1
45: putfield #17 // Field kotlin/coroutines/experimental/jvm/internal/CoroutineImpl.label:I
48: invokestatic #27 // Method streams4/ZKt.b3:(Lkotlin/coroutines/experimental/Continuation;)Ljava/lang/Object;
51: dup
52: aload_3
53: if_acmpne 66
56: aload_3
57: areturn
58: aload_2
59: dup
60: ifnull 64
63: athrow
64: pop
65: aload_1
66: pop
67: invokestatic #30 // Method streams4/ZKt.c:()V
70: getstatic #36 // Field kotlin/Unit.INSTANCE:Lkotlin/Unit;
73: areturn
74: new #38 // class java/lang/IllegalStateException
77: dup
78: ldc #40 // String call to 'resume' before 'invoke' with coroutine
80: invokespecial #44 // Method java/lang/IllegalStateException."<init>":(Ljava/lang/String;)V
83: athrow
streams4.ZKt$b3$1(kotlin.coroutines.experimental.Continuation);
descriptor: (Lkotlin/coroutines/experimental/Continuation;)V
Code:
0: aload_0
1: iconst_0
2: aload_1
3: invokespecial #58 // Method kotlin/coroutines/experimental/jvm/internal/CoroutineImpl."<init>":(ILkotlin/coroutines/experimental/Continuation;)V
6: return
}
The implementation of b3()
function is moved to a state machine anonymous object. The main method of the inner object does a switch over states of the state machine. The b3()
function is split by every suspend
function call. On the example below, we have only 2 states. This is up to helper functions to assert the machine is always in a correct state.
On every suspend
function call, Kotlin creates an object to encapsulate the state of the state machine, that is created to implement the continuations on top of JVM.
Conclusion
Coroutines in Kotlin are awesome, easy and powerful constructs that give us the power to fight the complexity (by the cost of an extra abstraction level). I’m looking forward to using coroutines to simplify asynchronous code in my apps.
For more information and details see Kotlin coroutines documentation.