skip to content

Kotlin/Native Mechanics

What is genuinely different once there is no JVM: the Native memory manager, C and Objective-C interop, how exceptions cross into Swift, and the threading model. This is where KMP questions get specific.

part ofKotlinoverview, primer and where to startread it →
on this pageshow

explore

questions

20

What is the cinterop tool in Kotlin/Native, and what role does a .def file play when binding to a C library?

level: juniorimportance: must knowfreq 55%

answer

  1. cinterop = C headers in, Kotlin .klib bindings out
  2. .def = package + headers + compilerOpts/linkerOpts
  3. Configured via cinterops { } in the native compilation
  4. Generated klib lands on the source set classpath
  5. Kotlin/Native only (not JVM/JS)

basics

~10 s

cinterop is a Kotlin/Native tool that reads C header files and produces Kotlin code so you can call the C library from Kotlin. A .def file tells it which headers and library to use.

solid answer

~30 s

cinterop is the Kotlin/Native command-line tool (driven through Gradle's cinterops block in the kotlin { } native target) that parses C/Objective-C headers and generates a Kotlin bindings library (a .klib). A .def file is the configuration: it names the package, the headers to include (headers = ...), and how to link (e.g. headerFilter, compilerOpts, linkerOpts, staticLibraries). Each C function/struct/enum becomes a Kotlin declaration. You don't write the bindings by hand; you point cinterop at the .def and Gradle wires the generated klib onto the source set's classpath so you can import and call it.

code

kotlin · 19 lines
kotlin
// libpng.def
// headers = png.h
// package = org.example.libpng
// linkerOpts = -lpng

// build.gradle.kts
kotlin {
    linuxX64 {
        compilations.getByName("main").cinterops {
            val libpng by creating {
                defFile("src/nativeInterop/cinterop/libpng.def")
            }
        }
    }
}

// usage
import org.example.libpng.png_access_version_number
fun main() = println(png_access_version_number())

go deeper

for a junior

Knows cinterop turns C headers into Kotlin bindings and that a .def names headers and the library.

for a middle

Can write a minimal .def with package/headers/linkerOpts and wire the cinterops block in Gradle.

for a senior

Explains headerFilter, compilerOpts vs linkerOpts, static vs dynamic linking, and the klib-on-classpath flow.

for a principal

Reasons about multi-target binding strategy, build reproducibility, and packaging native deps across CI/architectures.

## What cinterop is **cinterop** is a tool shipped with **Kotlin/Native** (the LLVM-backed Kotlin compiler that produces native binaries). Its job is **automatic binding generation**: you give it C (or Objective-C) headers, and it emits a **Kotlin library** (a `.klib`) full of Kotlin declarations that map onto the C API. You then `import` those declarations and call them like ordinary Kotlin. ## The .def file The **`.def` file** (definition file) is cinterop's configuration. It is a key/value text file. Common keys: - `headers` — the C header files to parse (e.g. `headers = png.h`). - `package` — the Kotlin package the generated declarations land in. - `headerFilter` — limits which transitively-included headers contribute declarations (avoids dragging in the whole system). - `compilerOpts` — flags passed to the C compiler front-end (e.g. `-I/path/to/include`). - `linkerOpts` — flags for the linker (e.g. `-lpng`, `-L/path/to/lib`). - `staticLibraries` / `libraryPaths` — bundle a static lib into the klib. ## Wiring it in Gradle You declare a cinterop in the native target: ```kotlin kotlin { macosArm64 { compilations.getByName("main") { cinterops { val libpng by creating { defFile(project.file("src/nativeInterop/cinterop/libpng.def")) packageName("org.example.libpng") } } } } } ``` Gradle runs cinterop, produces the klib, and adds it to the source set so `import org.example.libpng.*` works. ## What gets generated - C **functions** → Kotlin functions. - C **structs/unions** → Kotlin classes accessed through pointers (`CPointer<T>`). - C **enums** → Kotlin enums or constants. - C **typedefs**, macros (simple ones), and global variables → corresponding Kotlin entities. Because the C world is unmanaged, pointers, memory scopes (`memScoped`), and value types (`CValue`) appear in the generated API — but the starting point is always: **point cinterop at headers via a .def, get Kotlin bindings.**

  • Can cinterop be used in a Kotlin/JVM module?
    No. cinterop and the generated klib are Kotlin/Native-only; on JVM you'd use JNI/JNA/the FFM API instead.
  • Where do .def files conventionally live?
    Under src/nativeInterop/cinterop/ in the module, referenced by defFile(...) in the cinterops block.

A .def file is like an order form for a translator: you list which documents (headers) to translate and where to find the originals (lib paths); cinterop hands back the translated bindings.

saying these in an interview costs you the question

  • Thinking cinterop works on the JVM target
  • Claiming you write the Kotlin bindings by hand
  • Confusing .def with a Kotlin source file
  • Not knowing linkerOpts/compilerOpts go in the .def or Gradle config
  • Saying cinterop runs at runtime rather than at build time

context

open as a page

On Kotlin/Native with the new memory manager, can you share a mutable object between threads, and what changed compared to the old model?

level: juniorimportance: must knowfreq 55%

basics

~10 s

Yes. With the new memory manager you can pass and use the same object from several threads. The old model was much stricter and required freezing objects first; that is gone now.

open as a page

What is the 'new memory manager' in Kotlin/Native, and what old model did it replace?

level: juniorimportance: must knowfreq 62%

basics

~10 s

It is Kotlin/Native's modern garbage collector. It replaced an old rule that froze objects and locked them to one thread. Now you can share regular mutable objects across threads, like on the JVM.

open as a page

What does the @Throws annotation do when a Kotlin/Native function is called from Swift or Objective-C, and what happens to an exception that isn't listed?

level: juniorimportance: must knowfreq 60%

basics

~10 s

@Throws tells the Kotlin compiler which exceptions should be passed to Swift or Objective-C as errors instead of crashing. Any exception you don't list will crash the whole app.

open as a page

Explain memScoped, CPointer, and how you pass a C struct or an out-parameter to a C function from Kotlin/Native.

level: middleimportance: must knowfreq 50%

basics

~20 s

memScoped gives you a temporary native memory area that is freed when the block ends. Inside it you allocate C structs and get CPointers to them, which you can pass to C functions, including as out-parameters the C code fills in.

open as a page

How do you do lock-free shared mutable state on Kotlin/Native today, and which atomic types do you use?

level: middleimportance: must knowfreq 48%

basics

~10 s

Use the atomic types from kotlin.concurrent, such as AtomicInt and AtomicReference. They let several threads update one value safely without locks, using operations like compareAndSet and incrementAndGet.

open as a page

Now that the new manager allows shared mutable state across threads, what tools do you use to keep that state correct, and what does the GC NOT do for you?

level: middleimportance: must knowfreq 44%

basics

~10 s

The garbage collector only manages memory; it does not stop two threads from changing the same data at once. You protect shared data with atomics, locks/mutexes, or by keeping it inside one thread.

open as a page

How do you consume an Apple Objective-C framework (like Foundation/UIKit) from Kotlin/Native, and how do Objective-C types and nullability map into Kotlin?

level: middleimportance: should knowfreq 42%

basics

~10 s

Apple frameworks ship with prebuilt cinterop bindings, so you just import them (e.g. platform.Foundation.*) and call them. Objective-C classes become Kotlin classes, methods become Kotlin functions, and nullable Obj-C types become Kotlin nullable types.

open as a page

Explain the Worker API on Kotlin/Native: how do you start one, run work on it, and get results back?

level: middleimportance: should knowfreq 38%

basics

~10 s

A Worker is a background thread. You start it with Worker.start(), send work with execute(), which returns a Future. You read the answer from future.result, and stop the worker with requestTermination().

open as a page

Explain freeze(), @SharedImmutable, and InvalidMutabilityException in the legacy model, and what happens to them under the new manager.

level: middleimportance: should knowfreq 48%

basics

~10 s

Old Kotlin/Native made you 'freeze' an object to share it across threads, which made it permanently read-only; changing it later crashed with InvalidMutabilityException. @SharedImmutable auto-froze a global. The new manager removes all of this.

open as a page

How is a bridged Kotlin exception surfaced in Objective-C and Swift, and how do you recover the original Kotlin exception object on the Swift side?

level: middleimportance: should knowfreq 45%

basics

~10 s

Kotlin generates an extra error parameter in Objective-C and a throwing method in Swift. The original Kotlin exception is wrapped inside the NSError, so Swift can read it from the error's userInfo.

open as a page

Compare @Throws semantics on Kotlin/Native versus the JVM. Why can the same annotation behave so differently?

level: middleimportance: should knowfreq 40%

basics

~10 s

On the JVM, @Throws only adds a note for Java callers and changes nothing for Kotlin. On Native, it actually controls which exceptions can safely cross into Swift; unlisted ones crash.

open as a page

How is a Kotlin/Native module exposed to Swift, and what limitations of the Kotlin→Objective-C/Swift bridge should you design around?

level: seniorimportance: should knowfreq 35%

basics

~20 s

Kotlin/Native compiles your code into an Objective-C-compatible framework that Swift can import. Swift sees Kotlin classes and functions, but some Kotlin features (like generics, sealed types, default arguments) don't survive the bridge cleanly, so you design the public API around that.

open as a page

What did multi-threaded coroutines support change on Kotlin/Native, and how do dispatchers behave under the new memory manager?

level: seniorimportance: should knowfreq 40%

basics

~20 s

Coroutines on Native used to run on one thread only. Now they can run across many threads. Dispatchers.Default uses a real thread pool, and a coroutine can resume on a different thread than it started on.

open as a page

How does the new memory manager change multithreaded coroutines on Kotlin/Native, e.g. Dispatchers.Default and runBlocking with background work?

level: seniorimportance: should knowfreq 40%

basics

~20 s

Before, coroutines on Native couldn't freely jump threads because shared objects had to be frozen. The new manager lets coroutines move state across threads normally, so multithreaded dispatchers like Dispatchers.Default work like on the JVM.

open as a page

When exposing a Kotlin suspend function to Swift via @Throws, how should CancellationException be handled, and why is it special?

level: seniorimportance: should knowfreq 35%

basics

~20 s

Suspend functions become Swift async calls with a completion that can deliver an error. You should mark them so cancellation can be reported to Swift; otherwise a cancelled coroutine could crash instead of telling Swift it was cancelled.

open as a page

Contrast CValue<T> with CPointer<T>: when do you use cValue { } and useContents, and why does the distinction matter for C functions that take or return structs by value?

level: seniorimportance: nice to knowfreq 22%

basics

~20 s

CPointer<T> is a reference to native memory you can mutate; CValue<T> is an immutable copy of a C struct passed by value. You build one with cValue { } and read its fields with useContents { }. You use CValue when the C function takes or returns the struct itself, not a pointer.

open as a page

Describe the GC characteristics of the new Kotlin/Native memory manager (collector type, stop-the-world, tuning, and interop leaks). When can you still leak memory?

level: seniorimportance: nice to knowfreq 28%

basics

~20 s

The new manager uses a tracing collector that finds and frees unused objects, pausing threads briefly to scan. You can tune it with runtime settings. You can still leak memory through cycles that cross into Objective-C/Swift, or by holding references too long.

open as a page

You are designing a shared Kotlin Multiplatform module whose state is mutated from coroutines on Kotlin/Native. How do you choose between single-thread confinement, atomics, and a Mutex, and what failure modes do you guard against?

level: principalimportance: nice to knowfreq 22%

basics

~20 s

Pick by the shape of the state. Keep it on one thread if updates must stay ordered, use atomics for one simple value, and use a Mutex for a multi-step critical section. Watch for races, deadlocks, and blocking the main thread.

open as a page

You own a KMP library consumed by an iOS app. What is your strategy for using @Throws across the public API to avoid crashes while keeping the Swift error model usable?

level: principalimportance: nice to knowfreq 22%

basics

~20 s

Decide which failures are recoverable and list those in @Throws on the public functions Swift calls, so they become catchable errors. Leave only truly fatal bugs unlisted, and give Swift a small, predictable set of error types.

open as a page