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 参数的类型与我们刚刚恢复时相同(请参阅下一节)。因此,函数有三种可能的调用情况:
- 从另一个挂起函数或挂起 lambda 直接调用
- 恢复
- 递归
因此,我们需要至少存储另一个位的信息。我们使用 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 布局如下:
- 超类:
kotlin/coroutines/jvm/internal/ContinuationImpl
- 包局部的 int 类型的
label
字段。考虑到函数使用了它,并且函数位于类外部,所以是包局部的。 - 包局部的字段用于存储溢出变量。同样是包局部的。
- 公共最终方法
invokeSuspend
,类型为(Ljava/lang/Object;)Ljava/lang/Object;
。它重写了BaseContinuationImpl
的invokeSuspend
方法,用于调用函数。 - 公共或包私有的构造函数
<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 = {}
}
这样做可以重用捕获变量的逻辑并简化代码生成的逻辑。然而,由于旧后端的限制,其 create
和 invoke
是未擦除的。换句话说,编译器复制它们,生成未擦除和擦除的副本。未擦除的副本接受带类型的参数,并包含 Lambda 的 invoke
或 create
的逻辑。另一个副本只接受 Any?
。 局部挂起函数的布局如下:
- 超类型:
kotlin/coroutines/jvm/internal/SuspendLambda
和kotlin/jvm/functions/Function{N}
- 包私有捕获变量
- 私有的整型
label
字段。私有的,因为它仅在 Lambda 本身中使用。 - 私有参数字段。可见性的原因与
label
字段相同。 - 用于溢出变量的私有字段。同样是私有的。
- 公共最终方法
invokeSuspend
,类型为(Ljava/lang/Object;)Ljava/lang/Object;
。它重写了BaseContinuationImpl
的invokeSuspend
。 - 公共最终
create
方法,类型为(<params>,Lkotlin/coroutines/Continuation)Lkotlin/coroutines/Continuation
。<params>
的类型被擦除。 - 公共最终
create
方法,类型为(<params>,Lkotlin/coroutines/Continuation)Lkotlin/coroutines/Continuation
。<params>
的类型未被擦除。 - 公共最终
invoke
方法,类型为(<params>,Ljava/lang/Object;)Ljava/lang/Object;
。<params>
被擦除。 - 公共最终
invoke
方法,类型为(<params>,Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
。<params>
未被擦除。 - 公共或包私有构造函数:
<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
检查函数是否是尾调用很简单:检查所有(可达的)挂起点是否:
- 不在 try-catch 块内
- 紧跟着的是带有可选分支或堆栈修改的 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 +--+
+-----------+
returnInt1
和 returnInt3
是尾调用,没有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()
}
在这个例子中,tailCall1
和 tailCall2
被普通的尾调用优化所覆盖。然而,最后一个函数则不同。代码生成器生成了以下字节码:
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
正如你所看到的,Unit
被 POP
出栈,然后被重新推送到栈上并返回。不幸的是,我们不能简单地移除 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
),$result
是 42
,并将被打印出来。没错,一个返回 Unit
的函数似乎返回了非 Unit
的值。为了解决这个问题,我们将返回单位标记替换为 POP; GETSTATIC kotlin/Unit.INSTANCE
序列。这样,我们就忽略了传递给 resume
的值,就像没有挂起一样。顺便说一句,在 callSuspend
和 callSuspendBy
函数中,我们也做同样的事情。
然而,我们并不总是能够进行替换,就像下面的例子所示:
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)
}
导致错误的解释并不简单:
signInFlowStepFirst
调用suspendMe
并暂停。- 我们使用异常恢复了执行。
- 在
signInFlowStepFirst
中,我们使用Result
类包装了异常,就像包裹在一块卷饼中一样。 - 由于这是恢复路径(我们恢复了执行),执行返回到
invokeSuspend
,它将Result$Failure
返回给BaseContinuationImpl.resumeWith
。 BaseContinuationImpl.resumeWith
使用另一个Result
包装了Result$Failure
,但由于Result
是内联类,操作的结果(双关语不是故意的)是相同的Result$Failure
。BaseContinuationImpl.resumeWith
调用completion.resumeWith
,将Result$Failure
作为参数传递,这被视为completion
的resumeWithException
。
所以,如果函数返回内联类且编译器已经优化了装箱,我们需要在 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是 8
和 9
。
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
最后,在内联后,状态机生成器将取消装箱移到了恢复路径中。