Coroutines and crossinline

Support for suspending “gets through” into our lambda only if it is fully inlined, meaning that the code of the lambda effectively becomes a part of the code of the suspend function. In your above example println() and delay() are actually compiled directly into the main() function (or rather to the lambda passed in runBlocking()).

To make the above possible, we have to make sure that the lambda is always used in-place, it can’t be stored, passed anywhere else, etc. It has to be invoked directly by the higher-order function (test() in your example). But sometimes we need to pass the lambda somewhere, so it can’t be fully inlined. Then we lose possibility to use local returns, suspending doesn’t get through, etc.

crossinline is somewhere in between. We don’t inline it inside test(), so the contents of the lambda don’t become a part of main() and therefore local returns and suspending doesn’t work, but we still would like to inline it in other places.

See the example below. First, we inline fully:

fun main() {
    foo {
        println("main:1")
    }
}

inline fun foo(block: () -> Unit) {
    println("foo:1")
    bar {
        println("foo:3")
        block()
        println("foo:4")
    }
    println("foo:2")
}

inline fun bar(block: () -> Unit) {
    println("bar:1")
    block()
    println("bar:2")
}

In this case everything is fully inlined into main(), so it becomes something like:

fun main() {
    println("foo:1")
    println("bar:1")
    println("foo:3")
    println("main:1")
    println("foo:4")
    println("bar:2")
    println("foo:2")
}

Ok, but let’s say bar() is not an inline function, so foo() has to pass its block somewhere else and can’t inline it. We could use noinline, so: inline fun foo(noinline block: () -> Unit) and in this case block is not at all inlined:

fun main() {
    println("foo:1")
    val block = {
        println("main:1")
    }
    bar {
        println("foo:3")
        block()
        println("foo:4")
    }
    println("foo:2")
}

Here, we have two separate lambdas. First code fragment was originally provided by main() and the second was provided by foo(). Second lambda is now passed to bar() and it invokes the first lambda.

Of course, this is unnecessary. we could construct a single lambda by merging both code fragments and then pass it to bar() as a whole. This is exactly what crossinline do. It says the code won’t be fully inlined into main(), so it can’t use features like local returns and suspending, but it still can be inlined in some places. The resulting code is like this:

fun main() {
    println("foo:1")
    bar {
        println("foo:3")
        println("main:1")
        println("foo:4")
    }
    println("foo:2")
}

Code block with main:1 can’t be inlined into foo(), because we have to pass it to bar() which isn’t inlined, but we still can inline main:1 into the foo:3/foo:4 lambda.

Summing up, crossinline is like saying: “Please inline this lambda wherever it is possible, but we can’t inline it directly into the use-site of the higher-order function”.

2 Likes