Skip to Content
Course content

173: Interop with C Libraries in Kotlin/Native

Click on the "Edit" button in the top corner of the screen to edit your slide content.

Most of the time, Kotlin/Native gives us everything we need. But eventually, you'll run into a situation where you need a high-performance C library, a legacy system API, or some hardware-specific driver that only provides a C interface. When that happens, you don't rewrite the library in Kotlin; you build a bridge.

I've found that the hardest part of C interop isn't the Kotlin code—it's the configuration. To show you how this works, we're going to build a tiny C library called libtinymath that handles a specific calculation (the hypotenuse of a triangle) and call it from Kotlin. It's simple, but it covers the actual plumbing you'll use in production.

Writing the C side of the bridge

First, we need the actual C code. I'm keeping this minimal so we can focus on the interop. Here is my tinymath.h header file:

// tinymath.h
#ifndef TINYMATH_H
#define TINYMATH_H

double calculate_hypotenuse(double a, double b);

#endif

And the implementation in tinymath.c:

// tinymath.c
#include "tinymath.h"
#include 

double calculate_hypotenuse(double a, double b) {
    return sqrt((a * a) + (b * b));
}

At this point, I'd compile this into a static library using gcc or clang. In a real project, your build system (like CMake) would handle this, but for now, just imagine we have libtinymath.a sitting in our project folder.

Mapping C to Kotlin with a .def file

Kotlin/Native doesn't just "guess" what's in your C library. You have to provide a definition file (a .def file) that tells the cinterop tool which headers to read and how to link the binary. I'll create one called tinymath.def:

headers = tinymath.h
package = com.example.tinymath
linkerOpts = -L. -ltinymath

The package line is important—this is where the generated Kotlin bindings will live. The linkerOpts tell the linker to look in the current directory (-L.) for a library named libtinymath (-ltinymath).

The "Where is my symbol?" moment

Here is where I usually trip up. I wrote my Kotlin code, ran the cinterop tool, and tried to compile. I got a nasty linker error: undefined reference to 'calculate_hypotenuse'.

I spent five minutes staring at my Kotlin code thinking I'd called the function wrong. Then I realized the mistake: I had the .def file pointing to the library, but I hadn't actually compiled the C code into a .a file in the directory the linker was searching. The cinterop tool generates the declarations (so the IDE is happy), but it doesn't compile your C source files for you. It only links against existing binaries.

I fixed it by running gcc -c tinymath.c -o tinymath.o and then ar rcs libtinymath.a tinymath.o. Once the actual binary existed, the error vanished.

Calling C from Kotlin

Now for the fun part. Once the cinterop tool runs, Kotlin treats the C functions as if they were regular Kotlin functions. Because I defined the package as com.example.tinymath, I can just import it.

import com.example.tinymath.*

fun main() {
    val a = 3.0
    val b = 4.0
    
    // This calls the C function directly
    val result = calculate_hypotenuse(a, b)
    
    println("The hypotenuse of $a and $b is $result")
}

Notice how seamless that is? The double in C maps directly to Double in Kotlin. If I were dealing with strings, I'd have to deal with CPointer and pinned memory, but for primitive types, Kotlin/Native does the heavy lifting for us.




📋 Practical Task

Exercise: Building a C-Powered String Reverser

To practice C interop, you are going to create a small utility that reverses a string using a C function. This will force you to handle C strings (pointers), which are more complex than doubles.

  • The C Part: Create a reverser.h and reverser.c. Implement a function void reverse_string(char* str) that reverses a string in-place.
  • The Bridge: Create a reverser.def file that maps the header and links to the compiled libreverser.a.
  • The Kotlin Part: Write a Kotlin program that:
    1. Takes a Kotlin String.
    2. Converts it to a C-string using memScoped { ... } and allocArrayOf or cstr.
    3. Calls reverse_string.
    4. Converts the resulting C-string back into a Kotlin String using toKString().
    5. Prints the result to the console.

Goal: Successfully pass a string from Kotlin to C, mutate it in C, and bring the mutated version back into Kotlin.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.