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 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:
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: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 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:
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 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.