Kotlin Help

Mapping struct and union types from C – tutorial

Let's explore which C struct and union declarations are visible from Kotlin and examine advanced C interop-related use cases of Kotlin/Native and multiplatform Gradle builds.

In the tutorial, you'll learn:

Mapping struct and union C types

To understand how Kotlin maps struct and union types, let's declare them in C and examine how they are represented in Kotlin.

In the previous tutorial, you've already created a C library with the necessary files. For this step, update the declarations in the interop.def file after the --- separator:

--- typedef struct { int a; double b; } MyStruct; void struct_by_value(MyStruct s) {} void struct_by_pointer(MyStruct* s) {} typedef union { int a; MyStruct b; float c; } MyUnion; void union_by_value(MyUnion u) {} void union_by_pointer(MyUnion* u) {}

The interop.def file is enough to compile and run the application or open it in an IDE.

Inspect generated Kotlin APIs for a C library

Let's see how C struct and union types are mapped into Kotlin/Native and update your project:

  1. In src/nativeMain/kotlin, update your hello.kt file from the previous tutorial with the following content:

    import interop.* import kotlinx.cinterop.ExperimentalForeignApi @OptIn(ExperimentalForeignApi::class) fun main() { println("Hello Kotlin/Native!") struct_by_value(/* fix me*/) struct_by_pointer(/* fix me*/) union_by_value(/* fix me*/) union_by_pointer(/* fix me*/) }
  2. To avoid compiler errors, add interoperability to the build process. For that, update your build.gradle(.kts) build file with the following content:

    kotlin { macosArm64("native") { // macOS on Apple Silicon // macosX64("native") { // macOS on x86_64 platforms // linuxArm64("native") { // Linux on ARM64 platforms // linuxX64("native") { // Linux on x86_64 platforms // mingwX64("native") { // on Windows val main by compilations.getting val interop by main.cinterops.creating { definitionFile.set(project.file("src/nativeInterop/cinterop/interop.def")) } binaries { executable() } } }
    kotlin { macosArm64("native") { // Apple Silicon macOS // macosX64("native") { // macOS on x86_64 platforms // linuxArm64("native") { // Linux on ARM64 platforms // linuxX64("native") { // Linux on x86_64 platforms // mingwX64("native") { // Windows compilations.main.cinterops { interop { definitionFile = project.file('src/nativeInterop/cinterop/interop.def') } } binaries { executable() } } }
  3. Use the IntelliJ IDEA's Go to declaration command (Cmd + B/Ctrl + B) to navigate to the following generated API for C functions, struct, and union:

    fun struct_by_value(s: kotlinx.cinterop.CValue<interop.MyStruct>) fun struct_by_pointer(s: kotlinx.cinterop.CValuesRef<interop.MyStruct>?) fun union_by_value(u: kotlinx.cinterop.CValue<interop.MyUnion>) fun union_by_pointer(u: kotlinx.cinterop.CValuesRef<interop.MyUnion>?)

Technically, there is no difference between struct and union types on the Kotlin side. The cinterop tool generates Kotlin types for both struct and union C declarations.

The generated API includes fully qualified package names for CValue<T> and CValuesRef<T>, reflecting their location in kotlinx.cinterop. CValue<T> represents a by-value structure parameter, while CValuesRef<T>? is used to pass a pointer to a structure or a union.

Use struct and union types from Kotlin

Using C struct and union types from Kotlin is straightforward thanks to the generated API. The only question is how to create new instances of these types.

Let's take a look at the generated functions that take MyStruct and MyUnion as parameters. By-value parameters are represented as kotlinx.cinterop.CValue<T>, while pointer-typed parameters use kotlinx.cinterop.CValuesRef<T>?.

Kotlin provides a convenient API for creating and working with these types. Let's explore how to use it in practice.

Create a CValue<T>

CValue<T> type is used to pass by-value parameters to a C function call. Use the cValue function to create a CValue<T> instance. The function requires a lambda function with a receiver to initialize the underlying C type in-place. The function is declared as follows:

fun <reified T : CStructVar> cValue(initialize: T.() -> Unit): CValue<T>

Here's how to use cValue and pass by-value parameters:

import interop.* import kotlinx.cinterop.ExperimentalForeignApi import kotlinx.cinterop.cValue @OptIn(ExperimentalForeignApi::class) fun callValue() { val cStruct = cValue<MyStruct> { a = 42 b = 3.14 } struct_by_value(cStruct) val cUnion = cValue<MyUnion> { b.a = 5 b.b = 2.7182 } union_by_value(cUnion) }

Create struct and union as CValuesRef<T>

The CValuesRef<T> type is used in Kotlin to pass a pointer-typed parameter of a C function. To allocate MyStruct and MyUnion in the native memory, use the following extension function on kotlinx.cinterop.NativePlacement type:

fun <reified T : kotlinx.cinterop.CVariable> alloc(): T

NativePlacement represents native memory with functions similar to malloc and free. There are several implementations of NativePlacement:

  • The global implementation is kotlinx.cinterop.nativeHeap, but you must call nativeHeap.free() to release the memory after use.

  • A safer alternative is memScoped(), which creates a short-lived memory scope where all allocations are automatically freed at the end of the block:

    fun <R> memScoped(block: kotlinx.cinterop.MemScope.() -> R): R

With memScoped(), your code for calling functions with pointers can look like this:

import interop.* import kotlinx.cinterop.ExperimentalForeignApi import kotlinx.cinterop.memScoped import kotlinx.cinterop.alloc import kotlinx.cinterop.ptr @OptIn(ExperimentalForeignApi::class) fun callRef() { memScoped { val cStruct = alloc<MyStruct>() cStruct.a = 42 cStruct.b = 3.14 struct_by_pointer(cStruct.ptr) val cUnion = alloc<MyUnion>() cUnion.b.a = 5 cUnion.b.b = 2.7182 union_by_pointer(cUnion.ptr) } }

Here, the ptr extension property, which is available within the memScoped {} block, converts MyStruct and MyUnion instances into native pointers.

Since memory is managed inside the memScoped {} block, it's automatically freed at the end of the block. Avoid using pointers outside of this scope to prevent accessing deallocated memory. If you need longer-lived allocations (for example, for caching in a C library), consider using Arena() or nativeHeap.

Conversion between CValue<T> and CValuesRef<T>

Sometimes you need to pass a struct as a value in one function call and then pass the same struct as a reference in another.

To do this, you'll need a NativePlacement, but first, let's see how CValue<T> is turned into a pointer:

import interop.* import kotlinx.cinterop.ExperimentalForeignApi import kotlinx.cinterop.cValue import kotlinx.cinterop.memScoped @OptIn(ExperimentalForeignApi::class) fun callMix_ref() { val cStruct = cValue<MyStruct> { a = 42 b = 3.14 } memScoped { struct_by_pointer(cStruct.ptr) } }

Here again, the ptr extension property from memScoped {} turns MyStruct instances into native pointers. These pointers are only valid inside the memScoped {} block.

To turn a pointer back into a by-value variable, call the .readValue() extension function:

import interop.* import kotlinx.cinterop.alloc import kotlinx.cinterop.ExperimentalForeignApi import kotlinx.cinterop.memScoped import kotlinx.cinterop.readValue @OptIn(ExperimentalForeignApi::class) fun callMix_value() { memScoped { val cStruct = alloc<MyStruct>() cStruct.a = 42 cStruct.b = 3.14 struct_by_value(cStruct.readValue()) } }

Update Kotlin code

Now that you've learned how to use C declarations in Kotlin code, try to use them in your project. The final code in the hello.kt file may look like this:

import interop.* import kotlinx.cinterop.alloc import kotlinx.cinterop.cValue import kotlinx.cinterop.memScoped import kotlinx.cinterop.ptr import kotlinx.cinterop.readValue @OptIn(ExperimentalForeignApi::class) fun main() { println("Hello Kotlin/Native!") val cUnion = cValue<MyUnion> { b.a = 5 b.b = 2.7182 } memScoped { union_by_value(cUnion) union_by_pointer(cUnion.ptr) } memScoped { val cStruct = alloc<MyStruct> { a = 42 b = 3.14 } struct_by_value(cStruct.readValue()) struct_by_pointer(cStruct.ptr) } }

To verify that everything works as expected, run the runDebugExecutableNative Gradle task in IDE or use the following command to run the code:

./gradlew runDebugExecutableNative

Next step

In the next part of the series, you'll learn how function pointers are mapped between Kotlin and C:

Proceed to the next part

See also

Learn more in the Interoperability with C documentation that covers more advanced scenarios.

Last modified: 07 March 2025