Mapping primitive data types from C – tutorial
This is the first part of the Mapping Kotlin and C tutorial series.
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 data types are visible in Kotlin/Native and vice versa and examine advanced C interop-related use cases of Kotlin/Native and multiplatform Gradle builds.
In this tutorial, you'll:
You can use the command line to generate a Kotlin library, either directly or with a script file (such as .sh
or .bat
file). However, this approach doesn't scale well for larger projects that have hundreds of files and libraries. Using a build system simplifies the process by downloading and caching the Kotlin/Native compiler binaries and libraries with transitive dependencies, as well as by running the compiler and tests. Kotlin/Native can use the Gradle build system through the Kotlin Multiplatform plugin.
The C programming language has the following data types:
Basic types:
char, int, float, double
with modifierssigned, unsigned, short, long
Structures, unions, arrays
Pointers
Function pointers
There are also more specific types:
Boolean type (from C99)
size_t
andptrdiff_t
(alsossize_t
)Fixed width integer types, such as
int32_t
oruint64_t
(from C99)
There are also the following type qualifiers in the C language: const
, volatile
, restrict
, atomic
.
Let's see which C data types are visible in Kotlin.
In this tutorial, you won't create a lib.c
source file, which is only necessary if you want to compile and run your C library. For this setup, you'll only need a .h
header file that is required for running the cinterop tool.
The cinterop tool generates a Kotlin/Native library (a .klib
file) for each set of .h
files. The generated library helps bridge calls from Kotlin/Native to C. It includes Kotlin declarations that correspond to the definitions from the .h
files.
To create a C library:
Create an empty folder for your future project.
Inside, create a
lib.h
file with the following content to see how C functions are mapped into Kotlin:#ifndef LIB2_H_INCLUDED #define LIB2_H_INCLUDED void ints(char c, short d, int e, long f); void uints(unsigned char c, unsigned short d, unsigned int e, unsigned long f); void doubles(float a, double b); #endif
The file doesn't have the
extern "C"
block, which is not needed for this example but may be necessary if you use C++ and overloaded functions. See this Stackoverflow thread for more details.Create the
lib.def
definition file with the following content:headers = lib.h
It can be helpful to include macros or other C definitions in the code generated by the cinterop tool. This way, method bodies are also compiled and fully included in the binary. With this feature, you can create a runnable example without needing a C compiler.
To do that, add implementations to the C functions from the
lib.h
file to a newinterop.def
file after the---
separator:--- void ints(char c, short d, int e, long f) { } void uints(unsigned char c, unsigned short d, unsigned int e, unsigned long f) { } void doubles(float a, double b) { }
The interop.def
file provides everything necessary to compile, run, or open the application in an IDE.
tip
See the Get started with Kotlin/Native tutorial for detailed first steps and instructions on how to create a new Kotlin/Native project and open it in IntelliJ IDEA.
To create project files:
In your project folder, create a
build.gradle(.kts)
Gradle build file with the following content:KotlinGroovyplugins { kotlin("multiplatform") version "2.1.20" } repositories { mavenCentral() } 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 binaries { executable() } } } tasks.wrapper { gradleVersion = "8.10" distributionType = Wrapper.DistributionType.BIN }
plugins { id 'org.jetbrains.kotlin.multiplatform' version '2.1.20' } repositories { mavenCentral() } 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 } binaries { executable() } } } wrapper { gradleVersion = '8.10' distributionType = 'BIN' }
The project file configures the C interop as an additional build step. Check out the Multiplatform Gradle DSL reference to learn about different ways you can configure it.
Move your
interop.def
,lib.h
, andlib.def
files to thesrc/nativeInterop/cinterop
directory.Create a
src/nativeMain/kotlin
directory. This is where you should place all the source files, following Gradle's recommendations on using conventions instead of configurations.By default, all the symbols from C are imported to the
interop
package.In
src/nativeMain/kotlin
, create ahello.kt
stub file with the following content:import interop.* import kotlinx.cinterop.ExperimentalForeignApi @OptIn(ExperimentalForeignApi::class) fun main() { println("Hello Kotlin/Native!") ints(/* fix me*/) uints(/* fix me*/) doubles(/* fix me*/) }
You'll complete the code later as you learn how C primitive type declarations look from the Kotlin side.
Let's see how C primitive types are mapped into Kotlin/Native and update the example project accordingly.
Use IntelliJ IDEA's Go to declaration command (Cmd + B/Ctrl + B) to navigate to the following generated API for C functions:
fun ints(c: kotlin.Byte, d: kotlin.Short, e: kotlin.Int, f: kotlin.Long)
fun uints(c: kotlin.UByte, d: kotlin.UShort, e: kotlin.UInt, f: kotlin.ULong)
fun doubles(a: kotlin.Float, b: kotlin.Double)
C types are mapped directly, except for the char
type, which is mapped to kotlin.Byte
as it's usually an 8-bit signed value:
C | Kotlin |
---|---|
char | kotlin.Byte |
unsigned char | kotlin.UByte |
short | kotlin.Short |
unsigned short | kotlin.UShort |
int | kotlin.Int |
unsigned int | kotlin.UInt |
long long | kotlin.Long |
unsigned long long | kotlin.ULong |
float | kotlin.Float |
double | kotlin.Double |
Now that you've seen the C definitions, you can update your Kotlin code. The final code in the hello.kt
file may look like this:
import interop.*
import kotlinx.cinterop.ExperimentalForeignApi
@OptIn(ExperimentalForeignApi::class)
fun main() {
println("Hello Kotlin/Native!")
ints(1, 2, 3, 4)
uints(5u, 6u, 7u, 8u)
doubles(9.0f, 10.0)
}
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 struct and union types are mapped between Kotlin and C:
Learn more in the Interoperability with C documentation that covers more advanced scenarios.