映射来自 C 语言的字符串——教程
This is the final part of the Mapping Kotlin and C tutorial series. Before proceeding, make sure you've completed the previous steps.
Mapping primitive data types from C
Mapping struct and union types from C
Mapping function pointers
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:
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*
。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:
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
/* 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.