Bytecode behind coroutines in Kotlin

Bytecode behind coroutines in Kotlin

Eugene Petrenko

A 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.