Mapping struct and union types from C – tutorial
This is the second post in the series. The very first tutorial of the series is Mapping primitive data types from C. There are also the Mapping function pointers from C and Mapping Strings from C tutorials.
In the tutorial, you will learn:
Mapping struct and union C types
The best way to understand the mapping between Kotlin and C is to try a tiny example. We will declare a struct and a union in the C language, to see how they are mapped into Kotlin.
Kotlin/Native comes with the
cinterop tool, the tool generates bindings between the C language and Kotlin. It uses a
.def file to specify a C library to import. More details are discussed in the Interop with C Libraries tutorial.
In the previous tutorial, you've created a
lib.h file. This time, include those declarations directly into the
interop.def file, after the
--- separator line:
interop.def file is enough to compile and run the application or open it in an IDE. Now it is time to create project files, open the project in IntelliJ IDEA and run it.
Inspect Generated Kotlin APIs for a C library
While it is possible to use the command line, either directly or by combining it with a script file (such as
.bat file), this approach doesn't scale well for big projects that have hundreds of files and libraries. It is then better to use the Kotlin/Native compiler with a build system, as it helps to download and cache the Kotlin/Native compiler binaries and libraries with transitive dependencies and run the compiler and tests. Kotlin/Native can use the Gradle build system through the kotlin-multiplatform plugin.
We covered the basics of setting up an IDE compatible project with Gradle in the A Basic Kotlin/Native Application tutorial. Please check it out if you are looking for detailed first steps and instructions on how to start a new Kotlin/Native project and open it in IntelliJ IDEA. In this tutorial, we'll look at the advanced C interop related usages of Kotlin/Native and multiplatform builds with Gradle.
First, create a project folder. All the paths in this tutorial will be relative to this folder. Sometimes the missing directories will have to be created before any new files can be added.
Use the following
build.gradle(.kts) Gradle build file:
The project file configures the C interop as an additional step of the build. Let's move the
interop.def file to the
src/nativeInterop/cinterop directory. Gradle recommends using conventions instead of configurations, for example, the source files are expected to be in the
src/nativeMain/kotlin folder. By default, all the symbols from C are imported to the
interop package, you may want to import the whole package in our
.kt files. Check out the kotlin-multiplatform plugin documentation to learn about all the different ways you could configure it.
src/nativeMain/kotlin/hello.kt stub file with the following content to see how C struct and union declarations are visible from Kotlin:
Now you are ready to open the project in IntelliJ IDEA and to see how to fix the example project. While doing that, see how C struct and union types are mapped into Kotlin/Native.
Struct and union types in Kotlin
With the help of IntelliJ IDEA's Go to | Declaration or compiler errors, you see the following generated API for the C functions,
You see that
cinterop generated wrapper types for our
union types. For
MyUnion type declarations in C, there are the Kotlin classes
MyUnion generated respectively. The wrappers inherit from the
CStructVar base class and declare all fields as Kotlin properties. It uses
CValue<T> to represent a by-value structure parameter and
CValuesRef<T>? to represent passing a pointer to a structure or a union.
Technically, there is no difference between
union types on the Kotlin side. Note that
c properties of
MyUnion class in Kotlin use the same memory location to read/write their value just like
union does in C language.
More details and advanced use-cases are presented in the
C Interop documentation
Use struct and union types from Kotlin
It is easy to use the generated wrapper classes for C
union types from Kotlin. Thanks to the generated properties, it feels natural to use them in Kotlin code. The only question, so far, is how to create a new instance on those classes. As you see from the declarations of
MyUnion, their constructors require a
NativePtr. Of course, you are not willing to deal with pointers manually. Instead, you can use Kotlin API to have those objects instantiated for us.
Let's take a look at the generated functions that take our
MyUnion as parameters. You see that by-value parameters are represented as
kotlinx.cinterop.CValue<T>. And for typed pointer parameters you see
kotlinx.cinterop.CValuesRef<T>. Kotlin provides us with an API to deal with both types easily, let's try it and see.
Create a CValue
CValue<T> type is used to pass by-value parameters to a C function call. Use
cValue function to create
CValue<T> object instance. The function requires a lambda function with a receiver to initialize the underlying C type in-place. The function is declared as follows:
Now it is time to see how to use
cValue and pass by-value parameters:
Create struct and union as CValuesRef
CValuesRef<T> type is used in Kotlin to pass a typed pointer parameter of a C function. First, you need an instance of
MyUnion classes. Create them directly in the native memory. Use the
extension function on
kotlinx.cinterop.NativePlacement type for this.
NativePlacement represents native memory with functions similar to
free. There are several implementations of
NativePlacement. The global one is called with
kotlinx.cinterop.nativeHeap and don't forget to call the
nativeHeap.free(..) function to free the memory after use.
Another option is to use the
function. It creates a short-lived memory allocation scope, and all allocations will be cleaned up automatically at the end of the
Your code to call functions with pointers will look like this:
Note that this code uses the extension property
ptr which comes from a
memScoped lambda receiver type, to turn
MyUnion instances into native pointers.
MyUnion classes have the pointer to the native memory underneath. The memory will be released when a
memScoped function ends, which is equal to the end of its
block. Make sure that a pointer is not used outside of the
memScoped call. You may use
nativeHeap for pointers that should be available longer, or are cached inside a C library.
Conversion between CValue
Of course, there are use cases when you need to pass a struct as a value to one call, and then, to pass the same struct as a reference to another call. This is possible in Kotlin/Native too. A
NativePlacement will be needed here.
Let's see now
CValue<T> is turned to a pointer first:
This code uses the extension property
ptr which comes from
memScoped lambda receiver type to turn
MyUnion instances into native pointers. Those pointers are only valid inside the
For the opposite conversion, to turn a pointer into a by-value variable, we call the
readValue() extension function:
Run the code
Now when you have learned how to use C declarations in your code, you are ready to try it out on a real example. Let's fix the code and see how it runs by calling the
runDebugExecutableNative Gradle task in the IDE or by using the following console command:
The final code in the
hello.kt file may look like this:
Continue exploring the C language types and their representation in Kotlin/Native in the related tutorials:
The C Interop documentation covers more advanced scenarios of the interop.