How does Kotlin/Native interoperate with C libraries? Walk through cinterop, .def files, and how C types map into Kotlin.
answer
- cinterop tool + .def file (headers/package/linkerOpts)
- cinterops.create in compilations
- CPointer<T>, alloc, memScoped
- char* <-> .cstr / .toKString()
- @OptIn(ExperimentalForeignApi), platform libs (POSIX/Foundation)
basics
~20 sKotlin/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 sKotlin/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
Aware that Kotlin/Native can call C and that there is a cinterop tool.
Knows the .def file fields and that cinterop generates Kotlin bindings for C declarations.
Explains type mapping (CPointer, structs), memScoped memory handling, ExperimentalForeignApi opt-in, and platform libraries.
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