C interop
Kotlin/Native calls C directly. Kargo runs the Kotlin/Native cinterop tool for each C library you declare in kargo.toml, compiles the C files that ship with your project with the toolchain’s own clang, and builds C libraries and Apple frameworks from your Kotlin code. No Gradle, CMake or Makefile is involved.
This page goes through it in that order. The basics of the native target are in Kotlin/Native.
A C library on three platforms
Section titled “A C library on three platforms”One kargo.toml builds a sqlite tool for Linux, macOS and Windows. Each platform finds sqlite differently, so each gets its own options:
[target.native]platforms = ["linux_x64", "macos_arm64", "mingw_x64"]
[target.native.cinterop.sqlite]headers = ["sqlite3.h"] # no .def file neededpackage = "sqlite3"pkg-config = "sqlite3" # flags from sqlite3.pc, on the machine you build on
[target.native.cinterop.sqlite.platform.mingw_x64]compiler-options = ["-Ivendor/sqlite/include"]library-paths = ["vendor/sqlite/lib"]linker-options = ["-lsqlite3"]
[target.native.cinterop.sqlite.platform.macos_arm64]linker-options = ["-lsqlite3"] # macOS ships sqliteimport kotlinx.cinterop.ExperimentalForeignApiimport kotlinx.cinterop.toKStringimport sqlite3.sqlite3_libversion
@OptIn(ExperimentalForeignApi::class)fun main() { println("SQLite " + sqlite3_libversion()?.toKString())}$ kargo run Generating sqlite C bindings (linux_x64) Compiling app v0.1.0 (native, linux_x64) Finished `dev` profile [unoptimized + debuginfo] target(s) in 12.40s Running `target/debug/native/linux_x64/app.kexe`SQLite 3.45.1The bindings use kotlinx.cinterop APIs, which need @OptIn(ExperimentalForeignApi::class), or opt-in = ["kotlinx.cinterop.ExperimentalForeignApi"] in [kotlin].
kargo add --cinterop sqlite --header sqlite3.h --pkg-config sqlite3 writes the [target.native.cinterop.sqlite] entry and an interop/sqlite.def to start from.
Declaring a C library
Section titled “Declaring a C library”Each [target.native.cinterop.<name>] table binds one library. Every key is optional:
| Key | Meaning |
|---|---|
def |
The definition file, relative to the project root. Default: interop/<name>.def |
package |
Kotlin package of the bindings. Default: the package in the .def file |
headers |
Headers to bind, as written in #include. With headers (or modules) and no interop/<name>.def, no .def file is needed |
compiler-options |
Options for clang while it reads the headers: -I include directories, -D defines |
linker-options |
Linker options stored in the bindings and applied to every program that uses them: -L, -l |
library-paths |
Directories searched for static-libraries, also passed to the linker as -L. Relative to the project root |
static-libraries |
Static libraries (libfoo.a) found in library-paths and embedded in the bindings, so programs need no installed copy |
frameworks |
Apple frameworks to link (-framework <name>). Ignored on other platforms |
pkg-config |
A package name, or a list of them, whose compiler and linker flags the bindings use. See pkg-config |
link |
"dynamic" (default) or "static". See Static linking |
header-filter, excluded-functions, strict-enums, non-strict-enums, modules, language |
.def properties written in kargo.toml. See .def properties |
platform.<platform> |
Options for one platform. See Per-platform options |
Relative paths in def and compiler-options resolve against the project root, so -Iinclude means include/ next to kargo.toml.
A .def file can hold C code after a --- line, which is compiled into the bindings. That suits a few small helpers; for real C files use bundled C sources.
headers = twice.h---static int add(int a, int b) { return a + b; }Kotlin/Native libraries from Maven that already contain C bindings (Ktor, okio, SQLDelight’s driver) need no cinterop entry: add them to [dependencies].
Per-platform options
Section titled “Per-platform options”A platform.<platform> table adds options for one Kotlin/Native platform to the common ones. Use the names from platforms (linux_x64, macos_arm64, mingw_x64, …; linuxX64 works too). host is not allowed here: name the platform.
Under a cinterop entry the table takes compiler-options, linker-options, static-libraries, library-paths and frameworks:
[target.native.cinterop.curl]headers = ["curl/curl.h"]package = "libcurl"
[target.native.cinterop.curl.platform.linux_x64]compiler-options = ["-I/usr/include", "-I/usr/include/x86_64-linux-gnu"]linker-options = ["-L/usr/lib/x86_64-linux-gnu", "-lcurl"]
[target.native.cinterop.curl.platform.macos_arm64]compiler-options = ["-I/opt/homebrew/opt/curl/include"]linker-options = ["-L/opt/homebrew/opt/curl/lib", "-lcurl"]
[target.native.cinterop.curl.platform.mingw_x64]compiler-options = ["-IC:/msys64/ucrt64/include"]linker-options = ["-LC:/msys64/ucrt64/lib", "-lcurl"][target.native] has the same tables for the linker options of your program, for libraries that are not bound with cinterop. They take linker-options, static-libraries, library-paths and frameworks:
[target.native]platforms = ["linux_x64", "macos_arm64", "mingw_x64"]linker-options = ["-lm"] # every platform
[target.native.platform.macos_arm64]frameworks = ["CoreFoundation"]
[target.native.platform.mingw_x64]linker-options = ["-lws2_32"].def properties
Section titled “.def properties”These keys write .def file properties from kargo.toml, so a library rarely needs its own .def file:
| Key | .def property |
Meaning |
|---|---|---|
header-filter |
headerFilter |
Globs of the headers whose declarations are bound; others are only read |
excluded-functions |
excludedFunctions |
Functions left out of the bindings |
strict-enums |
strictEnums |
C enums bound as Kotlin enum classes |
non-strict-enums |
nonStrictEnums |
C enums bound as integer constants |
modules |
modules |
Clang modules to bind, such as Objective-C frameworks |
language |
language |
"C" or "Objective-C" |
[target.native.cinterop.sdl]headers = ["SDL2/SDL.h"]package = "sdl"header-filter = ["SDL2/*"]excluded-functions = ["SDL_main"]Kargo writes them with the contents of the project’s .def file, if there is one, into target/<profile>/native/<platform>/cinterop/<name>.def. A key set in both places takes the .def file’s value.
The linker options of the entry for that platform go into the same file as linkerOpts: -L for each of library-paths, linker-options (pkg-config’s included) and, on Apple platforms, -framework for each of frameworks. cinterop stores the .def file’s linkerOpts in the klib, so every program that links the bindings gets them, including programs of other projects that depend on your published library. If the project’s .def file sets linkerOpts too, Kargo appends its options to that value.
pkg-config
Section titled “pkg-config”pkg-config = "<package>" takes the compiler and linker flags of an installed library from its <package>.pc file:
[target.native.cinterop.curl]headers = ["curl/curl.h"]package = "libcurl"pkg-config = "libcurl"Kargo reads the .pc files itself, including the packages they require, so no pkg-config program is needed. It looks in this order:
- The directories in
PKG_CONFIG_PATH. - The directories an installed
pkg-config(orpkgconf) searches. - The usual places: Homebrew (
/opt/homebrew,/usr/local) and MacPorts on macOS;/usr,/usr/localand Linuxbrew on Linux; MSYS2 (C:/msys64/ucrt64,mingw64,clang64andMSYSTEM_PREFIX) on Windows; and vcpkg’sinstalled/<triplet>underVCPKG_ROOT.
Unlike pkg-config, Kargo keeps -I/usr/include and -L/usr/lib/..., which the Kotlin/Native sysroot needs. pkg-config describes the machine you build on, so its flags only apply to the host platform. Give the other platforms their options in platform.<platform> tables.
A missing package stops the build before anything is compiled:
$ kargo builderror: C library `sqlite3` of [target.native.cinterop.sqlite] not found: no pkg-config file `sqlite3.pc` was found (searched /usr/lib/x86_64-linux-gnu/pkgconfig, /usr/share/pkgconfig); install its development package (such as `apt install libsqlite3-dev` or `dnf install sqlite3-devel`), set PKG_CONFIG_PATH to the directory holding `sqlite3.pc`, or give its options in [target.native.cinterop.sqlite.platform.linux_x64] instead of `pkg-config`Static linking
Section titled “Static linking”With link = "static", every -l<name> option whose lib<name>.a is in a library directory (library-paths, -L options and pkg-config’s -L flags) is embedded in the bindings instead. The executable then runs on machines without the library. Libraries without a static archive, such as the system’s -lm or -lpthread, stay dynamic. pkg-config adds the package’s Libs.private, which a static link needs.
[target.native.cinterop.sqlite]headers = ["sqlite3.h"]package = "sqlite3"pkg-config = "sqlite3"link = "static"To embed a library you vendor, list it in static-libraries:
[target.native.cinterop.sqlite.platform.mingw_x64]compiler-options = ["-Ivendor/sqlite/include"]library-paths = ["vendor/sqlite/lib/mingw_x64"]static-libraries = ["libsqlite3.a"]link = "static" fails when it finds no static library at all, so a missing archive doesn’t silently produce a dynamically linked program.
Bundled C sources
Section titled “Bundled C sources”[target.native.c-sources] compiles the C, C++ and Objective-C files of your project with the clang that comes with Kotlin/Native, for each platform with that platform’s sysroot, into a static library that every native binary links. It works like Rust’s cc crate:
[target.native]platforms = ["host"]
[target.native.c-sources] # compiles c/**/*.c with -Icdefines = ["FACTOR=2"]
[target.native.c-sources.platform.mingw_x64]defines = ["BONUS=100"]
[target.native.cinterop.mathx] # the bindings for c/mathx.hheaders = ["mathx.h"]package = "mathx"int mathx_scale(int x);#include "mathx.h"#ifndef BONUS#define BONUS 0#endifint mathx_scale(int x) { return x * FACTOR + BONUS; }import kotlinx.cinterop.ExperimentalForeignApi
@OptIn(ExperimentalForeignApi::class)fun main() = println("scaled " + mathx.mathx_scale(21))$ kargo run Generating mathx C bindings (mingw_x64) Compiling app v0.1.0 (native, mingw_x64, C sources) Compiling app v0.1.0 (native, mingw_x64) Finished `dev` profile [unoptimized + debuginfo] target(s) in 8.00s Running `target\debug\native\mingw_x64\app.exe`scaled 142| Key | Meaning |
|---|---|
sources |
Files, directories and globs (gitignore syntax, such as vendor/**/*.c), relative to the project root. A directory contributes its .c, .cc, .cpp, .cxx, .m and .mm files. Default: ["c"] |
include-dirs |
Include directories (-I), relative to the project root. Every cinterop entry gets them too, so its headers are found. Default: ["c"] |
defines |
Macros: NAME or NAME=value |
compiler-options |
More clang options, such as -std=c11 or -Wall |
platform.<platform> |
defines and compiler-options for one platform |
C++ files (.cc, .cpp, .cxx, .mm) are compiled with clang++. Debug profiles compile with -O0 -g, optimized ones with -O2.
The library is target/<profile>/native/<platform>/c/lib<package>_c.a. Executables, test runners, shared and static libraries and frameworks link it; with kind = "library" it is packed into the klib, so whatever links the klib gets the C code. It is rebuilt when a source, a file in an include directory, an option or the Kotlin version changes.
Calling Kotlin from C
Section titled “Calling Kotlin from C”kind = "shared" or kind = "static" builds a C library from your Kotlin code. Kotlin/Native also writes a C header for it, which Kargo copies into target/<profile>/native/<platform>/include/:
[package]name = "mathlib"version = "0.1.0"
[target.native]platforms = ["linux_x64"]kind = "shared"@file:OptIn(kotlin.experimental.ExperimentalNativeApi::class)
import kotlin.native.CName
@CName("mathlib_add")fun add(a: Int, b: Int): Int = a + b$ kargo build Compiling mathlib v0.1.0 (native, linux_x64) Finished `dev` profile [unoptimized + debuginfo] target(s) in 9.12s$ ls target/debug/native/linux_x64/include/libmathlib_api.h#include "libmathlib_api.h"#include <stdio.h>
int main(void) { printf("%d\n", mathlib_add(20, 22)); return 0;}cc main.c -Itarget/debug/native/linux_x64/include -Ltarget/debug/native/linux_x64 -lmathlib -o mainFunctions marked @CName get that plain C name. Everything else public is reachable through the structure that the header’s ..._symbols() function returns. The files are named lib<name>.so, lib<name>.dylib, <name>.dll (with <name>_api.h) or lib<name>.a.
Calling Kotlin from Swift and Objective-C
Section titled “Calling Kotlin from Swift and Objective-C”kind = "framework" builds an Apple framework for each platform in platforms, which must all be Apple platforms. Frameworks are built on macOS.
[package]name = "shared"group = "com.example"version = "0.1.0"
[target.native]platforms = ["ios_arm64", "ios_simulator_arm64", "ios_x64", "macos_arm64"]kind = "framework"bundle-id = "com.example.Shared" # default: <group>.<name>export = ["org.jetbrains.kotlinx:kotlinx-datetime"]
[dependencies]"org.jetbrains.kotlinx:kotlinx-datetime" = "0.7.1"fun greeting(name: String): String = "Hello, $name!"The framework is target/<profile>/native/<platform>/shared.framework (dashes in the package name become underscores, since the name is a Swift module name). Swift sees top-level functions on a class named after their file:
import shared
let text = GreetingKt.greeting(name: "Swift")| Key | Meaning |
|---|---|
bundle-id |
The framework’s CFBundleIdentifier. Default: <group>.<name> when [package] group is set |
export |
Dependencies (group:artifact) whose API the framework’s header includes, so Swift can use their types. Also works with kind = "shared" and "static" |
To use one framework for devices and simulators, combine them into an XCFramework:
$ kargo build --release --xcframework Compiling shared v0.1.0 (native, ios_arm64) Compiling shared v0.1.0 (native, ios_simulator_arm64) Compiling shared v0.1.0 (native, ios_x64) Compiling shared v0.1.0 (native, macos_arm64) Packaging shared.xcframework Finished `release` profile [optimized] target(s) in 3m 02sKargo merges the frameworks of the same slice (the arm64 and x64 iOS simulators) with lipo, then runs xcodebuild -create-xcframework. The result is target/<profile>/native/shared.xcframework; drag it into an Xcode project’s Frameworks, Libraries, and Embedded Content. It needs Xcode’s command-line tools.
When something is missing
Section titled “When something is missing”Errors from clang and the linker are shown as they are, with their file and line. Kargo adds a note on what to change for the two most common failures:
$ kargo build Generating mathx C bindings (linux_x64)Exception in thread "main" java.lang.Error: /tmp/4645017247209148547.c:1:10: fatal error: 'mathx.h' file not foundnote: the C header `mathx.h` was not found: add the directory that holds it to `compiler-options` (`-I<dir>`) of [target.native.cinterop.mathx] or of its `platform.<platform>` table, name the library's package in `pkg-config`, or install the library's development fileserror: could not generate the `mathx` C bindings for linux_x64 due to previous errors$ kargo buildld.lld: error: undefined symbol: mathx_scalenote: the linker did not find `mathx_scale`: link the C library that defines it with `pkg-config`, `linker-options = ["-l<library>"]` or `static-libraries` in its [target.native.cinterop.<name>] entry (or [target.native.platform.<platform>]), or add its sources to [target.native.c-sources]error: could not compile `app` for linux_x64 due to previous errorsUp-to-date checks
Section titled “Up-to-date checks”Bindings are generated once per platform into target/<profile>/native/<platform>/cinterop/<name>.klib. They are regenerated when the .def file, the options (pkg-config’s included), a static library, a file in a project-relative -I directory or the Kotlin version changes. Run kargo clean after updating a system library.
Publishing
Section titled “Publishing”kargo publish uploads each cinterop klib next to the platform’s klib with the classifier cinterop-<name>, the way Kotlin Multiplatform libraries do. With [target.native.c-sources] the C library travels inside the klib. See Publishing.