Kotlin 语言参考文档 中文版 Help

教程 - 映射 C 语言的结构(Struct)和联合(Union)类型

我们来看看在 Kotlin/Native 中可以访问 C 的哪些结构(Struct)和联合(Union)类型声明, 并研究 Kotlin/Native 和 跨平台 Gradle 构建的与 C 互操作相关的高级使用场景.

在本教程中, 你将学习:

映射 C 的结构(Struct)和联合(Union)类型

为了理解 Kotlin 如何映射结构(Struct)和联合(Union)类型, 我们在 C 中声明这些类型, 然后看看它们在 Kotlin 中如何表达.

前面的教程 中, 你已经创建了一个 C 库, 以及必要的文件. 在这个教程中, 更新 interop.def 文件中 --- 分割行之后的声明:

--- 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 的结构(Struct)和联合(Union)类型如何映射到 Kotlin/Native 中, 并更新你的项目:

  1. 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*/) }
  2. 为了避免编译错误, 请在构建过程中添加 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() } } }
  3. 通过 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 的结构和联合类型是很简单的. 唯一的问题是, 如何为这些类创建这些类型的新实例.

我们来看一下生成的那些接受 MyStructMyUnion 参数的函数. 传值的参数表达为 kotlinx.cinterop.CValue<T>, 指针类型参数则使用 kotlinx.cinterop.CValuesRef<T>?.

Kotlin 提供了一个方便的 API 来创建和使用这些类型, 我们来看看在实践中如何使用这个 API.

创建一个 CValue<T>

CValue<T> 类型用来向 C 函数传递一个传值的参数. 使用 cValue 函数来创建一个 CValue<T> 实例. 函数要求一个 带接受者的 Lambda 函数 来初始化底层的 C 类型. 函数声明如下:

fun <reified T : CStructVar> cValue(initialize: T.() -> Unit): CValue<T>

使用 cValue, 并传递传值的参数的方法如下:

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<T> 创建结构和联合

在 Kotlin 中, CValuesRef<T> 类型用来传递 C 函数的指针类型参数. 要在 native 内存中创建 MyStructMyUnion, 请使用 kotlinx.cinterop.NativePlacement 类型的以下扩展函数:

fun <reified T : kotlinx.cinterop.CVariable> alloc(): T

NativePlacement 表示 native 内存, 它有类似于 mallocfree 的函数. NativePlacement 有几种实现:

  • 全局实现是 kotlinx.cinterop.nativeHeap, 但你必须在使用完毕后调用 nativeHeap.free() 函数来释放内存.

  • 另一个更加安全的选择是 memScoped(), 它创建一个短期存在(Short-Lived)的内存范围(Scope), 所有分配的内存将会在代码段的末尾自动释放.

    fun <R> memScoped(block: kotlinx.cinterop.MemScope.() -> R): R

使用 memScoped() 时, 使用指针调用函数的代码大致如下:

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, 将 MyStructMyUnion 实例转换为 native 指针.

由于内存在 memScoped {} 代码段之内管理, 因此会在代码段的末尾自动释放. 请不要在这个范围之外使用指针, 以免访问到已释放的内存. 如果你需要生存周期更长的内存分配(例如, 在 C 库中使用缓存), 请考虑使用 Arena()nativeHeap.

CValue<T> 和 CValuesRef<T> 之间的转换

有些时候, 你需要在一个函数调用中以值的方式传递一个结构, 然后对另一个函数调用需要以引用的方式传递同一个结构.

要实现这样的功能, 你需要使用 NativePlacement, 但在此之前, 我们先来看看 CValue<T> 如何转换为指针:

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 实例转换为 native 指针. 这些指针只在 memScoped {} 代码段之内有效.

要将一个指针反过来转换为一个传值的变量, 请调用 .readValue() 扩展函数:

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 文件中的最终代码大致如下:

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 中 运行 runDebugExecutableNative Gradle task, 或使用以下命令, 运行代码:

./gradlew runDebugExecutableNative

下一步

在这个教程系列的下一部分, 你将学习在 Kotlin 和 C 之间如何映射函数指针(Function Pointer):

继续下一部分

参见

更多详情请参见 与 C 代码交互 文档, 其中包含更多高级场景.

2025/08/04