映射来自 C 语言的结构与联合类型——教程

This is the second part of the Mapping Kotlin and C tutorial series. Before proceeding, make sure you've completed the previous step.

First step Mapping primitive data types from C
Second step Mapping struct and union types from C
Third step Mapping function pointers
Fourth step 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"}

Let's explore which C struct and union declarations are visible from Kotlin and examine advanced C interop-related use cases of Kotlin/Native and multiplatform Gradle builds.

在本教程中可以学到

映射 C 语言的结构与联合类型

To understand how Kotlin maps struct and union types, let's declare them in C and examine how they are represented in 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

Let's see how C struct and union types are mapped into Kotlin/Native and update your project:

  1. In src/nativeMain/kotlin, update your hello.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!")
    
        struct_by_value(/* fix me*/)
        struct_by_pointer(/* fix me*/)
        union_by_value(/* fix me*/)
        union_by_pointer(/* fix me*/)
    }
    
  2. To avoid compiler errors, add interoperability to the build process. For that, update your build.gradle(.kts) build file with the following content:


【Kotlin】

    kotlin {
        macosArm64("native") {    // macOS on Apple Silicon
        // macosX64("native") {   // macOS on x86_64 platforms
        // linuxArm64("native") { // Linux on ARM64 platforms 
        // linuxX64("native") {   // Linux on x86_64 platforms
        // mingwX64("native") {   // on Windows
            val main by compilations.getting
            val interop by main.cinterops.creating {
                definitionFile.set(project.file("src/nativeInterop/cinterop/interop.def"))
            }

            binaries {
                executable()
            }
        }
    }

【Groovy】

    kotlin {
        macosArm64("native") {    // Apple Silicon macOS
        // macosX64("native") {   // macOS on x86_64 platforms
        // linuxArm64("native") { // Linux on ARM64 platforms
        // linuxX64("native") {   // Linux on x86_64 platforms
        // mingwX64("native") {   // Windows
            compilations.main.cinterops {
                interop {   
                    definitionFile = project.file('src/nativeInterop/cinterop/interop.def')
                }
            }

            binaries {
                executable()
            }
        }
    }

  1. Use IntelliJ IDEA's Go to declaration command (Cmd + B/Ctrl + B) to navigate to the following generated API for C functions, struct, and union:

    fun struct_by_value(s: kotlinx.cinterop.CValue)
    fun struct_by_pointer(s: kotlinx.cinterop.CValuesRef?)
    
    fun union_by_value(u: kotlinx.cinterop.CValue)
    fun union_by_pointer(u: kotlinx.cinterop.CValuesRef?)
    

从技术上讲,在 Kotlin 看来结构与联合类型之间没有区别。 The cinterop tool generates Kotlin types for both struct and union C declarations.

The generated API includes fully qualified package names for CValue and CValuesRef, reflecting their location in kotlinx.cinterop. CValue 表示一个值类型的结构体参数,而 CValuesRef? 用于传递一个结构体或联合体的指针。

在 Kotlin 中使用结构与联合类型

Using C struct and union types from Kotlin is straightforward thanks to the generated API. The only question is how to create new instances of these types.

我们来看一看生成的函数,它将 MyStructMyUnion 作为参数。值类型参数表示为 kotlinx.cinterop.CValue,而指针类型参数使用 kotlinx.cinterop.CValuesRef?

Kotlin 提供了一种便利的 API 来创建及处理这些类型。我们来探索如何在实践中使用它。

创建一个 CValue<T>

CValue 类型用来传递一个值类型的参数到 C 函数调用。使用 cValue 函数来创建 CValue 实例。该函数需要一个带接收者的 lambda 函数字面值来就地初始化底层 C 类型。该函数的声明如下所示:

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

以下是如何使用 cValue 并传递值类型参数:

import interop.*
import kotlinx.cinterop.ExperimentalForeignApi
import kotlinx.cinterop.cValue

@OptIn(ExperimentalForeignApi::class)
fun callValue() {

    val cStruct = cValue {
        a = 42
        b = 3.14
    }
    struct_by_value(cStruct)

    val cUnion = cValue {
        b.a = 5
        b.b = 2.7182
    }

    union_by_value(cUnion)
}

使用 CValuesRef<T> 创建结构体与联合体

CValuesRef 类型用于在 Kotlin 中将指针类型的参数传递给 C 函数。如需在原生内存中分配 MyStructMyUnion,请使用 kotlinx.cinterop.NativePlacement 类型的以下扩展函数:

fun  alloc(): T

NativePlacement 代表原生内存,类似于 mallocfree 函数。 这里有几个 NativePlacement 的实现:

  • 全局的实现是 kotlinx.cinterop.nativeHeap,但你必须在使用过后调用 nativeHeap.free() 来释放内存。
  • A safer alternative is memScoped(), which creates a short-lived memory scope where all allocations are automatically freed at the end of the block:

    fun  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()
        cStruct.a = 42
        cStruct.b = 3.14

        struct_by_pointer(cStruct.ptr)

        val cUnion = alloc()
        cUnion.b.a = 5
        cUnion.b.b = 2.7182

        union_by_pointer(cUnion.ptr)
    }
}

这里的 ptr 扩展属性可在 memScoped {} 代码块中使用,它将 MyStructMyUnion 实例转换为原生指针。

Since memory is managed inside the memScoped {} block, it's automatically freed at the end of the block. Avoid using pointers outside of this scope to prevent accessing deallocated memory. If you need longer-lived allocations (for example, for caching in a C library), consider using Arena() or nativeHeap.

在 CValue<T> 与 CValuesRef<T> 之间转换

有时候,需要在一个函数调用中将结构体作为值传递,然后在另一个调用中将同一个结构体作为引用传递。

To do this, you'll need a NativePlacement, 但首先,我们来看看如何将 CValue 转换为一个指针:

import interop.*
import kotlinx.cinterop.ExperimentalForeignApi
import kotlinx.cinterop.cValue
import kotlinx.cinterop.memScoped

@OptIn(ExperimentalForeignApi::class)
fun callMix_ref() {
    val cStruct = cValue {
        a = 42
        b = 3.14
    }

    memScoped {
        struct_by_pointer(cStruct.ptr)
    }
}

这里再次说明,来自 memScoped {}ptr 扩展属性将 MyStruct 实例转换为原生指针。 这些指针只在 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()
        cStruct.a = 42
        cStruct.b = 3.14

        struct_by_value(cStruct.readValue())
    }
}

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.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 {
        b.a = 5
        b.b = 2.7182
    }

    memScoped {
        union_by_value(cUnion)
        union_by_pointer(cUnion.ptr)
    }

    memScoped {
        val cStruct = alloc {
            a = 42
            b = 3.14
        }

        struct_by_value(cStruct.readValue())
        struct_by_pointer(cStruct.ptr)
    }
}

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

接下来

In the next part of the series, you'll learn how function pointers are mapped between Kotlin and C:

Proceed to the next part

See also

Learn more in the Interoperability with C documentation that covers more advanced scenarios.