映射来自 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.

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<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 看来结构与联合类型之间没有区别。 The cinterop tool generates Kotlin types for both struct and union C declarations.

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

在 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<T>,而指针类型参数使用 kotlinx.cinterop.CValuesRef<T>?

Kotlin 提供了一种便利的 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> 创建结构体与联合体

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

fun <reified T : kotlinx.cinterop.CVariable> 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 <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)
    }
}

这里的 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<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 实例转换为原生指针。 这些指针只在 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())
    }
}

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<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)
    }
}

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.