7.3.3.2.2 从 C 映射结构体和联合体类型 – 教程
原文链接: https://kotlinlang.org/docs/mapping-struct-union-types-from-c.html
7.3.3.2.2 从 C 映射结构体和联合体类型 – 教程
注意: C 库导入处于 Beta 阶段。由 cinterop 工具从 C 库生成的所有 Kotlin 声明都应带有 @ExperimentalForeignApi 注解。随 Kotlin/Native 提供的原生平台库(例如 Foundation、UIKit 和 POSIX)只对部分 API 要求选择启用。
我们来探索哪些 C 结构体和联合体声明在 Kotlin 中可见,并考察 Kotlin/Native 与多平台 Gradle 构建中与 C 互操作相关的高级用例。
在本教程中,你将学习:
映射 C 结构体和联合体类型
为了理解 Kotlin 如何映射结构体和联合体类型,我们先用 C 声明它们,然后看看它们在 Kotlin 中如何表示。
在上一个教程中,你已经创建了包含必要文件的 C 库。对于这一步,请更新 interop.def 文件中 --- 分隔符之后的声明:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
---
typedef struct {
int a;
double b;
} MyStruct;
void struct_by_value(MyStruct s) {}
void struct_by_pointer(MyStruct* s) {}
typedef union {
int a;
MyStruct b;
float c;
} MyUnion;
void union_by_value(MyUnion u) {}
void union_by_pointer(MyUnion* u) {}
|
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
12
| import interop.*
import kotlinx.cinterop.ExperimentalForeignApi
@OptIn(ExperimentalForeignApi::class)
fun main() {
println("Hello Kotlin/Native!")
struct_by_value(/* 待补充 */)
struct_by_pointer(/* 待补充 */)
union_by_value(/* 待补充 */)
union_by_pointer(/* 待补充 */)
}
|
- 为避免编译错误,请把互操作加入构建过程。为此,请用以下内容更新你的
build.gradle(.kts) 构建文件:
Kotlin
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| kotlin {
macosArm64() // 在 Apple 芯片上的 macOS
// linuxArm64() // ARM64 平台上的 Linux
// linuxX64() // x86_64 平台上的 Linux
// mingwX64() // 在 Windows 上
targets.withType<KotlinNativeTarget>().configureEach {
val main by compilations.getting
val interop by main.cinterops.creating {
definitionFile.set(project.file("src/nativeInterop/cinterop/interop.def"))
}
binaries {
executable()
}
}
}
|
Groovy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| kotlin {
macosArm64() // Apple 芯片的 macOS
// linuxArm64() // ARM64 平台上的 Linux
// linuxX64() // x86_64 平台上的 Linux
// mingwX64() // Windows
targets.withType(KotlinNativeTarget).configureEach {
compilations.main.cinterops {
interop {
definitionFile = project.file('src/nativeInterop/cinterop/interop.def')
}
}
binaries {
executable()
}
}
}
|
- 使用 IntelliJ IDEA 的转到声明命令(Cmd + B/Ctrl + B)导航到为 C 函数、结构体和联合体生成的以下 API:
1
2
3
4
5
| fun struct_by_value(s: kotlinx.cinterop.CValue<interop.MyStruct>)
fun struct_by_pointer(s: kotlinx.cinterop.CValuesRef<interop.MyStruct>?)
fun union_by_value(u: kotlinx.cinterop.CValue<interop.MyUnion>)
fun union_by_pointer(u: kotlinx.cinterop.CValuesRef<interop.MyUnion>?)
|
从技术上讲,在 Kotlin 一侧结构体和联合体类型没有区别。cinterop 工具会为 C 的结构体和联合体声明都生成 Kotlin 类型。
生成的 API 包含 CValue<T> 和 CValuesRef<T> 的完全限定包名,反映了它们在 kotlinx.cinterop 中的位置。CValue<T> 表示按值传递的结构体参数,而 CValuesRef<T>? 用于传递指向结构体或联合体的指针。
在 Kotlin 中使用结构体和联合体类型
得益于生成的 API,在 Kotlin 中使用 C 结构体和联合体类型很直接。唯一的问题是如何创建这些类型的新实例。
我们来看一下以 MyStruct 和 MyUnion 为参数的生成函数。按值参数表示为 kotlinx.cinterop.CValue<T>,而指针类型参数使用 kotlinx.cinterop.CValuesRef<T>?。
Kotlin 提供了一套便捷的 API 来创建和操作这些类型。我们来看看在实践中如何使用它。
创建 CValue
CValue<T> 类型用于向 C 函数调用传递按值参数。使用 cValue 函数创建 CValue<T> 实例。该函数需要一个带接收者的 lambda 函数来就地初始化底层 C 类型。该函数声明如下:
1
| fun <reified T : CStructVar> cValue(initialize: T.() -> Unit): CValue<T>
|
下面演示如何使用 cValue 并传递按值参数:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| import interop.*
import kotlinx.cinterop.ExperimentalForeignApi
import kotlinx.cinterop.cValue
@OptIn(ExperimentalForeignApi::class)
fun callValue() {
val cStruct = cValue<MyStruct> {
a = 42
b = 3.14
}
struct_by_value(cStruct)
val cUnion = cValue<MyUnion> {
b.a = 5
b.b = 2.7182
}
union_by_value(cUnion)
}
|
以 CValuesRef 创建结构体和联合体
在 Kotlin 中,CValuesRef<T> 类型用于传递 C 函数的指针类型参数。要在原生内存中分配 MyStruct 和 MyUnion,请对 kotlinx.cinterop.NativePlacement 类型使用以下扩展函数:
1
| fun <reified T : kotlinx.cinterop.CVariable> alloc(): T
|
NativePlacement 表示原生内存,其函数类似于 malloc 和 free。NativePlacement 有几种实现:
- 全局实现是
kotlinx.cinterop.nativeHeap,但使用后必须调用 nativeHeap.free() 来释放内存。 - 更安全的替代方案是
memScoped(),它会创建一个短生命周期的内存作用域,其中所有分配都会在代码块结束时自动释放:
1
| fun <R> memScoped(block: kotlinx.cinterop.MemScope.() -> R): R
|
使用 memScoped(),调用带指针的函数的代码可以像这样:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| import interop.*
import kotlinx.cinterop.ExperimentalForeignApi
import kotlinx.cinterop.memScoped
import kotlinx.cinterop.alloc
import kotlinx.cinterop.ptr
@OptIn(ExperimentalForeignApi::class)
fun callRef() {
memScoped {
val cStruct = alloc<MyStruct>()
cStruct.a = 42
cStruct.b = 3.14
struct_by_pointer(cStruct.ptr)
val cUnion = alloc<MyUnion>()
cUnion.b.a = 5
cUnion.b.b = 2.7182
union_by_pointer(cUnion.ptr)
}
}
|
这里,在 memScoped {} 块内可用的 ptr 扩展属性会把 MyStruct 和 MyUnion 实例转换为原生指针。
由于内存是在 memScoped {} 块内管理的,它会在代码块结束时自动释放。请避免在该作用域之外使用这些指针,以免访问已释放的内存。如果你需要存活更久的分配(例如为了在 C 库中缓存),请考虑使用 Arena() 或 nativeHeap。
CValue 与 CValuesRef 之间的转换
有时你需要在一次函数调用中把结构体按值传入,然后在另一次调用中把同一个结构体按引用传入。
为此,你需要一个 NativePlacement,但首先我们来看看 CValue<T> 是如何转换为指针的:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| import interop.*
import kotlinx.cinterop.ExperimentalForeignApi
import kotlinx.cinterop.cValue
import kotlinx.cinterop.memScoped
@OptIn(ExperimentalForeignApi::class)
fun callMix_ref() {
val cStruct = cValue<MyStruct> {
a = 42
b = 3.14
}
memScoped {
struct_by_pointer(cStruct.ptr)
}
}
|
这里同样是 memScoped {} 中的 ptr 扩展属性把 MyStruct 实例转换为原生指针。这些指针只在 memScoped {} 块内有效。
要把指针转换回按值的变量,请调用 .readValue() 扩展函数:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| import interop.*
import kotlinx.cinterop.alloc
import kotlinx.cinterop.ExperimentalForeignApi
import kotlinx.cinterop.memScoped
import kotlinx.cinterop.readValue
@OptIn(ExperimentalForeignApi::class)
fun callMix_value() {
memScoped {
val cStruct = alloc<MyStruct>()
cStruct.a = 42
cStruct.b = 3.14
struct_by_value(cStruct.readValue())
}
}
|
更新 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
22
23
24
25
26
27
28
29
30
31
32
| import interop.*
import kotlinx.cinterop.alloc
import kotlinx.cinterop.cValue
import kotlinx.cinterop.memScoped
import kotlinx.cinterop.ptr
import kotlinx.cinterop.readValue
import kotlinx.cinterop.ExperimentalForeignApi
@OptIn(ExperimentalForeignApi::class)
fun main() {
println("Hello Kotlin/Native!")
val cUnion = cValue<MyUnion> {
b.a = 5
b.b = 2.7182
}
memScoped {
union_by_value(cUnion)
union_by_pointer(cUnion.ptr)
}
memScoped {
val cStruct = alloc<MyStruct> {
a = 42
b = 3.14
}
struct_by_value(cStruct.readValue())
struct_by_pointer(cStruct.ptr)
}
}
|
要验证一切是否按预期工作,请在你的 IDE 中运行 runDebugExecutable<YourTargetName> Gradle 任务,或在终端中使用控制台命令,例如:
1
| ./gradlew runDebugExecutableMacosArm64
|
下一步
在本系列的下一部分中,你将了解函数指针如何在 Kotlin 与 C 之间映射:
上一步
下一步
另请参阅
在与 C 互操作文档中了解更多内容,其中涵盖了更多高级场景。