Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Kotlin - suspending method has extra branching in byte code while method return type is inline class type

This is a dummy code to explain the issue;

  1. First method is a simple method calling another suspending method.
  2. Second method is a bit more complex under the hood since it is doing something else as well hence extra byte code will be generated. I completely understand the reason
  3. The third method is returning a string which is returned by a suspending method.
  4. The fourth and fifth methods are returning a custom class type i.e. StringWrapper

Note: The StringWrapper is an inline class.

@JvmInline
value class StringWrapper(val value: String)
class DummyServiceWrapper {

    private val p = DummyPrinter()

    suspend fun printHello() {
        p.printHello2()
    }

    suspend fun printHello1() {
        val value = p.printHello1()
        print("hey there $value")
    }


    suspend fun printHello2(): String {
        return p.printHello2()
    }


    suspend fun printHello3(): StringWrapper {
        return p.printHello1()
    }

    suspend fun printHello4(): StringWrapper {
        p.printHello1()
        return StringWrapper("Hey there")
    }

}

Below is the generated code for the above class;

public final class DummyServiceWrapper {
   private final DummyPrinter p = new DummyPrinter();

   @Nullable
   public final Object printHello(@NotNull Continuation $completion) {
      Object var10000 = this.p.printHello2($completion);
      return var10000 == IntrinsicsKt.getCOROUTINE_SUSPENDED() ? var10000 : Unit.INSTANCE;
   }

   @Nullable
   public final Object printHello1(@NotNull Continuation var1) {
      Object $continuation;
      label20: {
         if (var1 instanceof <undefinedtype>) {
            $continuation = (<undefinedtype>)var1;
            if ((((<undefinedtype>)$continuation).label & Integer.MIN_VALUE) != 0) {
               ((<undefinedtype>)$continuation).label -= Integer.MIN_VALUE;
               break label20;
            }
         }

         $continuation = new ContinuationImpl(var1) {
            // $FF: synthetic field
            Object result;
            int label;

            @Nullable
            public final Object invokeSuspend(@NotNull Object $result) {
               this.result = $result;
               this.label |= Integer.MIN_VALUE;
               return DummyServiceWrapper.this.printHello1(this);
            }
         };
      }

      Object $result = ((<undefinedtype>)$continuation).result;
      Object var6 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
      Object var10000;
      switch (((<undefinedtype>)$continuation).label) {
         case 0:
            ResultKt.throwOnFailure($result);
            DummyPrinter var7 = this.p;
            ((<undefinedtype>)$continuation).label = 1;
            var10000 = var7.printHello1-og2vJxQ((Continuation)$continuation);
            if (var10000 == var6) {
               return var6;
            }
            break;
         case 1:
            ResultKt.throwOnFailure($result);
            var10000 = ((StringWrapper)$result).unbox-impl();
            break;
         default:
            throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
      }

      String value = (String)var10000;
      String var3 = "hey there " + StringWrapper.toString-impl(value);
      System.out.print(var3);
      return Unit.INSTANCE;
   }

   @Nullable
   public final Object printHello2_og2vJxQ/* $FF was: printHello2-og2vJxQ*/(@NotNull Continuation var1) {
      Object $continuation;
      label20: {
         if (var1 instanceof <undefinedtype>) {
            $continuation = (<undefinedtype>)var1;
            if ((((<undefinedtype>)$continuation).label & Integer.MIN_VALUE) != 0) {
               ((<undefinedtype>)$continuation).label -= Integer.MIN_VALUE;
               break label20;
            }
         }

         $continuation = new ContinuationImpl(var1) {
            // $FF: synthetic field
            Object result;
            int label;

            @Nullable
            public final Object invokeSuspend(@NotNull Object $result) {
               this.result = $result;
               this.label |= Integer.MIN_VALUE;
               Object var10000 = DummyServiceWrapper.this.printHello2-og2vJxQ(this);
               return var10000 == IntrinsicsKt.getCOROUTINE_SUSPENDED() ? var10000 : StringWrapper.box-impl((String)var10000);
            }
         };
      }

      Object $result = ((<undefinedtype>)$continuation).result;
      Object var4 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
      Object var10000;
      switch (((<undefinedtype>)$continuation).label) {
         case 0:
            ResultKt.throwOnFailure($result);
            DummyPrinter var5 = this.p;
            ((<undefinedtype>)$continuation).label = 1;
            var10000 = var5.printHello1-og2vJxQ((Continuation)$continuation);
            if (var10000 == var4) {
               return var4;
            }
            break;
         case 1:
            ResultKt.throwOnFailure($result);
            var10000 = ((StringWrapper)$result).unbox-impl();
            break;
         default:
            throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
      }

      return (String)var10000;
   }

   @Nullable
   public final Object printHello3_og2vJxQ/* $FF was: printHello3-og2vJxQ*/(@NotNull Continuation var1) {
      Object $continuation;
      label20: {
         if (var1 instanceof <undefinedtype>) {
            $continuation = (<undefinedtype>)var1;
            if ((((<undefinedtype>)$continuation).label & Integer.MIN_VALUE) != 0) {
               ((<undefinedtype>)$continuation).label -= Integer.MIN_VALUE;
               break label20;
            }
         }

         $continuation = new ContinuationImpl(var1) {
            // $FF: synthetic field
            Object result;
            int label;

            @Nullable
            public final Object invokeSuspend(@NotNull Object $result) {
               this.result = $result;
               this.label |= Integer.MIN_VALUE;
               Object var10000 = DummyServiceWrapper.this.printHello3-og2vJxQ(this);
               return var10000 == IntrinsicsKt.getCOROUTINE_SUSPENDED() ? var10000 : StringWrapper.box-impl((String)var10000);
            }
         };
      }

      Object $result = ((<undefinedtype>)$continuation).result;
      Object var4 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
      Object var10000;
      switch (((<undefinedtype>)$continuation).label) {
         case 0:
            ResultKt.throwOnFailure($result);
            DummyPrinter var5 = this.p;
            ((<undefinedtype>)$continuation).label = 1;
            var10000 = var5.printHello1-og2vJxQ((Continuation)$continuation);
            if (var10000 == var4) {
               return var4;
            }
            break;
         case 1:
            ResultKt.throwOnFailure($result);
            var10000 = ((StringWrapper)$result).unbox-impl();
            break;
         default:
            throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
      }

      String var6 = (String)var10000;
      return StringWrapper.constructor-impl("Hey there");
   }
}

Can someone please explain why it has an extra branching inside the invokeSuspend

Given below,

@Nullable
public final Object invokeSuspend(@NotNull Object $result) {
       this.result = $result;
       this.label |= Integer.MIN_VALUE;
       Object var10000 = DummyServiceWrapper.this.printHello3-og2vJxQ(this);
       return var10000 == IntrinsicsKt.getCOROUTINE_SUSPENDED() ? var10000 : 
       StringWrapper.box-impl((String)var10000);
}

Compared to this when the suspending call does not return a inline type;


@Nullable
public final Object invokeSuspend(@NotNull Object $result) {
     this.result = $result;
     this.label |= Integer.MIN_VALUE;
     return DummyServiceWrapper.this.printHello1(this);
}

like image 430
rahat Avatar asked Aug 02 '26 16:08

rahat


1 Answers

When a suspend function is compiled, the compiler will add a Continuation type parameter and change the return type to Object.

  • Continuation parameter : To resume at where a function suspended. In Continuation, there is a label property to mark a resume point.
  • Object return type : When a suspend function suspends, COROUTINE_SUSPENDED is returned. its type is Any, which is Object in Java.

And according to the inline value classes document, inline classes are boxed whenever they are used as another type.

In StringWrapper, there are constructor_impl and box_impl.

  • constructor_impl : Return the parameter as the value type(String), called whenever you create a StringWrapper instance. StringWrapper("string") in Kotlin is StringWrapper.constructor_impl("string") in Java.
  • box_impl : Box the parameter into StringWrapper class. Called when it is needed.

In printHello4, return StringWrapper("Hey there") in Kotlin will be return StringWrapper.constructor_impl("Hey There") in Java, and the return value is String. But it need to be boxed as it is represented as Object. So it is boxed in invokeSuspend.

like image 53
ObscureCookie Avatar answered Aug 05 '26 06:08

ObscureCookie



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!