教程 - 映射 C 语言的结构(Struct)和联合(Union)类型
我们来看看在 Kotlin/Native 中可以访问 C 的哪些结构(Struct)和联合(Union)类型声明, 并研究 Kotlin/Native 和 跨平台 Gradle 构建的与 C 互操作相关的高级使用场景.
在本教程中, 你将学习:
映射 C 的结构(Struct)和联合(Union)类型
为了理解 Kotlin 如何映射结构(Struct)和联合(Union)类型, 我们在 C 中声明这些类型, 然后看看它们在 Kotlin 中如何表达.
在 前面的教程 中, 你已经创建了一个 C 库, 以及必要的文件. 在这个教程中, 更新 interop.def
文件中 ---
分割行之后的声明:
这个 interop.def
文件提供了所有需要的内容, 可以用来编译, 运行, 或在 IDE 中打开应用程序.
查看为 C 库生成的 Kotlin API
我们来看看 C 的结构(Struct)和联合(Union)类型如何映射到 Kotlin/Native 中, 并更新你的项目:
在
src/nativeMain/kotlin
中, 将你在 上一篇教程 中创建的hello.kt
文件, 更新为以下内容:import interop.* import kotlinx.cinterop.ExperimentalForeignApi @OptIn(ExperimentalForeignApi::class) fun main() { println("Hello Kotlin/Native!") struct_by_value(/* fix me*/) struct_by_pointer(/* fix me*/) union_by_value(/* fix me*/) union_by_pointer(/* fix me*/) }为了避免编译错误, 请在构建过程中添加 interop. 方法是, 更新你的
build.gradle(.kts)
构建文件, 内容如下:kotlin { macosArm64("native") { // 用于 Apple Silicon 的 macOS 环境 // macosX64("native") { // 用于 x86_64 平台的 macOS 环境 // linuxArm64("native") { // 用于 ARM64 平台的 Linux 环境 // linuxX64("native") { // 用于 x86_64 平台的 Linux 环境 // mingwX64("native") { // 用于 Windows 环境 val main by compilations.getting val interop by main.cinterops.creating { definitionFile.set(project.file("src/nativeInterop/cinterop/interop.def")) } binaries { executable() } } }kotlin { macosArm64("native") { // 用于 Apple Silicon 的 macOS 环境 // macosX64("native") { // 用于 x86_64 平台的 macOS 环境 // linuxArm64("native") { // 用于 ARM64 平台的 Linux 环境 // linuxX64("native") { // 用于 x86_64 平台的 Linux 环境 // mingwX64("native") { // 用于 Windows 环境 compilations.main.cinterops { interop { definitionFile = project.file('src/nativeInterop/cinterop/interop.def') } } binaries { executable() } } }通过 IntelliJ IDEA 的 Go to declaration 命令 (Cmd + B/Ctrl + B), 可以跳转到为 C 函数, struct 和 union 生成的 API:
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 中 struct 和 union 类型没有区别. cinterop 工具对 C 的 struct 和 union 声明都会生成 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 来创建和使用这些类型, 我们来看看在实践中如何使用这个 API.
创建一个 CValue<T>
CValue<T>
类型用来向 C 函数传递一个传值的参数. 使用 cValue
函数来创建一个 CValue<T>
实例. 函数要求一个 带接受者的 Lambda 函数 来初始化底层的 C 类型. 函数声明如下:
使用 cValue
, 并传递传值的参数的方法如下:
使用 CValuesRef<T> 创建结构和联合
在 Kotlin 中, CValuesRef<T>
类型用来传递 C 函数的指针类型参数. 要在 native 内存中创建 MyStruct
和 MyUnion
, 请使用 kotlinx.cinterop.NativePlacement
类型的以下扩展函数:
NativePlacement
表示 native 内存, 它有类似于 malloc
和 free
的函数. NativePlacement
有几种实现:
全局实现是
kotlinx.cinterop.nativeHeap
, 但你必须在使用完毕后调用nativeHeap.free()
函数来释放内存.另一个更加安全的选择是
memScoped()
, 它创建一个短期存在(Short-Lived)的内存范围(Scope), 所有分配的内存将会在代码段的末尾自动释放.fun <R> memScoped(block: kotlinx.cinterop.MemScope.() -> R): R
使用 memScoped()
时, 使用指针调用函数的代码大致如下:
这段代码使用来自 memScoped {}
代码段的扩展属性 ptr
, 将 MyStruct
和 MyUnion
实例转换为 native 指针.
由于内存在 memScoped {}
代码段之内管理, 因此会在代码段的末尾自动释放. 请不要在这个范围之外使用指针, 以免访问到已释放的内存. 如果你需要生存周期更长的内存分配(例如, 在 C 库中使用缓存), 请考虑使用 Arena()
或 nativeHeap
.
CValue<T> 和 CValuesRef<T> 之间的转换
有些时候, 你需要在一个函数调用中以值的方式传递一个结构, 然后对另一个函数调用需要以引用的方式传递同一个结构.
要实现这样的功能, 你需要使用 NativePlacement
, 但在此之前, 我们先来看看 CValue<T>
如何转换为指针:
这里和前面一样, 使用来自 memScoped {}
代码段的扩展属性 ptr
, 将 MyStruct
实例转换为 native 指针. 这些指针只在 memScoped {}
代码段之内有效.
要将一个指针反过来转换为一个传值的变量, 请调用 .readValue()
扩展函数:
更新 Kotlin 代码
现在你已经学习了如何在 Kotlin 代码中使用 C 的声明, 请在你的项目中试试吧. hello.kt
文件中的最终代码大致如下:
为了验证是否一切正确, 请 在你的 IDE 中 运行 runDebugExecutableNative
Gradle task, 或使用以下命令, 运行代码:
下一步
在这个教程系列的下一部分, 你将学习在 Kotlin 和 C 之间如何映射函数指针(Function Pointer):
参见
更多详情请参见 与 C 代码交互 文档, 其中包含更多高级场景.