The magic of Kotlin/Native: Part 2
Kotlin/Native Series
1. The Magic of Kotlin Native: Part 1
2. The Magic of Kotlin Native: Part 2 ( You are here)
3. The Magic of Kotlin Native: Part 3
In the Part 1 of this series we talked about what is Kotlin/Native and how it enables us to create programs for native platforms. In this article we are going to discuss how you can interop Kotlin code with C and Objective-C/Swift.
But first let me address the burning question that you may have.
When would I ever need to inter-op with C?
The simple answer is a lot of code and libraries that run on machines natively is written in C (or Objective-C in case of macOS). If you want to develop applications that would run natively you would need to ultimately inter-op with C code. Also there may be a great library written in C that you want to include in your application. Instead of re-inventing the wheel and implement the said library yourself you can just include it in your Kotlin/Native project.
DISCLAIMER : If you feel uncomfortable around C code please proceed only at your own risk
Background
Libraries in C can be of two types
- Static libraries (.a): Library of object code which is linked with, and becomes part of the application.
- Dynamically linked shared object libraries (.so): The libraries must be available during compile/link phase. The shared objects are not included into the executable component but are tied to the execution.
To reference methods inside these libraries they have a .h
file which contains the stubs for all the exposed functions.
Creating C libraries via gcc is out of scope for this article but if you want to dive deep into it you can read:
Consuming C Library in Kotlin code
The following workflow is expected when interacting with the native library.
- create a
.def
file describing what to include into bindings - use the
cinterop
tool to produce Kotlin bindings - run Kotlin/Native compiler on an application to produce the final executable
Let’s understand these steps one by one by the help of an example.
Below I have a snippet of getJoke
method implemented in RandomJoke.h
which get’s a random Chuck Norris joke.
char* getJoke() {
CURL *curl;
CURLcode res;
curl = curl_easy_init();
if(curl) {
struct string s;
init_string(&s);curl_easy_setopt(curl, CURLOPT_URL, "http://api.icndb.com/jokes/random");
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s);
res = curl_easy_perform(curl);
return s.ptr;
}
return "";
}
Note: Header files should not have implementations. Here I have implemented the method inside the header file for the sake of brevity
Step 1: Create a .def
file
A .def
file is a configuration file which tells the cinterop
tool how to package a given C library. A typical .def
file would look similar to:
headers = /Users/viveksingh/Personal/Projects/Joke/src/main/c_interop/randomJoke/RandomJoke.h
compilerOpts = -D_POSIX_SOURCE
linkerOpts.osx = -L/opt/local/lib -L/usr/local/opt/curl/lib -lcurl
linkerOpts.linux = -L/usr/lib/x86_64-linux-gnu -lcurl
headers
specifies the header files that need to be mapped to kotlin code.
compilerOpts
and linkerOpts
are used by the underlying gcc (the c/c++ compiler) to compile and link any libraries.
This line instructs the compiler to look for curl in the following two locations.
linkerOpts.osx = -L/opt/local/lib -L/usr/local/opt/curl/lib -lcurl
Step 2: Run the ‘cinterop’ command
cinterop -def RandomJoke.def -o randomJoke
This command will produce a randomJoke.klib
compiled library and randomJoke-build/kotlin
directory containing Kotlin source code for the library.
The .klib
are Kotlin/Native libraries which you can plug and play in any Kotlin/Native project. They are the jars
of rhe Kotlin/Native world.
Note: The
.klib
does not contain the implementation code ofcurl
methods but only the stubs. When your programs runs on a machine it will expectcurl
to be installed on that machine.
Step 3: Compile your program
Below is my PrintJoke.kt
import RandomJoke.*
import kotlinx.cinterop.*fun main(args: Array<String>) {
println(getJoke()?.toKString())
}
Run the following command which will create a joke.kexe
kotlinc PrintJoke.kt -l randomJoke -o joke
Run joke.kexe
and voila
{ "type": "success", "value": { "id": 163, "joke": "Ninjas want to grow up to be just like Chuck Norris. But usually they grow up just to be killed by Chuck Norris.", "categories": [] } }
Static Libraries
As I mentioned earlier curl
implementation will not be bundled inside the .klib
that is produced. But its not always desirable to assume that a library will be present in a target machine.
Imagine you have a great C library foo
whose code you want to bundle in the generated .klib
. Just add the following lines to the .def
file
staticLibraries = foo.a
libraryPaths = /opt/local/lib /usr/local/opt/curl/lib
When given the above snippet the cinterop
tool will search foo.a
in /opt/local/lib
and /usr/local/opt/curl/lib
, and if it is found include the library binary into klib
.
When using such klib
in your program, the library is linked automatically.
Consuming Kotlin code in C
So far we have talked about how to include a C library in kotlin code. But you cannot claim full interoperability if you can’t do the vice-versa.
Below is an awesome kotlin library to add two integers add.kt
fun add(a: Int, b: Int): Int {
return a + b
}
Now run the following command
kotlinc add.kt -p dynamic -o add
The kotlinc
generates the following files, depending on the host OS:
- macOS:
add_api.h
andlibadd.dylib
- Linux:
add_api.h
andlibadd.so
- Windows:
add_api.h
,add_symbols.def
andadd.dll
On macOS 10.13 with Xcode, we compile the C code and link it with the dynamic library with the following command:
clang main.c libadd.dylib
On Linux we call a similar command:
gcc main.c libadd.so
To delve deep into the generated Header file this Kotlin tutorial provides an excellent reference.
Type Mappings
Below are the cheat sheets of C types and keywords and how they are consumed in the Kotlin world.
To know more about C bindings in Kotlin you can go through the official documentation which is pretty detailed in itself.
For Objective-C/Swift mappings the official documentation provides an excellent reference
Kotlin/Native as an Apple Framework
To interop with Objective-C/Swift you need your kotlin code to be in the form of .framework
files.
$ kotlinc add.kt -produce framework -target macos_x64 -output macOS/Add $ kotlinc add.kt -produce framework -target ios_arm64 -output iOS/Add$ kotlinc add.kt -produce framework -target ios_x64 -output iOS_sim/Add
The above command creates a .framework
file that you can than consume in your Objective-C/Swift project. You can define different targets depending on the target platform.
Conclusion
So far we have covered how Kotlin/Native enables us with tools to create application for native environments and inter-op with C and Objective-C/Swift.
In the next article we will create an application for Android and iOS using Kotlin/Multi platform and Kotlin/Native plugin.
Source code for this article:
References
Thanks for reading! Share this article if you found it useful.
Please do Clap 👏 to show some love :)
Follow Androidiots for more such amazing android related content.