loading...

Handling SIGINT in Kotlin Native

mreichelt profile image Marc Reichelt Updated on ・5 min read

Just 2 weeks ago Patrick Lemke on the Kotlin Slack in the #kotlin-native channel asked an interesting question:

Original question by Patrick Lemke: How do I react to a SIGINT event in Kotlin Native?

I was intrigued by this question. A SIGINT is the event sent to a process if you press Ctrl + C in the shell, and that usually exits a process. So how would you actually handle events like SIGINT in Kotlin Native? πŸ€”

At first I thought this would be hard to implement. I tried to create a small example. On my way I not only managed to implement this, but discovered how Kotlin tooling and documentation can help you in solving similar problems. Onward!

How C/C++ handle SIGINT

At first it's probably a good idea to read about how these interrupts are actually created in C/C++. I searched for 'handle sigint c/c++' on Google and found a page on C++ Signal Handling. Basically, you define a function in C++ and you can register it as a signal handler by calling the signal function (that's the only C++ code I'll show here):

void signalHandler(int signum) {
    // your logic here when interrupt happens
}

int main () {
    // register your handler function - can be SIGINT, SIGABRT, or others
    signal(SIGINT, signalHandler);

    // ever-running code here that can/will be interrupted
}

Calling signal in Kotlin Native

Now let's find out how we can call that function in Kotlin Native! First, I opened IntelliJ, and created a new Kotlin Native project with Create New Project -> Kotlin -> Native | Gradle. This automatically creates a sample main file. Let's see if it can find a signal function. And jackpot - there it is, and it can be imported!

signal function from platform.posix in IntelliJ

And SIGINT and the other constants can be found as well:

SIGINT, SIGTERM, and more

I think this is tooling by IntelliJ/Kotlin is pretty amazing. It looks simple, but there is actually a whole lot going on under the covers. The POSIX bindings are created automatically for you and the platform(s) you target, and they are already indexed, waiting for you to type-and-complete.

Now, moving to the second parameter: we already know this is a lambda. But what the heck is a CPointer<CFunction<(Int) β†’ Unit>>? - and how do I get one? I had no idea, because I've never done this before in Kotlin (although I know what a pointer is). So I googled for 'kotlin cpointer cfunction', and landed on this excellent Kotlin documentation: Mapping Function Pointers from C

And there it is - scrolling down I find exactly what I need: Passing Kotlin Function as C Function Pointer Using that, we now know that Kotlin provides a helper function called staticCFunction. Let's use that!

So a trivial example might be that we print something forever. And then we will interrupt it. Here's my first, trivial implementation:

fun main() {
    signal(SIGINT, staticCFunction<Int, Unit> {
        println("Interrupt: $it")
    })
    while (true) {
        println("running")
        sleep(1)
    }
}

When we compile this (by clicking the Build button), it will create a binary with the name of our project and a .kexe ending. For me, it created a file under build/bin/macos/debugExecutable/kotlin-native-signal.kexe. I wanted to call the main method directly via the common 'green arrow run button', but that wasn't there. For now, we'll use the commandline - but I hope JetBrains continues to improve the tooling around Kotlin Native (see that section further down).

So let's call the program in a shell, and press Ctrl + C after 2 seconds to interrupt it:

$ build/bin/macos/debugExecutable/kotlin-native-signal.kexe
running
running
^CInterrupt: 2
running
running
running
[program continues, does not exit]

Nice, it does handle the interrupt! πŸ™‚

But we didn't actually exit the program in our interrupt handler - so now the program will continue forever. Unless we kill it with killall… 😈

# in a second shell:
$ killall kotlin-native-signal.kexe

# in the first shell this will kill the program:
[1]    66077 terminated  build/bin/macos/debugExecutable/kotlin-native-signal.kexe

To improve this program, we can call the (POSIX function!) exit in the interrupt. Also, we can use staticCFunction to create a CPointer reference to an existing Kotlin function as well. Here's the improved version, with imports added for reference:

import kotlinx.cinterop.staticCFunction
import platform.posix.SIGINT
import platform.posix.exit
import platform.posix.signal
import platform.posix.sleep

fun handleSignal(signalNumber: Int) {
    println("Interrupt: $signalNumber")
    exit(0)
}

fun main() {
    signal(SIGINT, staticCFunction(::handleSignal))
    while (true) {
        println("running")
        sleep(1)
    }
}

Finally, when we interrupt the program with Ctrl + C, it will run our handler code and exit.

IntelliJ: A tooling improvement suggestion

When I wrote this blogpost I wanted to run the main method directly from the editor. But where usually I expect to find a green run icon of happiness there was just an empty grey area:

Missing run icon in IntelliJ Community edition

At first I tried the free community edition of IntelliJ. Next, I tried IntelliJ Ultimate. I opened the same project. Ultimate suggested I install a plugin, so I did that:

IntelliJ Ultimate: Plugin for Kotlin Native

Nevertheless: even after doing that and restarting the IDE, Ultimate didn't show me the green run button. In the 'Run Configurations', I got a new entry 'Kotlin/Native Application' - but it didn't detect my Kotlin Native executable, so I couldn't select that.

Finally, I tried CLion. I installed an additional plugin for Kotlin Native. Still, no run icon to the left of the main method. But at least I got a run configuration that way, and I could run & debug the Kotlin Native program from the IDE! πŸŽ‰

CLion: debug a Kotlin Native program

So for tooling, I have the following wishes:

  • I'd love to have that green run icon. It's there in C/C++ programs in CLion and in all Java projects, so why not here?
  • IntelliJ Community could make the suggestion that run/debug is only supported in the commercial tools.
  • IntelliJ Ultimate and CLion should behave consistently. What works in CLion should work in Ultimate as well.

Conclusion

This was a fun ride! It's so amazing that we can use POSIX APIs and more (iOS & MacOS libs, etc.) in Kotlin Native - and the coding tooling is great already. There are still some things that could be improved. Then again, Kotlin Native is still a relatively new technology. But it's already on a good way to becoming stable!

If you liked this post, please give it a ❀️ and follow me on Twitter!

Posted on by:

mreichelt profile

Marc Reichelt

@mreichelt

Developer. Loves Linux, Android & open source. Making the world better, one step at a time!

Discussion

markdown guide