协程代码生成-2.Suspend Functions(译)

Suspend Functions

正如在 continuation-passing style一节中所解释的,每个挂起函数的签名都会改变:编译器会添加一个continuation参数,并将返回类型更改为Any?

当我们尝试使其可挂起时,即构建状态机并生成continuation时,问题就变得棘手起来了。与挂起 lambda 不同,我们不能重用现有的类来作为continuation,因为挂起函数可以是静态的,或者一个类中可能有多个函数。

解决这个问题的一种方法是将挂起函数转换为挂起 lambda。我们可以使用挂起函数的代码生成一个挂起 lambda,并在函数内部调用该 lambda。例如,当我们有一个函数时:

kotlin 复制代码
suspend fun test() {
    suspendMe()
    suspendMe()
}

we could generate code like

kotlin 复制代码
val test$1: suspend () -> Unit = {
    suspendMe()
    suspendMe()
}

suspend fun test() {
    test$1()
}

正如可以看到的,这两段代码在语义上是相同的。顺便说一句,这就是 JS 和 Native 后端生成挂起函数的方式。此外,在 JVM 中,我们以前也是这样做的,但现在不再这样做了。

我们不再这样做的原因是堆栈跟踪。如果我们将挂起函数的主体复制到 lambda 中,堆栈跟踪将如下所示:

text 复制代码
suspendFun1$1.invokeSuspend
suspendFun2$1.invokeSuspend
suspendFun3$1.invokeSuspend
suspendFun4$1.invokeSuspend
suspendFun5$1.invokeSuspend

but we want it to look like

text 复制代码
suspendFun1
suspendFun2
suspendFun3
suspendFun4
suspendFun5

因此,我们不将函数主体移动到 lambda 中,而是保留在函数中并在其中构建状态机。但是,我们也保留了"lambda",因此我们将所有溢出的变量和标签存储在其中。这个"lambda"被称为 continuation,它实质上是协程的状态。与挂起 lambda 不同,我们将状态(称为 continuation)和挂起函数的状态机进行了拆分。

Start

尽管如此,还有另一个问题。为了正确支持completion链,我们需要创建 continuation 并将 continuation 参数存储在 completion 字段中。我们还需要支持恢复协程,也就是说,我们需要从 continuation 中获取 label 和溢出变量。因此,我们需要区分这两种情况:重新开始和继续之前暂停的执行。最简单的方法是检查 continuation 参数的类型。因此,函数的开头将如下所示:

kotlin 复制代码
fun test($completion: Continuation<Unit>): Any? {
    val $continuation =
        if ($completion is test$1) $completion
        else test$1($completion)
    // state machine
}

只要我们为每个挂起函数生成不同的 continuation 类型,检查的技巧就能让我们区分这两种情况。

然而,我们有第三种情况:递归。当我们递归调用函数时,continuation 参数的类型与我们刚刚恢复时相同(请参阅下一节)。因此,函数有三种可能的调用情况:

  1. 从另一个挂起函数或挂起 lambda 直接调用
  2. 恢复
  3. 递归

因此,我们需要至少存储另一个位的信息。我们使用 label 字段的符号位来实现。因此,函数的前缀看起来像是:

kotlin 复制代码
fun test($completion: Continuation<Unit>): Any? {
    val $continuation =
        if ($completion is test$1 && $completion.label < 0) {
            $completion.label.sign_bit = 0
            $completion
        } else test$1($completion)
    // state machine
}

在这里,我们假设在递归调用中符号位未设置,而在resume过程中,continuation 类设置了它。让我们看看如何恢复和设置这个位。

Resume

我们已经处理了启动挂起函数并创建协程(在广义上),现在可以处理恢复过程了。正如之前解释的那样,当协程(在狭义上)暂停时,它返回 COROUTINE_SUSPENDED。因此,在协程的三个关键过程中:创建、暂停和恢复,只剩下了后者。

BaseContinuationImpl.resumeWith 中,我们调用 invokeSuspend。因此,在 invokeSuspend 内部,我们调用该函数并将 this 作为continuation参数传递:

kotlin 复制代码
fun invokeSuspend(result: Result<Any?>): Any? {
    test(this)
}

然而,我们也需要设置标签的符号位:

kotlin 复制代码
fun invokeSuspend(result: Result<Any?>): Any? {
    this.label.sign_bit = 1
    test(this)
}

让我们更改函数以调用返回值的另一个函数:

kotlin 复制代码
val c: Continuation<Int>? = null

fun suspendInt(): Int = suspendCoroutine {
    c = it
}

suspend fun test() {
    val i = suspendInt()
    println(i)
}

fun main() {
    builder {
        test()
    }
    c?.resume(42)
}

当我们运行这个示例时,会打印出 42。这意味着结果以某种方式传递给了函数。我们唯一可以传递结果的地方就是invokeSuspend。此外,函数只有一个 continuation 参数。因此,我们需要将结果放入 continuation 对象本身:

kotlin 复制代码
fun invokeSuspend(result: Result<Any?>): Any? {
    this.result = result
    this.label.sign_bit = 1
    test(this)
}

然后,在函数中从 continuation 对象中获取结果:

kotlin 复制代码
fun test($completion: Continuation<Unit>): Any? {
    val $continuation =
        if ($completion is test$1 && $completion.label < 0) {
            $completion.label.sign_bit = 0
            $completion
        } else test$1($completion)
    val $result = $continuation.result
    // state machine
}

变量溢出的方式与 lambda 或函数无关。然而,我们将变量溢出到 continuation 对象中。

JVM: Parameters

让我们现在来看看我们如何处理挂起函数参数。我们不为它们生成字段,因为 lambda 只是用它们将参数从 invoke 传递到 invokeSuspend。但是,对于挂起函数,我们不需要它们:参数是本地的;因此,我们重用了本地变量的溢出。

然而,保留函数签名中的参数会破坏恢复操作。在 continuation 的 invokeSuspend 中没有足够的信息来传递它们,就像它们之前或现在一样。因此,我们只需为引用类型放置 null,并为原始类型放置零。这意味着我们不能在函数开头生成空值检查,因此它们必须在第一个状态的开头生成,我们不能在那里恢复。

例如,如果我们将 test 函数更改为接受一个参数:

kotlin 复制代码
suspend fun dummy() {}

suspend fun test(a: Int) {
    dummy()
    dummy()
}

它的 continuation 的 invokeSuspend 变成了如下形式:

kotlin 复制代码
fun invokeSuspend(result: Result<Any?>): Any? {
    this.result = result
    this.label.sign_bit = 1
    test(0, this)
}

JVM: Layout

我们现在可以推断出挂起函数的continuation布局。

理想的挂起 lambda 布局如下:

  1. 超类:kotlin/coroutines/jvm/internal/ContinuationImpl
  2. 包局部的 int 类型的 label 字段。考虑到函数使用了它,并且函数位于类外部,所以是包局部的。
  3. 包局部的字段用于存储溢出变量。同样是包局部的。
  4. 公共最终方法 invokeSuspend,类型为 (Ljava/lang/Object;)Ljava/lang/Object;。它重写了 BaseContinuationImplinvokeSuspend 方法,用于调用函数。
  5. 公共或包私有的构造函数 <init>,类型为 (Lkotlin/coroutines/Continuation;)V,它调用了 BaseContinuatonImpl

Local Suspend Functions

局部函数有些奇怪:编译器在后端生成它们的方式各不相同。局部挂起函数甚至更奇怪。

Old JVM: Unerased Suspend Lambdas

老的 JVM 后端将局部函数生成为 Lambda 表达式。因此,挂起的局部函数被生成为挂起 Lambda,例如:

kotlin 复制代码
fun main() {
    suspend fun local(i: Int) {}
}

被生成为类似于:

kotlin 复制代码
fun main() {
    val local: suspend (Int) -> Unit = {}
}

这样做可以重用捕获变量的逻辑并简化代码生成的逻辑。然而,由于旧后端的限制,其 createinvoke 是未擦除的。换句话说,编译器复制它们,生成未擦除和擦除的副本。未擦除的副本接受带类型的参数,并包含 Lambda 的 invokecreate 的逻辑。另一个副本只接受 Any?。 局部挂起函数的布局如下:

  1. 超类型:kotlin/coroutines/jvm/internal/SuspendLambdakotlin/jvm/functions/Function{N}
  2. 包私有捕获变量
  3. 私有的整型 label 字段。私有的,因为它仅在 Lambda 本身中使用。
  4. 私有参数字段。可见性的原因与 label 字段相同。
  5. 用于溢出变量的私有字段。同样是私有的。
  6. 公共最终方法 invokeSuspend,类型为 (Ljava/lang/Object;)Ljava/lang/Object;。它重写了 BaseContinuationImplinvokeSuspend
  7. 公共最终 create 方法,类型为 (<params>,Lkotlin/coroutines/Continuation)Lkotlin/coroutines/Continuation<params> 的类型被擦除。
  8. 公共最终 create 方法,类型为 (<params>,Lkotlin/coroutines/Continuation)Lkotlin/coroutines/Continuation<params> 的类型未被擦除。
  9. 公共最终 invoke 方法,类型为 (<params>,Ljava/lang/Object;)Ljava/lang/Object;<params> 被擦除。
  10. 公共最终 invoke 方法,类型为 (<params>,Lkotlin/coroutines/Continuation;)Ljava/lang/Object;<params> 未被擦除。
  11. 公共或包私有构造函数:<init>,类型为 (<captured-variables>,Lkotlin/coroutines/Continuation;)V,在其中我们使用参数调用 SuspendLambda 的构造函数,并初始化捕获变量。 至于挂起 Lambda,编译器知道函数的 arity,但是完成函数是作为参数传递给构造函数的。

注意:这种实现存在大量的 bug。例如,局部挂起函数几乎无法递归。

JVM_IR: Static Functions

另一方面,JVM_IR 将局部函数生成为静态函数,并将捕获的变量作为第一个参数传递。因此,挂起的局部函数也被生成为静态函数。这样可以减小代码大小和方法数量,并且启用尾调用优化。

一个示例:

kotlin 复制代码
fun main() {
    val aa: Long = 1
    suspend fun local(i: Int) {
        println(aa)
    }
}

is generated as something like

kotlin 复制代码
suspend fun main$1(aa: Long, i: Int) {
    println(aa)
}

fun main() {
    val aa: Long = 1
}

Tail-Call Optimization

有人可能已经注意到,我们并不总是需要状态机。例如,当挂起函数根本不调用另一个挂起函数时。由于每个挂起调用都会创建一个 continuation,将其放在循环中成本相当高。出于这两个原因,我们不为将所有挂起调用置于尾位置的挂起函数生成 continuation 类和状态机。因为它们没有办法在函数中间挂起,它们不需要其中任何一个:它们只有一个状态。

尾调用函数示例:

kotlin 复制代码
suspend fun returnsInt() = suspendCoroutine<Int> { it.resume(42) }

suspend fun tailCall1(): Int {
    return returnsInt()
}

suspend fun tailCall2() = returnsInt()

对于这两个函数,编译器生成以下字节码(在 CoroutineTransformerMethodVisitor 之前):

text 复制代码
INVOKESTATIC InlineMarker.beforeInlineCall
ALOAD 1 // continuation
ICONST 0 // before suspend marker
INVOKESTATIC InlineMarker.mark
INVOKESTATIC returnsInt()
ICONST 1 // after suspend marker
INVOKESTATIC InlineMarker.mark
INVOKESTATIC InlineMarker.afterInlineCall
ARETURN

经过尾调用优化后,代码变为:

text 复制代码
ALOAD 1 // continuation
INVOKESTATIC returnsInt
ARETURN

检查函数是否是尾调用很简单:检查所有(可达的)挂起点是否:

  1. 不在 try-catch 块内
  2. 紧跟着的是带有可选分支或堆栈修改的 ARETURN,有一个显著的例外:GETSTATIC Unit; ARETURN(稍后会详细说明)。

MethodNodeExaminer 包含了此检查的逻辑。由于我们在两个后端中都使用相同的状态机构建器(因为我们应该支持 JVM_IR 中的字节码内联),因此逻辑是共享的。

请注意,由于我们不创建状态机,所以无需溢出变量,因此我们也不会创建 continuation 类。因此,completion链将缺少一个链接:

kotlin 复制代码
suspend fun returnsInt1() = suspendCoroutine<Int> { it.resume(42) }

suspend fun returnsInt2(): Int {
    val result = returnsInt1()
    println(result)
    return result
}

suspend fun returnsInt3() = returnsInt2()

suspend fun main() {
    println(returnsInt3())
}

如果没有尾调用优化,完成链会如下所示:

text 复制代码
          null<------+
                     |
      +-----------+  |
   +->+returnsInt1|  |
   |  +-----------+  |
   |  |completion +--+
   |  +-----------+
   |
   |
   |  +-----------+
   |  |returnsInt2+<-+
   |  +-----------+  |
   +--+completion |  |
      +-----------+  |
                     |
      +-----------+  |
   +->+returnsInt3|  |
   |  +-----------+  |
   |  |completion +--+
   |  +-----------+
   |
   |
   |  +-----------+
   |  |    main   |
   |  +-----------+
   +--+completion |
      +-----------+

但是使用尾调用优化后,它变成

text 复制代码
   +----->null
   |
   |  +-----------+
   |  |returnsInt2+<-+
   |  +-----------+  |
   +--+completion |  |
      +-----------+  |
                     |
      +-----------+  |
      |    main   |  |
      +-----------+  |
      |completion +--+
      +-----------+

returnInt1returnInt3 是尾调用,没有continuation。

在旧的 JVM 后端中,本地挂起函数是 lambda,它们不支持尾调用优化,但是由 JVM_IR 生成的本地挂起函数支持。

Redundant Locals Elimination

正如在有关变量溢出的部分所解释的那样,内联器在内联之前溢出堆栈,并在内联之后取消溢出。这导致了一堆重复的 ASTORE 和 ALOAD 指令,这可能会破坏尾调用消除,因为在挂起点和 ARETURN 之间可能会有一系列 ASTORE; ALOAD。这种字节码修改简化了链条,并使这些情况下的尾调用优化成为可能。

Tail-Call Optimization for Functions Returning Unit

如果我们想让返回 Unit 的挂起函数支持尾调用,那么就会面临一些挑战。让我们看看其中一个挑战。如果函数返回 Unit,则 return 关键字是可选的:

kotlin 复制代码
suspend fun returnsUnit() = suspendCoroutine<Unit> { it.resume(Unit) }

suspend fun tailCall1() {
    return returnsUnit()
}

suspend fun tailCall2() = returnsUnit()

suspend fun tailCall3() {
    returnsUnit()
}

在这个例子中,tailCall1tailCall2 被普通的尾调用优化所覆盖。然而,最后一个函数则不同。代码生成器生成了以下字节码:

text 复制代码
INVOKESTATIC InlineMarker.beforeInlineCall
ALOAD 1 // continuation
ICONST 0 // before suspending marker
INVOKESTATIC InlineMarker.mark(I)V
INVOKESTATIC returnsUnit()
ICONST 1 // after suspending marker
INVOKESTATIC InlineMarker.mark(I)V
INVOKESTATIC InlineMarker.afterInlineCall()V
POP
GETSTATIC kotlin/Unit.INSTANCE
ARETURN

正如你所看到的,UnitPOP 出栈,然后被重新推送到栈上并返回。不幸的是,我们不能简单地移除 POP; GETSTATIC kotlin/Unit.INSTANCE:如果我们用 returnsUnit 替换 returnsInt,字节码是相同的。由于在 CoroutineTransformerMethodVisitor 中我们无法获取挂起调用的返回类型信息,我们将所有这些调用视为只是 Any?,我们需要使用标记来标记返回 Unit 的函数调用。该标记与挂起标记类似,但参数不同:ICONST_2。因此,tailCall3 函数的完整字节码如下:

text 复制代码
INVOKESTATIC InlineMarker.beforeInlineCall
ALOAD 1 // continuation
ICONST 0 // before suspending marker
INVOKESTATIC InlineMarker.mark(I)V
INVOKESTATIC returnsUnit()
ICONST_2 // returns unit marker
INVOKESTATIC InlineMarker.mark(I)V
ICONST 1 // after suspending marker
INVOKESTATIC InlineMarker.mark(I)V
INVOKESTATIC InlineMarker.afterInlineCall
POP
GETSTATIC kotlin/Unit.INSTANCE
ARETURN

尾调用优化后,如预期的那样,没有状态机:

text 复制代码
ALOAD 1 // continuation
INVOKESTATIC returnsUnit()
ARETURN

我们暂时将returnsUnit替换为returnsInt

kotlin 复制代码
suspend fun returnsInt() = suspendCoroutine<Int> { it.resume(42) }

suspend fun tailCall() {
    returnsInt()
}

如前所述,不能简单地删除POP; GETSTATIC kotlin/Unit.INSTANCE,因为在这种情况下,返回Unit的函数将返回Int。但是,状态机中只能有一个状态。因此,我们只需保留POP; GETSTATIC kotlin/Unit.INSTANCE

text 复制代码
ALOAD 1 // continuation
INVOKESTATIC returnsInt()
POP
GETSTATIC kotlin/Unit.INSTANCE
ARETURN

然而,这里有一个问题。由于completion链缺少一个链接,有时会出现返回非Unit值的返回Unit的挂起函数的情况:

kotlin 复制代码
import kotlin.coroutines.*

var c: Continuation<Int>? = null

suspend fun returnsInt(): Int = suspendCoroutine { c = it }

suspend fun returnUnit() {
    returnsInt()
}

fun builder(c: suspend () -> Unit) {
    c.startCoroutine(Continuation(EmptyCoroutineContext) {
        it.getOrThrow()
    })
}

fun main() {
    builder {
        println(returnUnit())
    }

    c?.resume(42)
}

这个例子,就像前面的一个一样,一个返回Unit的尾调用函数(returnUnit)调用了一个返回非Unit的函数(returnsInt)。编译器生成了以下的completion链:

text 复制代码
           null<-----+
                     |
      +-----------+  |
   +->+ builder$1 |  |
   |  +-----------+  |
   |  |completion +--+
   |  +-----------+
   |
   |
   |  +-----------+
   |  |   main$1  |
   |  +-----------+
   +--+completion |
      +-----------+

这是正确的;编译器只生成了一个 continuation:main$1。此外,它被传递给了returnsUnit,然后传递给了returnsInt,最后存储在 c 变量中。

现在,让我们看看在构建状态机之前,codegen在 main$1 lambda中生成了什么:

text 复制代码
INVOKESTATIC InlineMarker.beforeInlineCall
ALOAD 1 // continuation
ICONST 0 // before suspending marker
INVOKESTATIC InlineMarker.mark(I)V
INVOKESTATIC returnsUnit()Ljava/lang/Object;
ICONST_2 // returns unit marker
INVOKESTATIC InlineMarker.mark(I)V
ICONST 1 // after suspending marker
INVOKESTATIC InlineMarker.mark(I)V
INVOKESTATIC InlineMarker.afterInlineCall
INVOKEVIRTUAL println(Ljava/lang/Object;)V
GETSTATIC kotlin/Unit.INSTANCE
ARETURN

我将内联的 println 替换为了一个调用,以便更清晰地理解。在转换为状态机之后,代码如下所示:

kotlin 复制代码
fun invokeSuspend($result: Any?): Any? {
    when (this.label) {
        0 -> {
            this.label = 1
            $result = returnsUnit(this)
            if ($result == COROUTINE_SUSPENDED) return COROUTINE_SUSPENDED
            goto 1
        }
        1 -> {
            println($result)
            return Unit
        }
        else -> {
            throw IllegalStateException("call to 'resume' before 'invoke' with coroutine")
        }
    }
}

在恢复协程后(其 label 值为 1),$result42,并将被打印出来。没错,一个返回 Unit 的函数似乎返回了非 Unit 的值。为了解决这个问题,我们将返回单位标记替换为 POP; GETSTATIC kotlin/Unit.INSTANCE 序列。这样,我们就忽略了传递给 resume 的值,就像没有挂起一样。顺便说一句,在 callSuspendcallSuspendBy 函数中,我们也做同样的事情。

然而,我们并不总是能够进行替换,就像下面的例子所示:

kotlin 复制代码
import kotlin.coroutines.*

var c: Continuation<*>? = null

suspend fun <T> tx(lambda: () -> T): T = suspendCoroutine { c = it; lambda() }

object Dummy

interface Base<T> {
    suspend fun generic(): T
}

class Derived: Base<Unit> {
    override suspend fun generic() {
        tx { Dummy }
    }
}

fun builder(c: suspend () -> Unit) {
    c.startCoroutine(object: Continuation<Unit> {
        override val context = EmptyCoroutineContext
        override fun resumeWith(result: Result<Unit>){
            result.getOrThrow()
        }
    })
}

fun main() {
    var res: Any? = null

    builder {
        val base: Base<*> = Derived()
        res = base.generic()
    }

    (c as? Continuation<Dummy>)?.resume(Dummy)

    println(res)
}

在这个例子中,我们无法确定 generic 是否返回 Unit。在这种情况下,编译器禁用尾调用优化。更一般地说,如果函数重写了返回非 Unit 类型的函数,那么编译器会禁用返回 Unit 的函数的尾调用优化。

Returning Inline Classes

在 1.4 之前,如果一个挂起函数返回一个内联类,那么该类的值会被装箱。对于包含引用类型的内联类来说,这是不可取的,因为它会导致额外的分配。因此,如果编译器可以验证调用者返回一个内联类,那么它就不会在调用者中生成装箱指令,并且在调用方中也不会生成拆箱指令。否则,调用者将返回一个装箱的值,就像下面的示例中一样:

kotlin 复制代码
inline class IC(val a: Any)

interface I {
    suspend fun overrideMe(): Any
}

class C : I {
    override suspend fun overrideMe(): IC = IC("OK")
}

suspend fun main() {
    val i = C()
    println(i.overrideMe())
}

在这里,编译器无法验证调用点总是期望内联类。因此,overrideMe 总是对类进行装箱。

然而,这种优化并不像看起来那么简单。挂起调用的执行有两条路径:直接路径,当调用方返回给调用者时;和恢复路径,当调用方返回给 invokeSuspend,然后返回给 resumeWith,后者调用 completion.resumeWith,它调用 invokeSuspend,它调用调用方。在直接路径中(最常见的情况),类被解包。

然而,在恢复路径中,我们应该对内联类进行装箱(在这种情况下,我们不太关心性能)。 BaseContinuationImpl.resumeWith 调用 invokeSuspend,它期望 invokeSuspend 的返回类型是 "T | COROUTINE_SUSPENDED",其中 T 是此处的装箱内联类。如果违反了这个契约,就会导致以下示例中抛出异常:

kotlin 复制代码
import kotlin.coroutines.*

fun main() {
    builder {
        signInFlowStepFirst()
    }
    continuation!!.resumeWithException(Exception("BOOYA"))
}

fun builder(c: suspend () -> Unit) {
    c.startCoroutine(object : Continuation<Unit> {
        override val context: CoroutineContext
            get() = EmptyCoroutineContext

        override fun resumeWith(result: Result<Unit>) {
            result.getOrThrow()
        }
    })
}

var continuation: Continuation<Unit>? = null

suspend fun suspendMe() = suspendCoroutine<Unit> { continuation = it }

@Suppress("RESULT_CLASS_IN_RETURN_TYPE")
suspend fun signInFlowStepFirst(): Result<Unit> = try {
    Result.success(suspendMe())
} catch (e: Exception) {
    Result.failure(e)
}

导致错误的解释并不简单:

  1. signInFlowStepFirst 调用 suspendMe 并暂停。
  2. 我们使用异常恢复了执行。
  3. signInFlowStepFirst 中,我们使用 Result 类包装了异常,就像包裹在一块卷饼中一样。
  4. 由于这是恢复路径(我们恢复了执行),执行返回到 invokeSuspend,它将 Result$Failure 返回给 BaseContinuationImpl.resumeWith
  5. BaseContinuationImpl.resumeWith 使用另一个 Result 包装了 Result$Failure,但由于 Result 是内联类,操作的结果(双关语不是故意的)是相同的 Result$Failure
  6. BaseContinuationImpl.resumeWith 调用 completion.resumeWith,将 Result$Failure 作为参数传递,这被视为 completionresumeWithException

所以,如果函数返回内联类且编译器已经优化了装箱,我们需要在 invokeSuspend 中对内联类进行装箱,以及在可调用引用中。这样修复了 invokeSuspend 的协程约定。

然而,在直接路径中,生成的代码期望是一个非装箱的值。因此,在调用者的恢复路径中,我们应该对其进行非装箱处理。我们可以在 invokeSuspend 和状态机中的取消溢出处对其进行非装箱处理。考虑以下代码片段:

kotlin 复制代码
import kotlin.coroutines.*

inline class IC(val a: Any)

fun builder(c: suspend () -> Unit) {
    c.startCoroutine(Continuation(EmptyCoroutineContext) { it.getOrThrow() })
}

var c: Continuation<Any>? = null

suspend fun returnsIC() = suspendCoroutine<IC> { c = it as Continuation<Any> }
suspend fun returnsAny() = suspendCoroutine<Any> { c = it }

suspend fun test() {
    println(returnsIC())
    println(returnsAny())
}

fun main() {
    builder {
        test()
    }
    c?.resume(IC("OK1"))
    c?.resume("OK2")
}

这里,我们两次恢复 test 函数,一次使用内联类,另一次使用普通类。然而,我们只需要在第一次恢复时对值进行装箱:在第一次恢复期间。这意味着如果我们想要进行装箱,我们需要在 invokeSuspend 中添加复杂的逻辑。在状态机内部进行装箱会更简单。

Unbox Inline Class Markers

这个状态机部分解释了编译器如何将顺序代码转换为状态机。简单来说,它在挂起点周围生成标记。因此,如果我们想要从代码生成器传递信息到状态机构建器或内联器,一些标记是必不可少的。自然地,我们为在恢复路径上需要生成的拆箱序列生成标记。

考虑以下示例。

kotlin 复制代码
inline class IC(val s: String)

suspend fun ic() = IC("OK")

suspend fun main() {
    println(ic().s)
}

代码生成器必须为 main 函数中的 IC 类生成一个拆箱序列,因此类构建器将其移动到恢复路径,因为 main 不是尾调用函数,因此具有状态机,而不像 ic 那样。此外,它应该告诉构建器这些指令要移动。因此,它将它们用新的标记包围起来,现在的标记id是 89

text 复制代码
INVOKESTATIC ic(Lkolint/coroutines/Continuation;)Ljava/lang/Object;
BIPUSH 8
INVOKESTATIC kotlin/jvm/internal/InlineMarker(I)V
CHECKCAST LIC;
INVOKEVIRTUAL IC.unbox-impl()Ljava/lang/String;
CHECKCAST Ljava/lang/Object;
BIPUSH 9
INVOKESTATIC kotlin/jvm/internal/InlineMarker(I)V

请注意 CHECKCAST Ljava/lang/Object; 是结束标记的一部分。我们需要生成它,以免干扰字节码分析。否则,字节码分析会认为挂起调用的返回类型是 String,而不是 Any?。将拆箱移动到恢复路径后,构建器会删除此转换。

通过这种方式传递信息也适用于内联。考虑以下示例:

kotlin 复制代码
import kotlin.coroutines.*

// Library lib1
inline class IC(val a: Any)

var c: Continuation<Any>? = null

suspend fun returnsIC() = suspendCoroutine<IC> { c = it as Continuation<Any> }

// Library lib2 depends on lib2

suspend inline fun inlineMe() {
    println(returnsIC())
}

// Main module

fun builder(c: suspend () -> Unit) {
    c.startCoroutine(Continuation(EmptyCoroutineContext) { it.getOrThrow() })
}

suspend fun test() {
    inlineMe()
}

fun main() {
    builder {
        test()
    }
    c?.resume(IC("OK1"))
}

这里,inlineMe$$forInline 没有状态机,因此,直接路径类似于恢复路径。然而,代码生成器生成了标记,内联器内联了这些标记:

text 复制代码
ICONST_1
INVOKESTATIC kotlin.jvm.internal.InlineMarker.mark(I)V
// The suspension point
ICONST_2
INVOKESTATIC kotlin.jvm.internal.InlineMarker.mark(I)V
ICONST_8
INVOKESTATIC kotlin.jvm.internal.InlineMarker.mark(I)V
// After this marker, there should be a call to box-impl
INVOKEVIRTUAL IC.unbox-impl(Ljava/lang/Object;)LIC;
CHECKCAST Ljava/lang/Object;
BIPUSH 9
INVOKESTATIC kotlin/jvm/internal/InlineMarker(I)V

最后,在内联后,状态机生成器将取消装箱移到了恢复路径中。

相关推荐
hsx66613 小时前
Kotlin 协程中的 Dispatchers
kotlin
每次的天空17 小时前
Android-重学kotlin(协程源码第二阶段)新学习总结
android·学习·kotlin
stevenzqzq17 小时前
Kotlin 中主构造函数和次构造函数的区别
android·kotlin
开发者如是说20 小时前
言叶是如何对文件进行端到端加密的
android·kotlin·swift
小李飞飞砖20 小时前
kotlin
开发语言·单例模式·kotlin
小李飞飞砖20 小时前
kotlin中的冷流和热流
android·开发语言·kotlin
Kotlin上海用户组2 天前
Koin vs. Hilt——最流行的 Android DI 框架全方位对比
android·架构·kotlin
Kapaseker2 天前
当Object遇到Json你可能会碰到的坑
kotlin
RichardLai882 天前
Kotlin Flow:构建响应式流的现代 Kotlin 之道
android·前端·kotlin
程序员江同学2 天前
Kotlin/Native 编译流程浅析
android·kotlin