Kotlin / Native —How to use C in Kotlin [Part 1]

Debanshu Datta
ProAndroidDev
Published in
8 min readSep 29, 2023

--

Kotlin/Native is a technology for compiling Kotlin code to native binaries that can run without a virtual machine. Kotlin/Native includes an LLVM-based backend for the Kotlin compiler and a native implementation of the Kotlin standard library. It allows Kotlin code to compile into native binaries, which makes it ideal for platforms where virtual machines are not desirable or possible (such as iOS or embedded targets) or to produce a reasonably-sized self-contained program without additional execution runtime.

After reading this:

Kotlin/Native architecture

Now let’s dive deep into the internal architecture. We previously mentioned that Kotlin/Native have an LLVM-based backend, what the backend, what does it mean? When Kotlin Compiler is used to compile code, it first transforms into Intermediate Representation (IR). Then, it converts IR to Java byte code in a JVM-based environment. The first step is called “compiler frontend,” and later is called “compiler backend”. So, LLVM is our compiler backend.

Kotlin has the same compiler frontend and several backends; there is a backend for JavaScript and one for native, which produces standalone binaries. For those who may not be familiar, LLVM is a set of compiler tools that have been around for nearly two decades and are in Apple’s macOS and iOS development environments. The Kotlin/Native technology compiles Kotlin code into LLVM’s Intermediate Representation (IR), a low-level, platform-independent language that LLVM can comprehend. LLVM further compiles the IR into executable code for the desired platform. The runtime implementation in Kotlin/Native is the prime component that executes the Intermediate Representation (IR) the Kotlin compiler generates on a specific platform.

It offers a platform-specific implementation of Kotlin language features such as memory management, type checking, and more. It is designed to be lightweight and efficient while providing all the necessary features to run Kotlin code. Furthermore, it also serves as a bridge between the platform-independent IR and the platform-specific machine code. The runtime implementation also manages memory usage, ensures code correctness, and provides APIs for native platform interaction.

Interoperability with native code

Let’s start by writing a simple Kotlin/Native application, just the default Hello World program provided when we build a native application with IntelliJ and Gradle. I have added only a getpid() function, an inbuilt function defined in the unistd.h library that returns the current process's ID.

import platform.posix.getpid


fun main() {
println("Hello, Kotlin/Native! ${getpid()}")
}

When we get into the getpid() definition, we will see it is binding for interoperability with C.

……
@kotlinx.cinterop.internal.CCall public external fun getpid(): platform.posix.pid_t /* = kotlin.Int */ { /* compiled code */ }
……

Now let’s build the project from the IDE. After the building phase, we will have a new .kexe inside build/bin/native/debugExecutable(default location) of our app will be up and running. So this build should generate an abc.kexe (Linux and macOS) or abc.exe (Windows) binary file.

C interop

We will understand interoperability with C language with the help of a simple example. For native platforms, the primary interoperability goal is with C libraries. To facilitate this, Kotlin/Native provides the cinterop tool, which generates everything required for interacting with external libraries quickly and easily.

Steps to consume C Library in Kotlin code

  • Create a .def file describing what to include in bindings.
  • Use the cinterop tool to produce Kotlin bindings.
  • Run the Kotlin/Native compiler on an application to produce the final executable. We will consume C Library to determine the prime number and return the result to our Kotlin Code.

Example 1

In this example, we will make a simple isPrime() function and return a string which we have created in C and directly call from Kotlin.

  • Firstly, create a new directory nativeInterop/cinterop in the src. It is the default convention for header file locations, though we can override it in the build.gradle file if we use a different site.
  • Start by creating a file primelib.h to see how C functions map into Kotlin.h files called header files. Which contain the function prototypes and tell the compiler how to trigger some functionality. The file consists of the stubs for all the exposed functions. When working with a set of .h files, We use the cinterop tool from Kotlin/Native to generate a Kotlin/Native library, also known as a .klib. This generated library facilitates communication between Kotlin/Native and C by providing Kotlin declarations for the definitions in the .h files. The only requirement for running the cinterop tool is the presence of the .h file.
#ifndef LIB2_H_INCLUDED
#define LIB2_H_INCLUDED

int isPrime(int num);
char* return_string(int isPrime);

#endif
  • Now make a new libcurl.def file. In this file, we need to add implementations to the C functions from the primelib.h file and place these functions into a .def file. A.def file is a configuration file which tells the cinterop tool how to package a given C library by describing what to include in bindings. headers specifies the header files that need to be mapped to Kotlin code. compilerOpts(used to analyze headers, such as preprocessor definitions)and linkerOpts(used to link final executables) can also be added to be used by the underlying GCC (the c/c++ compiler) to compile and link any libraries. In our case, we have added it to our build.gradle.kts.
headers = primelib.h
---


char* return_string(int isPrime) {
return (isPrime==0) ? "is prime": "is not prime";
}


int is_prime(int num){
int count = 0;
for(int i = 1;i<=num;i++){
if(num%i==0){
count++;
}
}
if(count ==2)
return 0;
else
return 1;
}
  • Add interoperability to the build process. To use header files, we need to make a part of the build process. cinterops is added, and then an entry for each def file. Here we have added the additional configuration of the path to def file and options to be passed to the compiler by cinterop the tool, i.e., our path to header files.
nativeTarget.apply {
compilations.getByName(“main”) {
cinterops {
val primeInterop by creating{
defFile(project.file(“src/nativeInterop/cinterop/primeInterop.def“))
compilerOpts(“-Isrc/nativeInterop/cinterop”)
}
}
}
binaries {
executable {
entryPoint = "main”
}
}
}
  • Finally, We can build the project from the command line approach while using IDE here. After the build is successful, we will find a new.klib file generated inside build/libs/native/main/NativeDemo-cinterop-primeInterop.klib (application name is NativeDemo). We can find the generated bindings inside this.knm file which is binding. We can also find manifest a file consisting of the details of the application. Inside target, we can see the cstubs.bc file is for the binary representation of LLVM IR.
Generated binding

.klib does not contain the implementation code of prime methods but only the stubs. When our program runs, it will expect a curl on that machine. Let's code out our application.

import kotlinx.cinterop.*
import primeInterop.*


fun main() {
println(“Enter number to check Prime”)
val number = readln().toInt()
val output = return_string(is_prime(number))?.toKString()
println(“Returned from C: $output”)
}

To compile the application, use this command in the terminal.

./gradlew runDebugExecutableNative

To run the application

build/bin/native/debugExecutable/NativeDemo.kexe
Expected Terminal Output

Example 2

In this example, we will use an existing curl to make a simple API call and get a response, which we have created in C and directly call from Kotlin.

  • We will follow similar steps, creating a new directory nativeInterop/cinterop in the src. It is the default convention for header file locations, though it can be overridden in the build.gradle file if we use a different site.
  • Finally, we start by writing the libcurl.def file. It consists of the headers. headers is a collection of header files used to generate Kotlin stubs. We can add multiple files to this entry, each separated by a new line. It's only curl.h, and referenced files must be on the system path. We have already discussed linkerOpts in Example 1.
headers = curl/curl.h
headerFilter = curl/*


linkerOpts.osx = -L/opt/local/lib -L/usr/local/opt/curl/lib -lcurl
  • Add interoperability to the build process. To use header files, we need to make a part of the build process. We see below binaries is executable that helps Gradle build an executable.
nativeTarget.apply {
compilations.getByName("main") {
cinterops {
val libcurl by creating {
defFile(project.file("src/nativeInterop/cinterop/libcurl.def"))
}
}
}
binaries {
executable {
entryPoint = "main"
}
}
}
  • After the build is successful, we will find a new .klib file generated inside build/classes/kotlin/native/main/cinterop/NativeDemo-cinterop-libcurl.klib (application name is NativeDemo) similar structure as described in the previous example. The only difference we will find is more binding files and more functions. It has multiple functions and implementations, hence multiple binding files.
Generated binding
  • Finally, we build the project from IDE. We can start implementing the application. It is simple and direct. All the functions like curl_easy_init(), curl_easy_setopt(), curl_easy_perform() and curl_easy_strerror() are known APIs from curl which are out of scope for the discussion.
import kotlinx.cinterop.*
import libcurl.*


fun main() {
val curl = curl_easy_init()
if (curl != null) {
curl_easy_setopt(curl, CURLOPT_URL, "https://jsonplaceholder.typicode.com/posts")
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L)
val res = curl_easy_perform(curl)
if (res != CURLE_OK) {
println("curl_easy_perform() failed ${curl_easy_strerror(res)?.toKString()}")
}
curl_easy_cleanup(curl)
}
}

To compile the application, use this command in the terminal.

./gradlew runDebugExecutableNative

To run the application

build/bin/native/debugExecutable/NativeDemo.kexe
Expected Terminal Output

The article also covered the essential aspect of interoperability with native C code, demonstrating how to create Kotlin bindings for C libraries using the cinterop tool. Two practical examples illustrated the process, one involving prime number determination and the other showcasing the use of the libcurl library for making API calls.

For any doubts and suggestions, you can reach out on my Instagram, or LinkedIn. Follow me for Kotlin content and more. Happy Coding!

I will well appreciate one of these 👏

--

--

Android(L2) @Gojek | Mobile Developer | Backend Developer (Java/Kotlin)