改进翻译

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

作者 Eugene Petrenko,乔禹昂(翻译)
最近更新 2019-04-15
来自 C 语言的结构与联合类型及其在 Kotlin/Native 中的样子

这是本系列的第二篇教程。本系列的第一篇教程是映射来自 C 语言的原始数据类型。 系列其余教程包括映射来自 C 语言的函数指针映射来自 C 语言的字符串

在本教程中我们将学习到

我们需要在自己的机器上已经安装了 Kotlin 编译器。 这篇基本 Kotlin 应用程序教程涵盖了这一步骤。 我们假定拥有一个控制台,其中 kotlinc-nativecinteropklib 命令均可用。

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

理解在 Kotlin 与 C 之间进行映射的最好方式是尝试编写一个小型示例。我们将在 C 语言中声明一个结构体与一个联合体,并以此来观察如何将它们映射到 Kotlin 中。

Kotlin/Native 附带 cinterop 工具,该工具可以生成 C 语言与 Kotlin 之间的绑定。 它使用一个 .def 文件指定一个 C 库来导入。更多的细节将在与 C 库互操作教程中讨论。

之前的教程中我们创建了一个 lib.h 文件。这次, 在 --- 分割行之后,我们将直接将这些声明导入到 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 中打开它。 现在我们创建工程文件,并在 IntelliJ IDEA 中打开该工程,然后运行它。

探查为 C 库生成的 Kotlin API

虽然可以使用命令行或直接通过将它与脚本文件(即 sh 或 bat 文件)相结合,但我们应该注意到, 对于拥有数百个文件以及库的大型项目来说,这不能很好地扩展。 所以最好使用附带构建系统的 Kotlin/Native 编译器,它可以帮助下载与缓存 Kotlin/Native 编译器二进制文件与库传递依赖,以及运行该编译器并测试。 Kotlin/Native 可以通过 kotlin 多平台插件来使用 Gradle 构建系统。

基本 Kotlin/Native 应用程序这篇教程涵盖了使用 Gradle 创建 IDE 兼容工程的基础知识。如果你正在寻找关于第一步的更多细节以及如何开始一个新的 Kotlin/Native 项目并在 IntelliJ IDEA 中打开它的说明,则请你阅读这篇教程,我们将看到关于在 Kotlin/Native 中进行高级的 C 互操作的相关用法以及使用 multiplatform(Kotlin 多平台插件)及 Gradle 进行构建。

首先,让我们创建一个工程目录。在本教程中的所有路径都是相对于这个目录的。有时在添加任何新文件之前,都必须创建缺少的目录。

我们将使用下面的 build.gradle build.gradle.kts Gradle 构建文件并添加以下内容:

plugins {
    id 'org.jetbrains.kotlin.multiplatform' version '1.3.21'
}

repositories {
    mavenCentral()
}

kotlin {
  macosX64("native") {
    compilations.main.cinterops {
      interop 
    }
    
    binaries {
      executable()
    }
  }
}

wrapper {
  gradleVersion = "5.3.1"
  distributionType = "ALL"
}
plugins {
    id 'org.jetbrains.kotlin.multiplatform' version '1.3.21'
}

repositories {
    mavenCentral()
}

kotlin {
  linuxX64("native") {
    compilations.main.cinterops {
      interop 
    }
    
    binaries {
      executable()
    }
  }
}

wrapper {
  gradleVersion = "5.3.1"
  distributionType = "ALL"
}
plugins {
    id 'org.jetbrains.kotlin.multiplatform' version '1.3.21'
}

repositories {
    mavenCentral()
}

kotlin {
  mingwX64("native") {
    compilations.main.cinterops {
      interop 
    }
    
    binaries {
      executable()
    }
  }
}

wrapper {
  gradleVersion = "5.3.1"
  distributionType = "ALL"
}
plugins {
    kotlin("multiplatform") version "1.3.21"
}

repositories {
    mavenCentral()
}

kotlin {
  macosX64("native") {
    val main by compilations.getting
    val interop by main.cinterops.creating
    
    binaries {
      executable()
    }
  }
}

tasks.withType<Wrapper> {
  gradleVersion = "5.3.1"
  distributionType = Wrapper.DistributionType.ALL
}
plugins {
    kotlin("multiplatform") version "1.3.21"
}

repositories {
    mavenCentral()
}

kotlin {
  linuxX64("native") {
    val main by compilations.getting
    val interop by main.cinterops.creating
    
    binaries {
      executable()
    }
  }
}

tasks.withType<Wrapper> {
  gradleVersion = "5.3.1"
  distributionType = Wrapper.DistributionType.ALL
}
plugins {
    kotlin("multiplatform") version "1.3.21"
}

repositories {
    mavenCentral()
}

kotlin {
  mingwX64("native") {
    val main by compilations.getting
    val interop by main.cinterops.creating
    
    binaries {
      executable()
    }
  }
}

tasks.withType<Wrapper> {
  gradleVersion = "5.3.1"
  distributionType = Wrapper.DistributionType.ALL
}

这个已经准备好的工程源文件可以直接在这里下载: GitHub GitHub GitHub GitHub GitHub GitHub

该项目文件将 C 互操作配置为构建的附加步骤。 让我们将 interop.def 文件移动到 src/nativeInterop/cinterop 目录。 Gradle 建议使用约定而不是配置, 比如说,这个源文件被期望位于 src/nativeMain/kotlin 文件夹。 默认的,C 中的所有符号都被导入到 interop 包中, 我们也许想要将整个包导入到我们的 .kt 文件。 查看 kotlin 多平台插件文档来学习所有不同的配置方式。

我们使用下面的内容创建一个 src/nativeMain/kotlin/hello.kt 存根文件, 以用来观察我们的 C 声明是如何在 Kotlin 中可见的:

import interop.*

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

现在我们已经准备好在 IntelliJ IDEA 中打开这个工程并且看看如何修正这个示例工程。当我们做了这些之后, 我们将观察到 C 的原始类型已经被映射到了 Kotlin/Native。

Kotlin 中的原始类型

通过 IntelliJ IDEA 的 Goto Declaration编译器错误的帮助,我们会看到如下的为 C 函数、struct 以及 union 生成的 API:

fun struct_by_value(s: CValue<MyStruct>)
fun struct_by_pointer(s: CValuesRef<MyStruct>?)

fun union_by_value(u: CValue<MyUnion>)
fun union_by_pointer(u: CValuesRef<MyUnion>?)

class MyStruct constructor(rawPtr: NativePtr /* = NativePtr */) : CStructVar {
    var a: Int
    var b: Double
    companion object : CStructVar.Type
}

class MyUnion constructor(rawPtr: NativePtr /* = NativePtr */) : CStructVar {
    var a: Int
    val b: MyStruct
    var c: Float
    companion object : CStructVar.Type
}

我们看到 cinterop 为我们的 structunion 类型生成了包装类型。 为在 C 中声明的 MyStructMyUnion 类型,我们分别为其生成了 Kotlin 类 MyStructMyUnion。 该包装器继承自 CStructVar 基类并将所有的字段声明为了 Kotlin 属性。 它使用 CValue<T> 来表示一个值类型的结构体参数并使用 CValuesRef<T>? 来表示传递一个结构体或共用体的指针。

从技术上讲,在 Kotlin 看来 structunion 类型之间没有区别。我们应该注意,Kotlin 中 MyUnion 类的 ab 以及 c 属性使用了相同的位置来进行读写值的操作,就像 C 语言中的 union 一样。

更多细节与高级用例将在 C 互操作文档中介绍

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

在 Kotlin 中使用为 C 的 structunion 类型生成的包装器非常简单。由于生成了属性,使得在 Kotlin 代码中使用它们是非常自然的。迄今为止唯一的问题是,如何为这些类创建新的实例。正如我们在 MyStructMyUnion 的声明中所见,它们的构造函数需要一个 NativePtr。 当然,我们不愿意手动处理指针。作为替代,我们可以使用 Kotlin API 来为我们实例化这些对象。

我们来看一看生成的函数,它将 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 并传递值类型参数:

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 类的实例。这次我们直接在原生内存中创建它们。 我们使用

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

kotlinx.cinterop.NativePlacement 上的扩展函数来做这个。

NativePlacement 代表原生内存,类似于 mallocfree 函数。 这里有几个 NativePlacement 的实现。其中全局的那个是调用 kotlinx.cinterop.nativeHeap 并且不要忘记在使用过后调用 nativeHeap.free(..) 函数来释放内存。

另一个配置是使用

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

函数。它创建一个短生命周期的内存分配作用域, 并且所有的分配都将在 block 结束之后自动清理。

我们的代码调用带指针类型参数的函数将会是这个样子:

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 lambda 表达式的接收者类型, 将 MyStructMyUnion 实例转换为原生指针。

MyStructMyUnion 类具有指向原生内存的指针。当 memScoped 函数结束的时候, 即 block 结尾的时候,内存将释放。请小心确保指针没有在 memScoped 调用的外部使用。我们可以为指针使用 Arena()nativeHeap 这样应该有更长的可用时间,或者将它们缓存在 C 库中。

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

当然,这里有一些用例:一种是我们需要将一个结构体作为值传递给一个调用,另一种是将同一个结构体作为引用传递给另一个调用。这在 Kotlin/Native 中同样也是可行的。这里将需要一个 NativePlacement

我们看看现在首先将 CValue<T> 转换为一个指针:

fun callMix_ref() {
  val cStruct = cValue<MyStruct> {
    a = 42
    b = 3.14
  }
  
  memScoped { 
    struct_by_pointer(cStruct.ptr)
  }
}

我们使用的扩展属性 ptr 来自 memScoped lambda 表达式的接收者类型, 将 MyStructMyUnion 实例转换为原生指针。这些指针只在 memScoped 块内是有效的。

对于反向转换,即将指针转换为值类型变量, 我们可以调用 readValue() 扩展函数:

fun callMix_value() {
  memScoped {
    val cStruct = alloc<MyStruct>()
    cStruct.a = 42
    cStruct.b = 3.14

    struct_by_value(cStruct.readValue())
  }
}

运行代码

现在我们应学习了如何在我们的代码中使用 C 声明,我们已经准备好在一个真实的示例中尝试它的输出。我们来修改代码并看看如何在 IDE 中调用 runDebugExecutableNative Gradle 任务来运行它。 或者使用下面的控制台命令:

./gradlew runDebugExecutableNative
./gradlew runDebugExecutableNative
gradlew.bat runDebugExecutableNative

hello.kt 文件中的最终代码看起来会是这样:

import interop.*
import kotlinx.cinterop.alloc
import kotlinx.cinterop.cValue
import kotlinx.cinterop.memScoped
import kotlinx.cinterop.ptr
import kotlinx.cinterop.readValue

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

接下来

加入我们的行列,在几篇相关的教程中继续浏览 C 语言的类型以及它们在 Kotlin/Native 中的表示:

这篇 C 互操作文档涵盖了更多的高级互操作场景