Kotlin / Native —How to use C in Kotlin [Part 1] | by Debanshu Datta | Sep, 2023
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.
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.getpidfun 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.
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 thesrc
. It is the default convention for header file locations, though we can override it in thebuild.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 calledheader 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 thecinterop
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 thecinterop
tool is the presence of the.h
file.
#ifndef LIB2_H_INCLUDED
#define LIB2_H_INCLUDEDint 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 theprimelib.h
file and place these functions into a.def
file. A.def
file is a configuration file which tells thecinterop
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)andlinkerOpts
(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 ourbuild.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 eachdef
file. Here we have added the additional configuration of the path todef
file and options to be passed to the compiler bycinterop
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 insidebuild/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 findmanifest
a file consisting of the details of the application. Insidetarget
, we can see thecstubs.bc
file is for the binary representation of LLVM IR.
.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
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 thesrc
. It is the default convention for header file locations, though it can be overridden in thebuild.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 onlycurl.h
, and referenced files must be on the system path. We have already discussedlinkerOpts
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
isexecutable
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 insidebuild/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.
- Finally, we
build
the project from IDE. We can start implementing the application. It is simple and direct. All the functions likecurl_easy_init()
,curl_easy_setopt()
,curl_easy_perform()
andcurl_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