Mapping strings from C – tutorial
In the final part of the series, let's see how to deal with C strings in Kotlin/Native.
In this tutorial, you'll learn how to:
Working with C strings
C doesn't have a dedicated string type. Method signatures or documentation can help you identify whether a given char *
represents a C string in a particular context.
Strings in the C language are null-terminated, so a trailing zero character \0
is added to the end of a byte sequence to mark the end of a string. Usually, UTF-8 encoded strings are used. The UTF-8 encoding uses variable-width characters and is backward-compatible with ASCII. Kotlin/Native uses UTF-8 character encoding by default.
To understand how strings are mapped between Kotlin and C, first create the library headers. In the first part of the series, you've already created a C library with the necessary files. For this step:
Update your
lib.h
file with the following function declarations that work with C strings:#ifndef LIB2_H_INCLUDED #define LIB2_H_INCLUDED void pass_string(char* str); char* return_string(); int copy_string(char* str, int size); #endifThis example shows common ways to pass or receive a string in the C language. Handle the return value of the
return_string()
function carefully. Ensure you use the correctfree()
function to release the returnedchar*
.Update the declarations in the
interop.def
file after the---
separator:--- void pass_string(char* str) { } char* return_string() { return "C string"; } int copy_string(char* str, int size) { *str++ = 'C'; *str++ = ' '; *str++ = 'K'; *str++ = '/'; *str++ = 'N'; *str++ = 0; return 0; }
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 string declarations are mapped into Kotlin/Native:
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!") pass_string(/*fix me*/) val useMe = return_string() val useMe2 = copy_string(/*fix me*/) }Use IntelliJ IDEA's Go to declaration command (Cmd + B/Ctrl + B) to navigate to the following generated API for C functions:
fun pass_string(str: kotlinx.cinterop.CValuesRef<kotlinx.cinterop.ByteVarOf<kotlin.Byte> /* from: kotlinx.cinterop.ByteVar */>?) fun return_string(): kotlinx.cinterop.CPointer<kotlinx.cinterop.ByteVarOf<kotlin.Byte> /* from: kotlinx.cinterop.ByteVar */>? fun copy_string(str: kotlinx.cinterop.CValuesRef<kotlinx.cinterop.ByteVarOf<kotlin.Byte> /* from: kotlinx.cinterop.ByteVar */>?, size: kotlin.Int): kotlin.Int
These declarations are straightforward. In Kotlin, C char *
pointers are mapped into str: CValuesRef<ByteVarOf>?
for parameters and into CPointer<ByteVarOf>?
for return types. Kotlin represents the char
type as kotlin.Byte
, as it's usually an 8-bit signed value.
In the generated Kotlin declarations, str
is defined as CValuesRef<ByteVarOf<Byte>>?
. Since this type is nullable, you can pass null
as the argument value.
Pass Kotlin strings to C
Let's try to use the API from Kotlin. Call the pass_string()
function first:
Passing a Kotlin string to C is straightforward, thanks to the String.cstr
extension property. There is also the String.wcstr
property for cases that involve UTF-16 characters.
Read C strings in Kotlin
Now take a returned char *
from the return_string()
function and turn it into a Kotlin string:
Here, the .toKString()
extension function converts a C string returned from the return_string()
function into a Kotlin string.
Kotlin provides several extension functions for converting C char *
strings into Kotlin strings, depending on the encoding:
Receive C string bytes from Kotlin
This time, use the copy_string()
C function to write a C string to a given buffer. It takes two arguments: a pointer to the memory location where the string should be written and the allowed buffer size.
The function should also return something to indicate if it has succeeded or failed. Let's assume 0
means it succeeded, and the supplied buffer was big enough:
Here, a native pointer is passed to the C function first. The .usePinned
extension function temporarily pins the native memory address of the byte array. The C function fills in the byte array with data. Another extension function, ByteArray.decodeToString()
, turns the byte array into a Kotlin string, assuming UTF-8 encoding.
Update Kotlin code
Now that you've learned how to use C declarations in Kotlin code, try to use them in your project. The code in the final 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:
What's next
Learn more in the Interoperability with C documentation that covers more advanced scenarios.