skip to content

How does Kotlin/Native interoperate with C libraries? Walk through cinterop, .def files, and how C types map into Kotlin.

level: seniorimportance: should knowfreq 35%

answer

  1. cinterop tool + .def file (headers/package/linkerOpts)
  2. cinterops.create in compilations
  3. CPointer<T>, alloc, memScoped
  4. char* <-> .cstr / .toKString()
  5. @OptIn(ExperimentalForeignApi), platform libs (POSIX/Foundation)

basics

~20 s

Kotlin/Native ships a tool called cinterop. You give it a small .def file pointing at a C header and library, and it generates a Kotlin package whose functions and types call straight into the C code.

solid answer

~40 s

Kotlin/Native talks to C through the **cinterop** tool. You declare an interop in Gradle under a target's `compilations` via `cinterops.create("name")`, pointing it at a **`.def` file**. The `.def` lists the C `headers`, optional `package`, `compilerOpts`, `linkerOpts`, and `staticLibraries`. cinterop parses the headers and generates Kotlin bindings: C functions become Kotlin functions, structs/typedefs become Kotlin classes, and pointers become **`CPointer<T>`**. Because C uses manual memory and raw pointers, you work inside a **`memScoped { }`** block and use **`alloc`**, **`cstr`**, **`toKString()`**, and `CValues` to bridge memory. Everything touching raw pointers and C calls is **`@OptIn(ExperimentalForeignApi::class)`**. Platform OS headers (POSIX, Foundation) are pre-bundled as **platform libraries**. Objective-C/Swift interop on Apple targets is a richer extension of the same machinery, mapping Obj-C classes to Kotlin types.

go deeper

for a junior

Aware that Kotlin/Native can call C and that there is a cinterop tool.

for a middle

Knows the .def file fields and that cinterop generates Kotlin bindings for C declarations.

for a senior

Explains type mapping (CPointer, structs), memScoped memory handling, ExperimentalForeignApi opt-in, and platform libraries.

for a principal

Designs a clean Kotlin facade over C/Obj-C interop, manages memory/lifetime safely, and weighs interop maintenance cost across targets.

## Why interop matters Kotlin/Native compiles to native code, so it can call **C** libraries directly with no FFI bridge layer like JNI. The tool that makes this work is **cinterop**. ## The `.def` file You describe the C library in a small **`.def`** (definition) file: ``` headers = curl/curl.h package = libcurl compilerOpts = -I/usr/include linkerOpts = -lcurl ``` - **`headers`** — the C header(s) to parse. - **`package`** — the Kotlin package the bindings land in. - **`compilerOpts` / `linkerOpts`** — flags passed to the C compiler/linker. - **`staticLibraries`** — static archives to bundle. ## Wiring it in Gradle ```kotlin kotlin { linuxX64 { compilations.getByName("main") { cinterops.create("libcurl") { defFile(project.file("src/nativeInterop/cinterop/libcurl.def")) } } } } ``` cinterop runs at build time, generates Kotlin bindings, and you `import` the declared package. ## Type mapping - C functions → Kotlin functions. - C `struct`/`typedef` → Kotlin classes (allocated via `alloc<T>()`). - Pointers → **`CPointer<T>`**; a possibly-null pointer is `CPointer<T>?`. - C strings (`char*`) ↔ Kotlin `String` via **`.toKString()`** and **`.cstr`**. - Primitive C ints map to Kotlin `Int`/`Long`/`UByte` etc. ## Memory management C memory isn't garbage-collected, so you scope native allocations: ```kotlin import kotlinx.cinterop.* @OptIn(ExperimentalForeignApi::class) fun useC() = memScoped { val buffer = allocArray<ByteVar>(256) val ptr: CPointer<ByteVar> = buffer // ... call C functions with ptr ... ptr.toKString() } ``` - **`memScoped { }`** frees everything allocated inside when the block exits. - **`alloc` / `allocArray`** reserve native memory. - All raw-pointer APIs require **`@OptIn(ExperimentalForeignApi::class)`**. ## Platform libraries Kotlin/Native pre-generates bindings for OS headers — **POSIX**, **Foundation**, **CoreFoundation**, etc. — called **platform libraries**, so you can use them without writing a `.def`. ## Objective-C / Swift On Apple targets, the same interop engine maps **Objective-C** classes, protocols, and selectors to Kotlin types, and (with the framework output) lets Swift call Kotlin. This is how KMP integrates with iOS SDKs.

  • Why must native allocations happen inside memScoped?
    C memory isn't garbage-collected; memScoped tracks allocations made with alloc/allocArray and frees them when the block exits, preventing leaks.
  • How do you call POSIX functions without writing a .def file?
    Use the pre-bundled platform libraries (e.g., the `platform.posix` package); Kotlin/Native ships generated bindings for OS headers.
  • What opt-in annotation guards raw C-pointer APIs?
    `@OptIn(ExperimentalForeignApi::class)` from kotlinx.cinterop, since the foreign API is experimental.

The .def file is like a customs declaration that tells cinterop exactly which C goods (headers/libs) you're importing so it can stamp Kotlin bindings for them.

saying these in an interview costs you the question

  • Claiming Kotlin/Native uses JNI to call C
  • Not knowing the .def file or what it contains
  • Forgetting that native allocations need memScoped/manual freeing
  • Believing C strings auto-convert without .cstr/.toKString()
  • Unaware of platform libraries for POSIX/Foundation

context