https://staticvar.dev/post/kotlin-native-c-interop/ staticvar.dev * Home * Posts * Nexus/ * Reading list/ * Resume [?] From Source to Sorcery - Kotlin Native's Interop Magic From Source to Sorcery - Kotlin Native's Interop Magic Pragmatic, desktop focused guide to Kotlin/Native C interop on macOS, Windows, and Linux: .def file anatomy, Gradle cinterops, type mapping, memory safety patterns, callbacks, string/encoding, platform specific linking and packaging, error handling, performance tuning, debugging, and checklists. September 5, 2025 6 min read kotlinkotlin nativecinteropmacOSWindowsLinux Listen to this article AI-generated narration 0.5x0.75x1x1.25x1.5x 0:00 / -:-- Your browser does not support the audio element. Unable to load audio. Download MP3 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 1. Write a definition file with extension .def that tells interop which headers to import and how to parse them 2. Configure Gradle so cinterop runs for each desktop target you build 3. 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 # What to import headers = curl/curl.h headerFilter = curl/** # Where the generated symbols live package = com.example.curl # Platform specific flags # You can use general platform flags (osx, linux, windows) compilerOpts.linux = -I/usr/include -I/usr/include/x86_64-linux-gnu linkerOpts.linux = -L/usr/lib/x86_64-linux-gnu -lcurl compilerOpts.osx = -I/opt/homebrew/opt/curl/include -I/usr/local/opt/curl/include linkerOpts.osx = -L/opt/homebrew/opt/curl/lib -L/usr/local/opt/curl/lib -lcurl -framework Security -framework SystemConfiguration compilerOpts.windows = -IC:/msys64/mingw64/include linkerOpts.windows = -LC:/msys64/mingw64/lib -lcurl -lws2_32 # Or use more specific target flags for fine-grained control # compilerOpts.macos_x64 = -I/usr/local/opt/curl/include # compilerOpts.macos_arm64 = -I/opt/homebrew/opt/curl/include # Optional quality of life knobs # staticLibraries = libcurl.a # libraryPaths = /opt/homebrew/opt/curl/lib Definition file properties Property Description Example A space separated list of C headers header files to process. headers = my_lib.h This is the core input for another_header.h the tool Specifies the Kotlin package = package package where the generated com.mycompany.clib bindings will be placed A glob pattern to selectively include declarations only from headerFilter specific headers. Useful headerFilter = my_lib/** for reducing binary size and avoiding conflicts with system headers Passes flags directly to the underlying C compiler compilerOpts = -I/usr/ (Clang) used for parsing local/ compilerOpts headers. Essential for includecompilerOpts.linux defining macros or adding = -DDEBUG=1 include paths. Can be platform specific Passes flags directly to the linker. Used to link linkerOpts = -L/usr/local linkerOpts against the actual C /lib -lmy_lib library (.so, .dylib, .a). Can be platform-specific Specifies static libraries staticLibraries (.a) to be bundled directly staticLibraries = into the resulting .klib libfoo.a file (Experimental) A space separated list of directories to search for libraryPaths the static libraries libraryPaths = /path/to/ specified in libs staticLibraries (Experimental) --------------------------------------------------------------------- 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. kotlin { macosArm64() macosX64() linuxX64() mingwX64() targets.withType { compilations["main"].cinterops { val curl by creating { definitionFile.set(project.layout.projectDirectory.file("src/nativeInterop/cinterop/libcurl.def")) packageName("com.example.curl") // Gradle side setting not a .def key } } } } 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 C type Kotlin Native type Notes 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 UInt ULong use unsigned types to match unsigned long semantics float double Float Double direct mapping T* CPointer? nullable to represent null T* parameter CValuesRef? accepts a pointer or a contiguous sequence use toKString to read and char* CPointer? string.cstr to pass, wide APIs on Windows use wcstr struct S by CValue build with cValue block and read value with useContents allocate with struct S* alloc() inside pass via .ptr memScoped function CPointer Int>>? staticCFunction, state via (*f)(int) StableRef Size and numeric conversion * Use convert() 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 // Array literal without manual alloc foo(cValuesOf(1, 2, 3), 3) // Struct literal val p: CValue = cValue { x = 10; y = 20 } usePoint(p) // Inspect contents p.useContents { println("$x,$y") } // Create a modified copy (CValue is immutable) val movedPoint = p.copy { x = 15 // Change x while keeping y the same } Passing a struct by pointer import kotlinx.cinterop.* @OptIn(ExperimentalForeignApi::class) fun processStruct() { memScoped { val s = alloc() s.field1 = 100 s.field2 = 3.14 process_struct_by_pointer(s.ptr) } } Reading into a pinned buffer import kotlinx.cinterop.* import platform.posix.* @OptIn(ExperimentalForeignApi::class) fun readFile(fd: Int) { val buffer = ByteArray(1024) buffer.usePinned { pinned -> val n = read(fd, pinned.addressOf(0), buffer.size.convert()) // process 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 val cb = staticCFunction { code: Int -> println("code is $code") } Stable user data example class Context(val message: String) val ref = StableRef.create(Context("hello")) val user = ref.asCPointer() // pass user to C API // inside callback recover it val ctx = user.asStableRef().get() println(ctx.message) ref.dispose() --------------------------------------------------------------------- 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: noStringConversion = LoadCursorA LoadCursorW processRawData 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 import kotlinx.cinterop.* @OptIn(ExperimentalForeignApi::class) fun safeOperation(): Result { val result = c_function_that_may_fail() return if (result < 0) { Result.failure(Exception("Operation failed with code: $result")) } else { Result.success("Success") } } Working with errno import kotlinx.cinterop.* import platform.posix.* @OptIn(ExperimentalForeignApi::class) fun readWithErrorCheck(fd: Int, buffer: ByteArray): Int { errno = 0 // Clear errno before the call val bytesRead = buffer.usePinned { pinned -> read(fd, pinned.addressOf(0), buffer.size.convert()) } if (bytesRead < 0) { val errorCode = errno val errorMessage = strerror(errorCode)?.toKString() ?: "Unknown error" throw IOException("Read failed: $errorMessage (errno: $errorCode)") } return bytesRead.toInt() } --------------------------------------------------------------------- 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 # In your .def file compilerOpts = -v # Verbose Clang output excludedFunctions = problematic_function # Skip specific functions strictEnums = CURLcode MyErrorCode # Generate these C enums as Kotlin enums (space separated list) 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() 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. Categories:Kotlin Multiplatform - Previous Detekt Convention Plugin Next - Four Doors, One Party - K/ N, JNI, JNA & FFM Copyright (c) 2020-2025 by Shreyansh Lodha [px]