映射来自 C 语言的字符串——教程

This is the final part of the Mapping Kotlin and C tutorial series. Before proceeding, make sure you've completed the previous steps.

First step Mapping primitive data types from C
Second step Mapping struct and union types from C
Third step Mapping function pointers
Fourth step Mapping strings from C

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.

{style="warning"}

In the final part of the series, let's see how to deal with C strings in Kotlin/Native.

在本教程中会学到如何:

使用 C 语言字符串

C 语言没有专门的字符串类型。方法签名或文档可以帮助你识别给定的 char * 在特定上下文中是否表示 C 字符串。

C 语言中的字符串以空值终止,因此会在字节序列末尾添加零字符 \0 来标记字符串的结束。通常,使用 UTF-8 编码字符串。 UTF-8 编码使用可变宽度字符,并且向后兼容 ASCII。 Kotlin/Native 默认使用 UTF-8 字符编码。

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:

  1. 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);
    
    #endif
    

    此示例展示了在 C 语言中传递或接收字符串的常见方式。请谨慎处理 return_string() 函数的返回值。确保使用正确的 free() 函数来释放返回的 char*

  2. 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;
    }
    

interop.def 文件提供了在 IDE 中编译、运行或打开应用程序所需的一切。

探查为 C 库生成的 Kotlin API

Let's see how C string declarations are mapped into Kotlin/Native:

  1. In src/nativeMain/kotlin, update your hello.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*/)
    }
    
  2. 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 /* from: kotlinx.cinterop.ByteVar */>?)
    fun return_string(): kotlinx.cinterop.CPointer /* from: kotlinx.cinterop.ByteVar */>?
    fun copy_string(str: kotlinx.cinterop.CValuesRef /* from: kotlinx.cinterop.ByteVar */>?, size: kotlin.Int): kotlin.Int
    

这些声明看起来很清晰。所有的 char * 指针在参数处都被转换为 str: CValuesRef? 而返回值类型则被转换为 CPointer?。Kotlin 将 char 类型转换为 kotlin.Byte 类型, 因为它通常是 8 位有符号值。

在生成的 Kotlin 声明中,str 会定义为 CValuesRef>?。 由于该类型是可空的,可以将 null 作为参数值传入。

将 Kotlin 字符串传递给 C

我们来尝试在 Kotlin 中使用这些 API。首先调用 pass_string() 函数:

import interop.*
import kotlinx.cinterop.ExperimentalForeignApi
import kotlinx.cinterop.cstr

@OptIn(ExperimentalForeignApi::class)
fun passStringToC() {
    val str = "This is a Kotlin string"
    pass_string(str.cstr)
}

将 Kotlin 字符串传递到 C 语言也很直观,得益于 String.cstr 扩展属性。 对于涉及 UTF-16 字符的情况,也有 String.wcstr 属性。

在 Kotlin 中读取 C 字符串

现在从 return_string() 函数获取一个返回的 char * 并将其转换为 Kotlin 字符串:

import interop.*
import kotlinx.cinterop.ExperimentalForeignApi
import kotlinx.cinterop.toKString

@OptIn(ExperimentalForeignApi::class)
fun passStringToC() {
    val stringFromC = return_string()?.toKString()

    println("Returned from C: $stringFromC")
}

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:

fun CPointer>.toKString(): String // Standard function for UTF-8 strings
fun CPointer>.toKStringFromUtf8(): String // Explicitly converts UTF-8 strings
fun CPointer>.toKStringFromUtf16(): String // Converts UTF-16 encoded strings
fun CPointer>.toKStringFromUtf32(): String // Converts UTF-32 encoded strings

在 Kotlin 中接收 C 字符串字节

这次,使用 copy_string() C 函数将 C 字符串写入给定的缓冲区。它接受两个参数: 指向应写入字符串的内存位置的指针以及允许的缓冲区大小。

该函数还应返回一些内容以指示它是成功还是失败。我们假设 0 表示成功, 并且提供的缓冲区足够大:

import interop.*
import kotlinx.cinterop.ExperimentalForeignApi
import kotlinx.cinterop.addressOf
import kotlinx.cinterop.usePinned

@OptIn(ExperimentalForeignApi::class)
fun sendString() {
    val buf = ByteArray(255)
    buf.usePinned { pinned ->
        if (copy_string(pinned.addressOf(0), buf.size - 1) != 0) {
            throw Error("Failed to read string from C")
        }
    }

    val copiedStringFromC = buf.decodeToString()
    println("Message from C: $copiedStringFromC")
}

这里,首先将一个原生指针传递给 C 函数。.usePinned 扩展函数临时固定字节数组的原生内存地址。该 C 函数填充了带数据的字节数组。另一个扩展函数 ByteArray.decodeToString() 将字节数组转换为一个 Kotlin 字符串,假设它是 UTF-8 编码的。

Update Kotlin code

Now that you've learned how to use C declarations in Kotlin code, try to use them in your project. hello.kt 文件中的代码最终看起来会是这样的:

import interop.*
import kotlinx.cinterop.*

@OptIn(ExperimentalForeignApi::class)
fun main() {
    println("Hello Kotlin/Native!")

    val str = "This is a Kotlin string"
    pass_string(str.cstr)

    val useMe = return_string()?.toKString() ?: error("null pointer returned")
    println(useMe)

    val copyFromC = ByteArray(255).usePinned { pinned ->
        val useMe2 = copy_string(pinned.addressOf(0), pinned.get().size - 1)
        if (useMe2 != 0) throw Error("Failed to read a string from C")
        pinned.get().decodeToString()
    }

    println(copyFromC)
}

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

接下来

Learn more in the Interoperability with C documentation that covers more advanced scenarios.