Exploring Kotlin/Native – Part 1

I initially became interested in Kotlin/Native for its potential multiplatform capabilities. Multiplatforming can reduce the amount of work and money expended on a planned software project. If you are looking to build a full-fledged software product team or planning to build a professional consumer software product, development costs can quickly sky-rocket if custom code is crafted for multiple different platforms. However, software designed to be multiplatform can potentially result in huge cost savings, in addition to helping you garner a much larger user base.

After all, Java’s phenomenal widespread popularity (and profitability) in the enterprise world was fueled by its ability to run on any machine, eliminating the need to replicate software tailored to different hardware platforms. The creators of Java, Sun Microsystems, had a memorable and catchy slogan: “Write once, run anywhere.” This implied that Java could be written by anyone and run on any device — so long as that device was equipped with a Java Virtual Machine (JVM).

This caveat can prove to be a costly obstacle because, for some devices, the need to have a JVM pre-installed is impractical or impossible. Embedded devices with limited memory may not have the capacity to house a JVM. Some operating systems, such as iOS, simply do not allow it.

The Kotlin compiler has a method to compile the language to native binaries of several popular operating systems, eliminating the need for a special virtual machine to be installed on the device. For a handful of supported operating systems, the Kotlin compiler can remove the need to download and install a virtual machine in order to run your Kotlin program. In theory, this means that you can write a Kotlin program and compile it to produce eight different executable files that can run natively on each of their respective platforms, right?

In this blog series, we will explore Kotlin/Native in-depth: What it is, what it can do, and what it can’t.

Expectation:

Through my research, I found that some Kotlin/Native target platforms must be built on the appropriate host machine. Windows targets, for example, must be built on a Windows host. So you cannot create native binaries for all eight platforms using only one host machine.

Figure 1. Informational message received when building a Kotlin/Native project in IntelliJ IDEA. The Windows Kotlin/Native target cannot be built on a host running MacOS.

Since I am operating on a MacBook, let’s instead explore creating native binaries that can be compiled from MacOS, i.e. Mac, Linux, and iosARM native binaries.

Test Driving Kotlin/Native

The Kotlin/Native compiler is already included in the Kotlin/Native distribution in the form of a command line tool. Using the command line is straight-forward when compiling for a target platform that is the same as the host machine, but we are trying to take one Kotlin source file and compile it down to eight native binary files that can run on the eight supported hardware platforms. Additionally, compiling one Kotlin source code file as opposed to compiling an entire project with multiple Kotlin source code files is unlikely in the real world. In the scenario where multiple source files and libraries need to be compiled, it is easier to use an IDE and Gradle build system. IntelliJ is a great choice because it allows you to compile multiple source files in a project at once, and can allow you to target multiple platforms for the compilation of those source files simultaneously by using Gradle. Because it is overall more practical to use the Kotlin/Native compiler via IntelliJ, we will also be using IntelliJ to accomplish our objective.

I followed the instructions on Creating a new Kotlin/Native project in IntelliJ IDEA, but added support for the other platforms in the Gradle file in the Kotlin block:

plugins { id 'org.jetbrains.kotlin.multiplatform' version '1.3.61' } repositories { mavenCentral() } kotlin { // For ARM, should be changed to iosArm32 or iosArm64 // For Linux, should be changed to e.g. linuxX64 // For MacOS, should be changed to e.g. macosX64 // For Windows, should be changed to e.g. mingwX64 macosX64("macos") { binaries { executable { // Change to specify fully qualified // name of your application's entry point: entryPoint = 'sample.main' // Specify command-line arguments, if necessary: runTask?.args('') } } } linuxX64("linux") { binaries { executable { entryPoint = 'sample.main' runTask?.args('') } } } mingwX64("windows") { binaries { executable { entryPoint = 'sample.main' runTask?.args('') } } } iosArm64("ARM") { binaries { executable { entryPoint = 'sample.main' runTask?.args('') } } } sourceSets { // Note: To enable common source sets please // comment out 'kotlin.import.noCommonSourceSets' property // in gradle.properties file and re-import your project in IDE. macosMain { } macosTest { } } } // Use the following Gradle tasks to run your application: // :runReleaseExecutableMacos - without debug symbols // :runDebugExecutableMacos - with debug symbols

I tried to build but was hit with this error:

Execution failed for task ':compileKotlinMacos'. > Process 'command '/Applications/IntelliJ IDEA CE.app/Contents/jbr/Contents/Home/bin/java'' > finished with non-zero exit value 2

If you see this error message, you may be missing Xcode Command Line Tools. Here’s how you can set them:

Open Xcode. In the top menu bar, navigate to Xcode > Preferences. In the window that opens, select the Locations tab in the top menu bar.

In the dropdown menu next to “Command Line Tools,” select the latest version available. This should fix the issue.

Now it is time to build. Hit the green, hammer-shaped build button towards the top right of the IDE window.

The program should compile without issue. You may notice that nothing seemed to happen. No new folders were generated for our platforms. What gives?

Once you build the program, the IDE compiles the source code and produces an executable file. You can find this executable file in the project’s build files.

Figure 2. The executable file is placed in the ./build/bin/macos/releaseExecutable directory of your project files.

Assuming you are currently in your project root file, you can go to ./build/bin/macos and you’ll be able to find your runnable executable file in releaseExecutable. You can even run it directly in the terminal.

Alternatively, you can run the program in the IDE. To run the program in the IDE, double click the runReleaseExecutableMacos task in the Gradle tasks menu. To get to the Gradle task menu, click the Gradle side tab located on the right side of the IDE window. Open your project’s drop-down and then open Tasks > run and you should find the run task for the MacOS executable.

As you can see in the Run view, the executable ran successfully!

Creating Multiple Native Binaries from a Common Source

Next, we will create a common source module. Comment out the kotlin.import.noCommonSourceSets=true line in the gradle.properties file.

Next, in the project view located in the left hand drawer menu, create a new Directory.

Once you click Directory, a pop-out options menu should appear. Select commonMain/kotlin and commonTest/kotlin and hit enter. To select multiple items, hold down the control key on Windows or command key on Mac while selecting the items with your mouse.

You should see some new folders appear: commonMain and commonTest.

These folders are for our common source code. We will place our Kotlin program in the commonMain/kotlin directory and compile it down to native binaries that target the Mac, Linux, and iOS (ARM) platforms.

After making a common Kotlin source file, like so:

I made a few changes to the build.gradle file:

kotlin { // For ARM, should be changed to iosArm32 or iosArm64 // For Linux, should be changed to e.g. linuxX64 // For MacOS, should be changed to e.g. macosX64 // For Windows, should be changed to e.g. mingwX64 macosX64("macos") { binaries { executable { // Change to specify fully qualified // name of your application's entry point: entryPoint = 'main' // Specify command-line arguments, if necessary: runTask?.args('') } } } linuxX64("linux") { binaries { executable { entryPoint = 'main' runTask?.args('') } } } mingwX64("windows") { binaries { executable { entryPoint = 'main' runTask?.args('') } } } iosArm64("ARM") { binaries { executable { entryPoint = 'main' runTask?.args('') } } } sourceSets { // Note: To enable common source sets please // comment out 'kotlin.import.noCommonSourceSets' property // in gradle.properties file and re-import your project in IDE. commonMain { dependencies { implementation(kotlin("stdlib-common")) } } commonTest { dependencies { implementation(kotlin("test-common")) implementation(kotlin("test-annotations-common")) } } } }

I rebuilt the project and verified that native binaries were produced for each of the three target platforms I specified in the build.gradle file, except for Windows. Since I am using a Mac to compile the source code, the compiler just ignores the Windows instructions in build.gradle.

Voila! Executables were created for all three platforms.

Reality:

In reality and for more complex projects, multi-platforming will likely require platform-specific code to be implemented, especially for Android and iOS. So the multi-platforming structure probably looks closer to this (ignoring host machine requirements):

Next time, we can try to build a program using different libraries to see how Kotlin/Native will hold up under the added complexity.

Conclusion

Kotlin/Native allows you to compile a single Kotlin program to eight different types of native binaries. Each binary file produced is intended to run on a specific target platform. The only caveat is that some targets require a specific type of host in order to compile the native binary. So if you wanted to compile eight different native binaries, one for each of the supported eight platforms, you would have to make sure you compile each one on the specific hardware platform it requires. Fortunately, having access to Windows and Mac systems will allow you to cover the most commonly used supported platforms. Alternatively, you could try compiling on a virtual machine or using an automated build service such as CircleCI. Apart from the emerging multiplatform capabilities, you will have the option to create Kotlin programs for a variety of platforms with direct access to native libraries and frameworks.