Cross Compilation Adventures with Kotlin Native

Cross Compilation Adventures with Kotlin Native

December 11, 2023·
Nishant Srivastava

Banner

ℹ️

This post is part of a series.

TLDR; I want to build cross-platform CLI utility tools that can be compiled on my laptop and run seamlessly on other platforms.

I am an Android Engineer by profession, so my goto language is Kotlin. While looking at other programming languages, I also wanted to take a look at building a CLI tool using Kotlin. Thankfully, there exists Kotlin/Native that does exactly the same.

From the official website:

  • Kotlin is a modern but already mature programming language designed to make developers happier. It’s concise, safe, interoperable with Java and other languages, and provides many ways to reuse code between multiple platforms for productive programming.
  • The Kotlin/Native compiler is available for three operating systems: macOS, Linux, and Windows. It can be accessed through the command line or as part of the standard Kotlin distribution, which can be downloaded from GitHub Releases. The compiler supports various targets, including Linux, macOS, iOS, and others. Additionally, it allows cross-platform compilation, enabling developers to compile code for a different platform than the one they are using.

Sounds good! Let’s dive into building a very basic CLI tool.

You will build the same example as in the last post.

A good example to showcase would be to build a CLI tool that can convert from °C to F and vice versa. Our tool will take an input for value and the unit to be converted to, then output would be converted temprature value.

NOTE: I am using macOS (M2 Pro, Apple Silicon), so the instructions follow through using that only. However the steps should work on all platform with little tweaks.

First we need to install kotlin and kotlin-native. Open your Terminal app and execute the command

brew install kotlin
brew install --cask kotlin-native

Once installed, you should have access to kotlinc-native compiler in your Terminal. If not restart your session or open a new Terminal window so it is loaded in the PATH. Follow through next steps

  • Create a file named run.kt.

    touch run.kt
  • Add the below code to the run.kt file and save the file.

    fun celsiusToFahrenheit(celsius: Double): Double {
        return celsius * 9 / 5 + 32
    }
    
    fun fahrenheitToCelsius(fahrenheit: Double): Double {
        return (fahrenheit - 32) * 5 / 9
    }
    
    fun main(args: Array<String>) {
        if (args.size != 2) {
            println("Usage: ./run.kexe <value> <unit_to_convert_to>")
            return
        }
    
        val value = args[0].toDoubleOrNull()
        val unit = args[1].uppercase()
    
        if (value == null) {
            println("Invalid temperature value.")
            return
        }
    
        val convertedTemperature = when (unit) {
            "C" -> celsiusToFahrenheit(value)
            "F" -> fahrenheitToCelsius(value)
            else -> {
                println("Invalid unit. Please use C or F.")
                return
            }
        }
    
        println("Converted temperature: $convertedTemperature${if (unit == "C") " °F" else " °C"}")
    }

    I am not going to explain this code as it is simple and self explanatory.

    To understand and learn the language you can use Learn X in Y minutes: Kotlin 🚀

  • Now to compile, execute the kotlinc-native compiler with -oargument with the name of output file and the run.kt source file:

    kotlinc-native run.kt -o run

    You should now have a binary generated in the same directory with the same name as the kt file i.e run.kexe

    run

    NOTE: I use dust CLI tool to list files in directory with their sizes. TIP: You can generate an optimized binary by passing -opt flags at the time of compilation. i.e kotlinc-native run.kt -o run -opt. Result is just a smaller binary.

    run optimized

  • Time to execute our generated run.kexe binary file:

    ❯ ./run.kexe
    Usage: ./run.kexe <value> <unit_to_convert_to>

    Didn’t work 🙄, but we have a helpful message stating how to use the CLI tool 😊

    ❯ ./run.kexe 49 C
    Converted temperature: 120.2°C

Done! That was a super quick intro to working with Kotlin/Native Compiler and Kotlin Language in less than 5 mins 😅

But we aren’t done yet. This generated binary would work on *nix systems. I mentioned earlier that I would like to have cross-(platform + compilation).

Kotin/Native allows to do that easily. Since we already have *nix compatible binary i.e Linux and macOS are sorted for us. We need to cross compile to a format that Windows understands i.e exe/executable. Let’s do that next.

  • First install the mingw-w64 toolchain using homebrew for macOS:

    brew install mingw-w64
  • Compile the run.kt file with -target mingw flag:

    kotlinc-native run.kt -o run.exe -target mingw

    You should now have a .exe binary generated in the same directory with the same name as the kt file i.e run.exe

    run.exe

    TIP: You can generate an optimized binary by passing -opt flags at the time of compilation. i.e kotlinc-native run.kt -o run.exe -target mingw -opt. Result is just a smaller binary.

    run.exe optimized

    NOTE: In order to run this .exe file, you need to either execute this on Windows directly or if on a *nix system then make use of Wine.

Thats it. I think Kotlin/Native and Kotlin Language pretty much does what I wanted to get out of it:

Generate cross-platform binariesCan cross-compile to platformsEasy syntax, so maintainable code

All check boxes ticked is good 😊

The only drawback that I saw was that the binary size (even after using the -opt flag) was considerably bigger than when I generated the same using Nim Lang. But this is not a big concern for my usecase. Being able to use a programming language that I am highly familiar with overshadows the size drawback for my usecase atleast.

BONUS: While my requirement isn’t about compiling to other platforms, but Kotlin/Native is quite capable such as compiling for Android, iOS, watchOS, tvOS, etc.

I’ll be trying this approach of evaluating more languages in the future. You can find the code for this post here.

Last updated on