Mapping struct and union types from C – tutorial
This is the second part of the Mapping Kotlin and C tutorial series. Before proceeding, make sure you've completed the previous step.
Mapping primitive data types from C
Mapping struct and union types from C
Mapping function pointers
Mapping strings from C
warning
The C libraries import is Experimental. All Kotlin declarations generated by the cinterop tool from C libraries should have the
@ExperimentalForeignApi
annotation.Native platform libraries shipped with Kotlin/Native (like Foundation, UIKit, and POSIX) require opt-in only for some APIs.
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:
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 provides everything necessary to compile, run, or open the application in an IDE.
Let's see how C struct and union types are mapped into Kotlin/Native and update your project:
In
src/nativeMain/kotlin
, update yourhello.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*/) }
To avoid compiler errors, add interoperability to the build process. For that, update your
build.gradle(.kts)
build file with the following content:KotlinGroovykotlin { 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.
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.
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)
}
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 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:
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
.
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())
}
}
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
import kotlinx.cinterop.ExperimentalForeignApi
@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 your IDE or use the following command to run the code:
./gradlew runDebugExecutableNative
In the next part of the series, you'll learn how function pointers are mapped between Kotlin and C:
Learn more in the Interoperability with C documentation that covers more advanced scenarios.
Thanks for your feedback!