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:
The interop.def file provides everything necessary to compile, run, or open the application 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:
In
src/nativeMain/kotlin, update yourhello.ktfile 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*/) }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() } } }Use 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:
Here's how to use cValue and pass by-value parameters:
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:
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 callnativeHeap.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:
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:
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:
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:
To verify that everything works as expected, run the runDebugExecutableNative Gradle task in your IDE or use the following command to run the code:
Next step
In the next part of the series, you'll learn how function pointers are mapped between Kotlin and C:
See also
Learn more in the Interoperability with C documentation that covers more advanced scenarios.