Interoperability with C
This document covers general aspects of Kotlin's interoperability with C. Kotlin/Native comes with a cinterop tool, which you can use to quickly generate everything you need to interact with an external C library.
The tool analyzes C headers and produces a straightforward mapping of C types, functions, and constants into Kotlin. The generated stubs then can be imported into an IDE to enable code completion and navigation.
Setting up your project
Here's a general workflow when working with a project that needs to consume a C library:
Create and configure a definition file. It describes what the cinterop tool should include into Kotlin bindings.
Configure your Gradle build file to include cinterop in the build process.
Compile and run the project to produce the final executable.
In many cases, there's no need to configure custom interoperability with a C library. Instead, you can use APIs available on the platform standardized bindings called platform libraries. For example, POSIX on Linux/macOS platforms, Win32 on the Windows platform, or Apple frameworks on macOS/iOS are available this way.
Bindings
Basic interop types
All the supported C types have corresponding representations in Kotlin:
Signed, unsigned integral, and floating point types are mapped to their Kotlin counterpart with the same width.
Pointers and arrays are mapped to
CPointer<T>?
.Enums can be mapped to either Kotlin enum or integral values, depending on heuristics and the definition file settings.
Structs and unions are mapped to types having fields available via the dot notation, i.e.
someStructInstance.field1
.typedef
are represented astypealias
.
Also, any C type has the Kotlin type representing the lvalue of this type, i.e., the value located in memory rather than a simple immutable self-contained value. Think C++ references as a similar concept. For structs (and typedef
s to structs), this representation is the main one and has the same name as the struct itself. For Kotlin enums, it's named ${type}.Var
; for CPointer<T>
, it's CPointerVar<T>
; and for most other types, it's ${type}Var
.
For types that have both representations, the one with the lvalue has a mutable .value
property for accessing the value.
Pointer types
The type argument T
of CPointer<T>
must be one of the lvalue types described above. For example, the C type struct S*
is mapped to CPointer<S>
, int8_t*
is mapped to CPointer<int_8tVar>
, and char**
is mapped to CPointer<CPointerVar<ByteVar>>
.
C null pointer is represented as Kotlin's null
, and the pointer type CPointer<T>
is not nullable, but the CPointer<T>?
is. The values of this type support all the Kotlin operations related to handling null
, for example, ?:
, ?.
, !!
, and so on:
Since the arrays are also mapped to CPointer<T>
, it supports the []
operator for accessing values by index:
The .pointed
property for CPointer<T>
returns the lvalue of type T
, pointed by this pointer. The reverse operation is .ptr
, it takes the lvalue and returns the pointer to it.
void*
is mapped to COpaquePointer
– the special pointer type which is the supertype for any other pointer type. So if the C function takes void*
, the Kotlin binding accepts any CPointer
.
Casting a pointer (including COpaquePointer
) can be done with .reinterpret<T>
, for example:
Or:
As is with C, these .reinterpret
casts are unsafe and can potentially lead to subtle memory problems in the application.
Also, there are unsafe casts between CPointer<T>?
and Long
available, provided by the .toLong()
and .toCPointer<T>()
extension methods:
Memory allocation
The native memory can be allocated using the NativePlacement
interface, for example:
Or:
The most logical placement is in the object nativeHeap
. It corresponds to allocating native memory with malloc
and provides an additional .free()
operation to free allocated memory:
nativeHeap
requires memory to be freed manually. However, it's often useful to allocate memory with a lifetime bound to the lexical scope. It's helpful if such memory is freed automatically.
To address this, you can use memScoped { }
. Inside the braces, the temporary placement is available as an implicit receiver, so it's possible to allocate native memory with alloc and allocArray, and the allocated memory will be automatically freed after leaving the scope.
For example, a C function returning values through pointer parameters can be used like:
Pass pointers to bindings
Although C pointers are mapped to the CPointer<T> type
, C function pointer-typed parameters are mapped to CValuesRef<T>.
When passing a CPointer<T>
as a value of such a parameter, it's passed to the C function as is. However, a sequence of values can be passed instead of a pointer. In this case, the sequence is passed "by value", i.e., the C function receives the pointer to the temporary copy of that sequence, which is valid only until the function returns.
The CValuesRef<T>
representation of pointer parameters is designed to support C array literals without explicit native memory allocation. To construct the immutable self-contained sequence of C values, the following methods are provided:
${type}Array.toCValues()
, wheretype
is the Kotlin primitive typeArray<CPointer<T>?>.toCValues()
,List<CPointer<T>?>.toCValues()
cValuesOf(vararg elements: ${type})
, wheretype
is a primitive or pointer
For example:
Strings
Unlike other pointers, the parameters of type const char*
are represented as a Kotlin String
. So it's possible to pass any Kotlin string to a binding expecting a C string.
There are also some tools available to convert between Kotlin and C strings manually:
fun CPointer<ByteVar>.toKString(): String
val String.cstr: CValuesRef<ByteVar>
.
To get the pointer, .cstr
should be allocated in native memory, for example:
In all cases, the C string is supposed to be encoded as UTF-8.
To skip automatic conversion and ensure raw pointers are used in the bindings, add the noStringConversion
property to the .def
file:
This way, any value of type CPointer<ByteVar>
can be passed as an argument of const char*
type. If a Kotlin string should be passed, code like this could be used:
Scope-local pointers
It's possible to create a scope-stable pointer of C representation for the CValues<T>
instance using the CValues<T>.ptr
extension property, available under memScoped {}
. It allows using APIs that require C pointers with a lifetime bound to a certain MemScope
. For example:
In this example, all values passed to the C API new_menu()
have a lifetime of the innermost memScope
it belongs to. Once the control flow leaves the memScoped
scope, C pointers become invalid.
Pass and receive structs by value
When a C function takes or returns a struct/union T
by value, the corresponding argument type or return type is represented as CValue<T>
.
CValue<T>
is an opaque type, so the structure fields cannot be accessed with the appropriate Kotlin properties. This can be fine if an API uses structures as opaque handles. However, if field access is required, the following conversion methods are available:
fun T.readValue(): CValue<T>
converts (the lvalue)T
to aCValue<T>
. So to construct theCValue<T>
,T
can be allocated, filled, and then converted toCValue<T>
.CValue<T>.useContents(block: T.() -> R): R
temporarily stores theCValue<T>
in memory, and then runs the passed lambda with this placed valueT
as receiver. So to read a single field, you can use the following code:val fieldValue = structValue.useContents { field }fun cValue(initialize: T.() -> Unit): CValue<T>
applies the providedinitialize
function to allocateT
in memory and converts the result into aCValue<T>
.fun CValue<T>.copy(modify: T.() -> Unit): CValue<T>
creates a modified copy of an existingCValue<T>
. The original value is placed in memory, altered using themodify()
function, and then converted back into a newCValue<T>
.fun CValues<T>.placeTo(scope: AutofreeScope): CPointer<T>
places theCValues<T>
into anAutofreeScope
, returning a pointer to the allocated memory. The allocated memory is automatically freed when theAutofreeScope
is disposed.
Callbacks
To convert a Kotlin function to a pointer to a C function, you can use staticCFunction(::kotlinFunction)
. It's also possible to provide a lambda instead of a function reference. The function or lambda must not capture any values.
Pass user data to callbacks
Often C APIs allow passing some user data to callbacks. Such data is usually provided by the user when configuring the callback. It's passed to some C function (or written to the struct) as void*
, for example. However, references to Kotlin objects can't be directly passed to C. So they require wrapping before configuring the callback and then unwrapping in the callback itself, to safely swim from Kotlin to Kotlin through the C world. Such wrapping is possible with the StableRef
class.
To wrap the reference:
Here, the voidPtr
is a COpaquePointer
and can be passed to the C function.
To unwrap the reference:
Here, kotlinReference
is the original wrapped reference.
The created StableRef
eventually be manually disposed using the .dispose()
method to prevent memory leaks:
After that it becomes invalid, so voidPtr
can't be unwrapped anymore.
Macros
Every C macro that expands to a constant is represented as a Kotlin property.
Macros without parameters are supported in cases when the compiler can infer the type:
In this case, FOO
is available in Kotlin.
To support other macros, you can expose them manually by wrapping them with supported declarations. For example, function-like macro FOO
can be exposed as a function foo()
by adding custom declaration to the library:
Portability
Sometimes the C libraries have function parameters or struct fields of a platform-dependent type, for example, long
or size_t
. Kotlin itself doesn't provide either implicit integer casts or C-style integer casts (for example, (size_t) intValue
), so to make writing portable code in such cases easier, the convert
method is provided:
Here, each of type1
and type2
must be an integral type, either signed or unsigned.
.convert<${type}>
has the same semantics as one of the .toByte
, .toShort
, .toInt
, .toLong
, .toUByte
, .toUShort
, .toUInt
or .toULong
methods, depending on type
.
An example of using convert
:
Also, the type parameter can be inferred automatically and so may be omitted in some cases.
Object pinning
Kotlin objects could be pinned, i.e. their position in memory is guaranteed to be stable until they are unpinned, and pointers to such objects' inner data could be passed to C functions.
There's a couple of approaches you can take:
Use the
usePinned
service function that pins an object, executes a block, and unpins it on normal and exception paths:import kotlinx.cinterop.* import platform.posix.* @OptIn(ExperimentalForeignApi::class) fun readData(fd: Int) { val buffer = ByteArray(1024) buffer.usePinned { pinned -> while (true) { val length = recv(fd, pinned.addressOf(0), buffer.size.convert(), 0).toInt() if (length <= 0) { break } // Now `buffer` has raw data obtained from the `recv()` call. } } }Here,
pinned
is an object of a special typePinned<T>
. It provides useful extensions likeaddressOf
, which allows getting the address of a pinned array body.Use the
refTo()
function that has similar functionality under the hood but, in certain cases, may help you reduce boilerplate code:import kotlinx.cinterop.* import platform.posix.* @OptIn(ExperimentalForeignApi::class) fun readData(fd: Int) { val buffer = ByteArray(1024) while (true) { val length = recv(fd, buffer.refTo(0), buffer.size.convert(), 0).toInt() if (length <= 0) { break } // Now `buffer` has raw data obtained from the `recv()` call. } }Here,
buffer.refTo(0)
has theCValuesRef
type that pins the array before entering therecv()
function, passes the address of its zeroth element to the function, and unpins the array after exiting.
Forward declarations
To import forward declarations, use the cnames
package. For example, to import a cstructName
forward declaration declared in a C library with a library.package
, use a special forward declaration package: import cnames.structs.cstructName
.
Consider two cinterop libraries: one that has a forward declaration of a struct and another with an actual implementation in another package:
To transfer objects between the two libraries, use an explicit as
cast in your Kotlin code:
What's next
Learn how types, functions, and constants are mapped between Kotlin and C by completing the following tutorials: