Prerequisites and assumptions
You are using the Kotlin Gradle DSL and the Kotlin Multiplatform plugin. You have a working C toolchain per OS. On macOS install Xcode command line tools or Homebrew. On Linux install GCC or Clang and the development headers for the libraries you plan to use. On Windows install MSYS2 with MinGW and ensure gcc and pkg config are in PATH.
Good reads and references
- Kotlin/Native overview and C interop guides
- Mapping primitive data types, strings, structs and unions, function pointers from C
- Kotlin/Native libraries and Gradle cinterops
- Exploring Kotlin (native) compilation - deep dive into compilation process
- POSIX.1-2024 specifications for Linux and macOS and Win32 API reference for Windows
What you get with Kotlin Native (desktop)
Kotlin Native compiles Kotlin to native machine code. On desktop that means you can ship fast binaries that talk to existing C libraries and to the system APIs on macOS Windows and Linux.
Why teams pick this
- Performance and small footprint for command line tools daemons services and GUI backends
- Direct access to POSIX on Linux and macOS and to Win32 on Windows which are exposed primarily in C
- Seamless reuse of proven C code without a rewrite
Note on platform libraries: Kotlin/Native provides prebuilt platform libraries like platform.posix for POSIX systems and platform.windows for Windows APIs. Before creating custom interop, check if the functionality you need is already available in these platform libraries.
The interop workflow at a glance
Interop has two phases. Phase one translates C headers into Kotlin facing APIs. Phase two compiles and links your Kotlin code with those bindings into a platform binary.
flowchart TD A[C library\n headers and .so or .dylib or .dll ] --> B[Definition file .def] B --> C[Gradle cinterops configuration] C --> D[cinterop parses headers and emits .klib] D --> E[Your Kotlin code imports generated API] E --> F[Kotlin Native compiler lowers to LLVM] F --> G[Platform toolchain links] G --> H[Final native artifact\n .kexe or .so or .dylib or .dll ]Steps you perform
- Write a definition file with extension .def that tells interop which headers to import and how to parse them
- Configure Gradle so cinterop runs for each desktop target you build
- Write Kotlin that calls the generated bindings
Anatomy of the definition file
The .def file is more than a list of headers. It configures the Clang parser and it carries link flags that are used at the final link step.
Example libcurl.def
Definition file properties
headers | A space separated list of C header files to process. This is the core input for the tool | headers = my_lib.h another_header.h |
package | Specifies the Kotlin package where the generated bindings will be placed | package = com.mycompany.clib |
headerFilter | A glob pattern to selectively include declarations only from specific headers. Useful for reducing binary size and avoiding conflicts with system headers | headerFilter = my_lib/** |
compilerOpts | Passes flags directly to the underlying C compiler (Clang) used for parsing headers. Essential for defining macros or adding include paths. Can be platform specific | compilerOpts = -I/usr/local/includecompilerOpts.linux = -DDEBUG=1 |
linkerOpts | Passes flags directly to the linker. Used to link against the actual C library (.so, .dylib, .a). Can be platform-specific | linkerOpts = -L/usr/local/lib -lmy_lib |
staticLibraries | Specifies static libraries (.a) to be bundled directly into the resulting .klib file (Experimental) | staticLibraries = libfoo.a |
libraryPaths | A space separated list of directories to search for the static libraries specified in staticLibraries (Experimental) | libraryPaths = /path/to/libs |
Gradle configuration for desktop targets
Use the Kotlin Multiplatform plugin and enable desktop targets. Configure cinterops on the main compilation so a .klib is generated per target.
Tips
- Interop runs per target so you get tasks like cinteropCurlMacosArm64 and cinteropCurlLinuxX64
- The generated .klib is added to the compilation dependencies so your Kotlin code can import the package you chose in the .def file
What cinterop generates and what it does not
cinterop uses Clang to parse the headers and generates a Kotlin Native library file with extension .klib that contains declarations and metadata the compiler consumes. The entire process is orchestrated by the Kotlin/Native compiler (internally called Konan), which uses an LLVM backend to produce optimized machine code for the target platform. If staticLibraries is set a static archive can be embedded so that consumers link it automatically just by depending on the .klib.
What is imported
- Functions global variables enums structs and typedefs
- Simple macros that can be turned into constants
What is not imported or has caveats
- Complex C macros and inline functions may not become callable Kotlin and often require a small C shim library
- Variadic functions and bit fields can have limitations
- Nullability and enum mapping are heuristic based so verify the generated API
The Rosetta stone for types
All interop types live under package kotlinx.cinterop
int long short | Int Long Short | direct mapping of signed integrals |
char | Byte | C char is 8 bit and is not Kotlin Char |
unsigned int unsigned long | UInt ULong | use unsigned types to match semantics |
float double | Float Double | direct mapping |
T* | CPointer<TVar>? | nullable to represent null |
T* parameter | CValuesRef<TVar>? | accepts a pointer or a contiguous sequence |
char* | CPointer<ByteVar>? | use toKString to read and string.cstr to pass, wide APIs on Windows use wcstr |
struct S by value | CValue<S> | build with cValue block and read with useContents |
struct S* | allocate with alloc<S>() inside memScoped | pass via .ptr |
function pointer int (*f)(int) | CPointer<CFunction<(Int) -> Int>>? | non-capturing callbacks via staticCFunction, state via StableRef |
Size and numeric conversion
- Use convert<size_t>() when passing sizes and lengths to C
- Prefer explicit convert calls at boundaries so the same code works on 32 bit and 64 bit size platforms
Memory management that keeps you safe
Important: The C interop functionality is currently in Beta status. The cinterop API is marked as experimental, and you need to opt-in by adding @OptIn(ExperimentalForeignApi::class) to your functions that use cinterop types. This annotation acknowledges that the API may change in future Kotlin versions, though the core concepts remain stable.
memScoped block
- Allocations done with alloc and allocArray inside memScoped are freed when the block exits including error paths
nativeHeap
- Manual lifetime for long lived native objects using nativeHeap.alloc and nativeHeap.free use with care and pair allocations with frees
Pinning managed memory
- Use usePinned on a Kotlin array or string to get a stable native pointer that stays valid while the C function runs
- Alternatively, use refTo() for simpler cases where you need to pass a reference to a single element
CValues helpers to avoid manual allocation
Passing a struct by pointer
Reading into a pinned buffer
Callbacks from C back into Kotlin
Non capturing callbacks
- Use staticCFunction with a top level or static Kotlin function. The function must not capture Kotlin state
Passing state through user data
- Allocate StableRef of a Kotlin object and pass its pointer with asCPointer as the user data parameter of the C API. Later recover it with asStableRef get and dispose it when done
Thread safety note: With Kotlin/Native’s modern memory manager, StableRef can be accessed from different threads. However, if your callback may be invoked from multiple threads concurrently, you still need to ensure proper synchronization for any shared mutable state within the referenced object. Consider using thread safe data structures or explicit locking mechanisms for concurrent access.
Signature example
Stable user data example
Strings and encoding across platforms
C strings are sequences of bytes terminated by zero. Use toKString to read a C string into Kotlin and use string.cstr to pass a Kotlin string to C. On Windows many APIs use wide strings. Use wcstr for those.
The noStringConversion property in your .def file allows you to specify functions that should not have automatic string conversion. List the function names where you need explicit control:
Use this when:
- You need explicit control over memory allocation for strings
- Working with binary data that shouldn’t be interpreted as UTF-8
- Performance is critical and you want to avoid conversion overhead
Desktop specifics
macOS
- Targets macosArm64 and macosX64
- Headers from Xcode SDK or Homebrew
- Link frameworks with -framework flags in linkerOpts.osx for example Security or SystemConfiguration
- Use headerFilter to keep imports focused when working with large SDKs
Linux
- Target linuxX64
- Headers and libraries come from system paths such as /usr/include and /usr/lib on Debian based or from include and lib under /usr on other distros
- When you vendor a library install headers to a known prefix and pass explicit include and library paths in compilerOpts and linkerOpts
Windows with MinGW
- Target mingwX64 (Note: This is a Tier 3 target with limited official support)
- Install the library via MSYS2 packages headers under mingw64/include and libs under mingw64/lib
- Link system libraries such as ws2_32 when needed
- Many Win32 APIs have ANSI and Wide variants. For wide functions pass string.wcstr
Packaging and distribution notes
Static versus dynamic
- With staticLibraries you can embed a static archive in the .klib so dependents link it automatically. Dynamic linking requires that the target machine can find the .so or .dylib or .dll at runtime. Set rpath or the appropriate environment variable when you use shared libraries
Binary kinds
- Kotlin Native produces executables (.kexe), shared libraries and static libraries. Choose the kind that matches your integration story
Runtime search paths
- On Linux adjust LD_LIBRARY_PATH or embed rpath. On macOS adjust rpath and codesign when necessary. On Windows ensure the .dll is reachable via PATH or next to the executable
Error handling patterns
C libraries typically signal errors through return codes or by setting errno. Here are idiomatic patterns for handling these in Kotlin:
Checking return codes
Working with errno
Performance considerations
Crossing the interop boundary has overhead. Keep these points in mind:
- Callback overhead: Callbacks through staticCFunction have additional indirection cost
- Memory allocation: Prefer stack allocation with memScoped over heap allocation for temporary data
- Large data transfers: Consider using direct memory buffers for bulk data operations
For performance critical code, profile to identify bottlenecks and consider writing performance sensitive loops in C if the interop overhead becomes significant.
Troubleshooting quick wins
Header not found
- Check include paths in compilerOpts and run Gradle with --info to see the Clang command line
Undefined symbol at link time
- Verify your linkerOpts and that the library actually exports the symbol you call
Type does not match
- Inspect the generated stubs under build and confirm the mapping. Use a C shim if you need to bridge a tricky signature
Callback crashes
- Ensure your callback is non capturing and that any StableRef is disposed when no longer needed
Different behavior across platforms
- Use convert calls for sizes and numeric conversions and audit any platform specific macros or calling conventions
Debugging tips
When things go wrong, these techniques help diagnose issues:
Gradle build debugging
- Run with --info or --debug flags to see detailed cinterop invocation: ./gradlew cinteropCurlMacosArm64 --info
- Inspect generated bindings via IDE navigation or explore the build directory for generated stubs
Runtime debugging
- Enable Kotlin Native GC logging to monitor memory behavior: -Xruntime-logs=gc=info
- Use platform debuggers: lldb on macOS/Linux, Visual Studio debugger on Windows
- Add C side logging in a shim library to trace execution flow
- Check for memory leaks using kotlin.native.internal.GC.lastGCInfo() in tests
Common cinterop flags for debugging
Inspecting generated bindings
- Generated Kotlin stubs show the actual API surface
- Use IDE navigation to jump to generated declarations
- Use the klib tool to inspect library contents if needed
Quick checklist for new desktop interop
- Tight headerFilter in the .def and correct package name
- cinterops configured on the desktop targets you support
- Use memScoped and CValues helpers as the default allocation pattern
- Use convert<size_t>() for sizes and lengths
- On Windows identify ANSI versus Wide APIs early
- Treat the .klib as the interop boundary and avoid depending on undocumented internals
- Test on each target during development not only at the end
Glossary
Clang
A C/C++ compiler frontend for the LLVM project, used by Kotlin/Native to parse C headers and understand C code structure.
.def file
A definition file that configures how cinterop should process C headers, including compiler flags, linker options, and package names.
GCC
GNU Compiler Collection, a popular open source compiler system that supports C, C++, and other languages.
.kexe
Kotlin/Native executable file extension, the final compiled binary that can run directly on the target platform.
.klib
Kotlin/Native library format that contains compiled code and metadata, can be shared between Kotlin/Native projects.
Konan
The internal name for the Kotlin/Native compiler that transforms Kotlin code into native binaries using LLVM.
LLVM
Low Level Virtual Machine, a compiler infrastructure that Kotlin/Native uses to generate optimized machine code for different platforms.
memScoped
A Kotlin/Native memory management scope that automatically frees allocated native memory when the scope exits.
MinGW
Minimalist GNU for Windows, provides GCC compiler and GNU tools for Windows development.
MSYS2
A software distribution and building platform for Windows that provides a Unix like environment and package management.
POSIX
Portable Operating System Interface, a family of standards for maintaining compatibility between operating systems like Linux and macOS.
StableRef
A Kotlin/Native mechanism to create a stable reference to a Kotlin object that can be passed to C code as a pointer.
Static library
A library (.a, .lib) that is compiled directly into the final executable at link time.
Dynamic library
A library (.so, .dylib, .dll) that is loaded at runtime and shared between multiple programs.
Toolchain
A set of programming tools (compiler, linker, debugger) used together to build software for a specific platform.
Closing note
Describe the C surface in a clear .def file, generate bindings per desktop target, and let Kotlin Native produce fast and predictable binaries that interoperate with your C libraries and system APIs. Keep the surface tight rely on safe allocation patterns and verify each platform along the way. This results in an approachable workflow for teams that want the convenience of Kotlin and the reach of native code on macOS Windows and Linux.