Coroutines and the JS Compiler


#1

Hi everyone!

I am currently re-doing the kotlinx.coroutines library (or at least a really simplified version) in order to learn more about how coroutines really work and how to use them correctly. I would really like to make that librairy multiplateform and I already build the JVM part (works like a charm).

I struggle now to understand the result of the JS compiler as it adds continuations parameter to my suspending functions

Kotlin:

suspend fun delay(delay : Long): Unit = suspendCoroutine { cont ->
  if(cont.context[SuspentionTimingKey] is SuspentionTiming){
      if(delay>0){
        (cont.context[SuspentionTimingKey] as SuspentionTiming).shouldResumeAt = getCurrentMillis()+delay;
      }
   }

   Dispatcher.dispatch(cont,Unit);
}

suspend fun suspend(){
   delay(0);
}

JS Output:

function delay$lambda(closure$delay) {
    return function (cont) {
      var tmp$;
      if (Kotlin.isType(cont.context.get_8oh8b3$(SuspentionTimingKey_getInstance()), SuspentionTiming)) {
        if (closure$delay.compareTo_11rb$(Kotlin.Long.fromInt(0)) > 0) {
          (Kotlin.isType(tmp$ = cont.context.get_8oh8b3$(SuspentionTimingKey_getInstance()), SuspentionTiming) ? tmp$ : throwCCE()).shouldResumeAt = getCurrentMillis().add(closure$delay);
        }
      }
      Dispatcher_getInstance().dispatch_n6qzss$(cont, Unit);
      return Unit;
    };
}
var SafeContinuation_init = Kotlin.kotlin.coroutines.experimental.SafeContinuation_init_n4f53e$;
  function suspendCoroutine$lambda(closure$block) {
    return function (c) {
      var safe = SafeContinuation_init(c);
      closure$block(safe);
      return safe.getResult();
    };
  }

  function delay(delay, continuation) {
    return suspendCoroutine$lambda(delay$lambda(delay))(continuation.facade);
  }

  function suspend(continuation_0, suspended) {
    var instance = new Coroutine$suspend(continuation_0);
    if (suspended)
      return instance;
    else
      return instance.doResume(null);   
   }
}

Where does that continuation parameter come from? am I doing something wrong?

I also have an error saying that $receiver is undefined in the main kotlin.js file, which is wierd as I do not use any receiver in any of my coroutines for now.

Again my lib works just fine with Java, what am I doing wrong for it to fail in Js ?


#2

As a first approximation you can think it is a callback on steroids.

For a proper explanation here is a nice video about the coroutine implementation in Kotlin.
Also I highly recommend watching this video first. It gives a lot of insight on the reasoning behind the design choices and how Kotlin approach compares to others.

Hope that helps.


#3

Regarding the library working on JVM and not on JS. Could you share some more information? Ideally a self-contained example. A stacktrace of the $receiver is undefined error might be a good start as well.


#4

Thank you for the videos, I’m gonna watch them soon (and hopefully improve my design)

As for the $receiver problem I’m gonna detail my lib a bit.

You call an async() function that takes a context and a suspending lambda as parameters (or you can omit the context, a default context will be provided,nothing new), the newly created coroutine is sent to a singleton called Dispatcher, this dispatcher will look into the coroutine context and get the Worker instance then send the coroutine to this worker, the worker simply call continuation.resume(value).

If the coroutine needs to be suspended for a certain time the dispatcher will also read a timestamp and send the coroutine to another singleton (Scheduler), the scheduler will send back the coroutine to the dispatcher when its time has come. Of course there is a ContinuationInterceptor that send the continuation to the dispatcher and so on.

This design is a bit clumsy but it worked just fine until now.

question: Is using a receiver for the coroutines utility functions (delay, suspend or other) a better way to go than having standalone functions (I belive kotlinx is doing like that)?

for the stack trace the code is like so:

window["koroutine-js"].koroutine.common.async_r55nsn$(function(){
    var i = 5;
    while(i>0){
	    console.log("aze");
	    i--;
            //obviously dosen't work because of the extra parameter created by the compiler
	    //window["koroutine-js"].koroutine.common.suspend();
    }
});

Note: WIth or Without the suspend() part the coroutine execute fine until the first suspention (print one “aze”)

I just noticed the error is different in Firefox and in Chrome. I’m gonna detail each case

Firefox without suspend():

TypeError: $receiver(…) is undefined
the coroutine print one “aze”

Firefox with suspend:
TypeError: this.resultContinuation_0 is undefined
the coroutine print one “aze”

Chrome without suspend()
kotlin.js:3691 Uncaught TypeError: Cannot read property ‘facade’ of undefined
but the coroutine execute itself until the end (maybe because it has not been suspended and the error happen when calling the final Continuation.resume() )

Chrome with suspend()
Uncaught TypeError: Cannot read property ‘context’ of undefined
print one “aze”

As always Javascript in all its beauty, not a single browser gives the same error.

Obviously the errors all occur when the coroutine needs to suspend.For the stack traces I don’t know how you want me to send them as the text isn’t really usefull so I send you the code I used for my tests:

test.7z (107.9 KB)

Thank you so much for your time, I’m gonna watch those conf and try to modify my lib accordingly, maybe that will fix my JS problem


#5

Alright I watched the two conferences (great job by the way) and as I understand it, Kotlin2JS just does the same thing than Kotlin2JVM, it adds a continuation parameter to the suspending function, the difference is that in the JVM this is transparent and doesn’t need an explicit call with the continuation parameter so why does the Kotlin2JS compiler changes the signature of my function with an explicit continuation parameter?

Correct me if I’m wrong but it looks like a bug to me.

After seeing the second video taking a more in-depth look at the coroutines implementation I think I’m doing it right in my lib (that explain why it is working on the JVM) so I can’t really find any explanation of the behaviour of Kotlin2JS


#6

Thanks for the explanation, I seem to understand your question better now.

Kotlin/JS is indeed doing exactly the same thing as Kotlin/JVM - adds an additional continuation parameter. What you’ve missed though is that calling this suspend function does actually need an explicit continuation parameter in JVM as well.

For example try using a suspend fun foo() {} from Java. You’ll notice that it’s signature is actually foo(Continuation<? super Unit> continuation).

So you are going to run into the same problem when using suspend functions from plain Java. The difference is that it would result in a compilation error in Java, whereas JS happily passes undefined for the missing arguments.

Why don’t you have to specify continuation explicitly when you call suspend function from Kotlin? That’s because the compiler does that for you. If you try calling some suspend function from a regular function, the compiler will say something like Suspend function 'foo' should be called only from a coroutine or another suspend function. The reason for that is simple: the compiler doesn’t know what continuation to pass as an argument.

You might say that suspend functions were not designed to be used from the platform (e.g. Java or Javascript). You can still implement a custom continuation if you absolutely have to, but it will be a pain.

How are you supposed to interract with the platform code then? Well, you expose and consume API that’s idiomatic to the platform (e.g. callback, Promise, Future, etc.). On the Kotlin side you use/write functions that glues together the platform API and coroutines. This way you can get both nice interoperability and the coroutine goodness.

Moving on to the exceptions.
It seems that async_r55nsn$ expects a suspend lambda, and you are giving it a regular callback.
Also you are trying to invoke a suspend function ‘suspend’ without giving it a valid continuation.
So it boils down to using suspend function as if they were regular ones. Which they aren’t.

What I would advise doing is either

  1. Use you Kotlin/JS library from Kotlin/JS. The compiler will do all the work for you.
  2. Create a “regular” API for interop with JS. For example you might have something like
    @JsName("async")
    fun asyncJs(block: () -> Unit) {
        async { // Wrap callback `block` into a `suspend` lambda
            block() 
        }
    }  
    
  3. (Hardcore) Implement continuations by hand. I don’t advise going this route, unless you want the ‘Kotlin coroutine master’ title. :slight_smile: Also Kotln/JS doesn’t guarantee binary compatibility (even though we strive for it), so this approach is extremely brittle.

#7

Ooooh! Now I get it!

Yeah that makes much more sense now, so if i tried to compile my lib for java and then use it from there I would have encountered the same issues right?

I think I’m gonna go with solutions number 1 and 2, the coroutine master title is completely out of reach for me :slight_smile: , I might still take a look at the output code from KotlinJS to see how those coroutine parameters are provided.

I still have trouble to find a way to make suspend() usable directly in JS, do you have any start point I could use to understand it ?

Again thank you very much for the incredible support and patience!


#8

Yeah that makes much more sense now, so if i tried to compile my lib for java and then use it from there I would have encountered the same issues right?

Exactly. Actually doing that might be worthwhile, because you will see all the hidden arguments and types exposed. Could be good for understanding.

I still have trouble to find a way to make suspend() usable directly in JS, do you have any start point I could use to understand it ?

Using anything with suspend in its signature directly from the platform code will be always hard unless you know exactly what you are doing. And most likely pointless. What you want is to expose an API, which works well with whatever asynchronous techniques you are using on the platform.

Basically you need to think how to tie you platform-specific asynchronous mechanisms with coroutines. Like how are you going to suspend your whole JS computation? Are you going to pass the rest as a callback? Or are you going to await on the invocation? Depending on the answer you are going to use the function differently. You could wrap you suspend function into a function that returns a Promise. Or take a callback and execute it after the suspend function has finished. It all depends.