Kotlin coroutines and GTK

A week ago I rewrote the lcarsde status-bar to use Kotlin/native and GTK. Here are some lessons learned.

So the python version of the status-bar was giving me some trouble. I will not go into the details but in the end I decided to rewrite it to Kotlin/Native. The python variant was using GTK and its widgets that were updated in a separate thread. Because that previous version gave me some knowledge about how to work with GTK (and because it has C libraries that work with Kotlin\Native) I decided to use it again. I was looking forward to the rewrite because I could simply use coroutines and dump the extra thread.

So how does a general GTK application in Kotlin\Native look like? Here is an example:

fun main() {
  gtk_init(cValuesOf(0), cValue())

  // set up your window and stuff

  g_signal_connect_data(window, "destroy", (staticCFunction { _: CPointer<GtkWidget> -> gtk_main_quit() }).reinterpret(), null, null, 0U)

  gtk_widget_show_all(window)

  gtk_main()
}

So far so good. This works great when all you need to to is to react to user input or signals that are processed inside of GTK. What if you want to trigger asynchronous processing with coroutines. By the way, why coroutines? Coroutines run on the same thread. So there is no need to create another thread and there is no headache in terms of locking resources or using transactions. Back to the topic, I was going to use something like this:

fun main() = runBlocking {
  gtk_init(cValuesOf(0), cValue())

  // set up your window and stuff

  g_signal_connect_data(window, "destroy", (staticCFunction { _: CPointer<GtkWidget> -> gtk_main_quit() }).reinterpret(), null, null, 0U)

  gtk_widget_show_all(window)

  val job = launch {
    while(true) {
      delay(100)
      doSomeUpdates()
    }
  }

  gtk_main()

  job.cancel()
}

Well, as it turns out that this doesn't work. Why? Because the job will never run. The reason is that gtk_main, which runs the GTK main loop, check for signals and does so in a blocking manner. Therefore, the one thread that the coroutines are supposed to run on is always blocked and there's no time for the coroutine left. Because there is no error message, it took me a little while to figure out what exactly went wrong. However, the solution is rather simple. GTK offers functions to manually check for signals and as a result allows you to create your own main loop, which make it look like this:

var stop = false

  fun main() = runBlocking {
  gtk_init(cValuesOf(0), cValue())

  // set up your window and stuff

  g_signal_connect_data(window, "destroy", (staticCFunction(::destroy)).reinterpret(), null, null, 0U)

  gtk_widget_show_all(window)

  val job = launch {
    while(true) {
      delay(100)
      doSomeUpdates()
    }
  }

  while(!stop) {
    while (gtk_events_pending() != 0) {
      gtk_main_iteration()
    }
    delay(50)
  }

  job.cancel()
}

fun destroy() {
  stop = true
}

As you see, I replaced gtk_main() with this double while loop. The outer one is the actual main loop. The inner while is for handling a batch of signals at once for better responsiveness to user actions. That might delay the coroutine a little every now and then, but I don't consider this an issue in the status bar application. This main loop calls the unblocking function gtk_main_iteration() repeatedly and takes 50ms brakes if there are no events to process. In these brakes the thread is really idle and that gives time to the coroutine for running.

Anything else? Ah yes, chances are that we use classes and want to use instances of the classes in the functions that are called when processing a signal. Because the signal handlers are static C functions, we are not allowed to hand them in directly. But we can do so with stable references. Here is how to do this:

  // ... stuff ...

  val something = Something()
  val somethingRef = StableRef.create(something)

  g_signal_connect_data(someButton, "clicked",
      (staticCFunction { _: CPointer<GtkWidget>, p: COpaquePointer -> handleClick(p) }).reinterpret(),
      somethingRef.asCPointer(), null, 0U)

  // ... stuff ...

fun handleClick(somethingP: COpaquePointer) {
  val something = somethingP.asStableRef<Something>().get()

  // ... stuff ...
}

By turning the class instances into stable references and handing them as data into the connect function, they can be forwarded into the static methods.

That's all, have fun using Kotlin\Native with GTK. Feel free to check out the status-bar application code on GitHub.