7.3.3.2.4 从 C 映射字符串 – 教程
原文链接: https://kotlinlang.org/docs/mapping-strings-from-c.html
7.3.3.2.4 从 C 映射字符串 – 教程
注意: C 库导入处于 Beta 阶段。由 cinterop 工具从 C 库生成的所有 Kotlin 声明都应带有 @ExperimentalForeignApi 注解。随 Kotlin/Native 提供的原生平台库(例如 Foundation、UIKit 和 POSIX)只对部分 API 要求选择启用。
在本系列的最后一部分,我们来看看如何在 Kotlin/Native 中处理 C 字符串。
在本教程中,你将学习如何:
处理 C 字符串
C 没有专门的字符串类型。方法签名或文档可以帮助你判断某个 char * 在特定上下文中是否表示 C 字符串。
C 语言中的字符串以 null 结尾,因此在字节序列末尾会添加一个零字符 \0 来标记字符串的结束。通常使用 UTF-8 编码的字符串。UTF-8 编码使用变宽字符,并与 ASCII 向后兼容。Kotlin/Native 默认使用 UTF-8 字符编码。
要理解字符串如何在 Kotlin 与 C 之间映射,请先创建库头文件。在本系列的第一部分中,你已经创建了包含必要文件的 C 库。对于这一步:
- 用以下处理 C 字符串的函数声明更新你的
lib.h 文件:
1
2
3
4
5
6
7
8
| #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*。
- 更新
interop.def 文件中 --- 分隔符之后的声明:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| ---
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
我们来看看 C 字符串声明如何映射到 Kotlin/Native:
- 在
src/nativeMain/kotlin 中,把上一个教程中的 hello.kt 文件更新为以下内容:
1
2
3
4
5
6
7
8
9
10
11
| import interop.*
import kotlinx.cinterop.ExperimentalForeignApi
@OptIn(ExperimentalForeignApi::class)
fun main() {
println("Hello Kotlin/Native!")
pass_string(/* 待补充 */)
val useMe = return_string()
val useMe2 = copy_string(/* 待补充 */)
}
|
- 使用 IntelliJ IDEA 的转到声明命令(Cmd + B/Ctrl + B)导航到为 C 函数生成的以下 API:
1
2
3
| fun pass_string(str: kotlinx.cinterop.CValuesRef<kotlinx.cinterop.ByteVarOf<kotlin.Byte> /* 来自:kotlinx.cinterop.ByteVar */>?)
fun return_string(): kotlinx.cinterop.CPointer<kotlinx.cinterop.ByteVarOf<kotlin.Byte> /* 来自:kotlinx.cinterop.ByteVar */>?
fun copy_string(str: kotlinx.cinterop.CValuesRef<kotlinx.cinterop.ByteVarOf<kotlin.Byte> /* 来自:kotlinx.cinterop.ByteVar */>?, size: kotlin.Int): kotlin.Int
|
这些声明很直观。在 Kotlin 中,C 的 char * 指针在参数位置映射为 str: CValuesRef<ByteVarOf>?,在返回类型位置映射为 CPointer<ByteVarOf>?。Kotlin 把 char 类型表示为 kotlin.Byte,因为它通常是 8 位有符号值。
在生成的 Kotlin 声明中,str 被定义为 CValuesRef<ByteVarOf<Byte>>?。由于该类型是可空的,你可以把 null 作为实参值传入。
把 Kotlin 字符串传给 C
我们试着从 Kotlin 使用这些 API。先调用 pass_string() 函数:
1
2
3
4
5
6
7
8
9
| 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)
}
|
得益于 String.cstr 扩展属性,把 Kotlin 字符串传给 C 非常直接。对于涉及 UTF-16 字符的情况,还有 String.wcstr 属性。
在 Kotlin 中读取 C 字符串
现在取回 return_string() 函数返回的 char *,并把它转换为 Kotlin 字符串:
1
2
3
4
5
6
7
8
9
10
| 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")
}
|
这里,.toKString() 扩展函数把 return_string() 函数返回的 C 字符串转换为 Kotlin 字符串。
Kotlin 提供了若干扩展函数,用于根据编码把 C 的 char * 字符串转换为 Kotlin 字符串:
1
2
3
4
| fun CPointer<ByteVarOf<Byte>>.toKString(): String // UTF-8 字符串的标准函数
fun CPointer<ByteVarOf<Byte>>.toKStringFromUtf8(): String // 显式转换 UTF-8 字符串
fun CPointer<ShortVarOf<Short>>.toKStringFromUtf16(): String // 转换 UTF-16 编码的字符串
fun CPointer<IntVarOf<Int>>.toKStringFromUtf32(): String // 转换 UTF-32 编码的字符串
|
从 Kotlin 接收 C 字符串字节
这次使用 copy_string() C 函数把 C 字符串写入给定的缓冲区。它接受两个参数:指向应写入字符串的内存位置的指针,以及允许的缓冲区大小。
该函数还应返回某些内容以表明它是成功还是失败。假设 0 表示成功,且所提供的缓冲区足够大:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| 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 编码。
更新 Kotlin 代码
现在你已经学会了如何在 Kotlin 代码中使用 C 声明,试着在你的项目中使用它们。最终的 hello.kt 文件中的代码可能如下所示:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| 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)
}
|
要验证一切是否按预期工作,请在你的 IDE 中运行 runDebugExecutable<YourTargetName> Gradle 任务,或在终端中使用控制台命令,例如:
1
| ./gradlew runDebugExecutableMacosArm64
|
上一步
接下来做什么
在与 C 互操作文档中了解更多内容,其中涵盖了更多高级场景。