改进翻译

映射来自 C 语言的函数指针

作者 Eugene Petrenko,乔禹昂(翻译)
最近更新 2019-04-15
来自 C 的函数指针以及它们在 Kotlin/Native 中的样子

这是本系列的第三篇教程。本系列的第一篇教程是映射来自 C 语言的原始数据类型。系列其余教程包括映射来自 C 语言的结构与联合类型映射来自 C 语言的字符串

在本篇教程中我们将学习如何:

映射 C 中的函数指针类型

理解在 Kotlin 与 C 之间进行映射的最好方式是尝试编写一个小型示例。我们声明一个函数,它接收一个函数指针作为参数,而另一个函数返回一个函数指针。

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

最快速的尝试 C API 映射的方法是将所有的 C 声明写到 interop.def 文件,而不用创建任何 .h.c 文件。在 .def 文件中, 所有的 C 声明都在特殊的 --- 分割行之后。


---

int myFun(int i) {
  return i+1;
}

typedef int  (*MyFun)(int);

void accept_fun(MyFun f) {
  f(42);
}

MyFun supply_fun() {
  return myFun;
}

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.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.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.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!")
  
  accept_fun(/*fix me */)
  val useMe = supply_fun()
}

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

Kotlin 中的 C 函数指针

通过 IntelliJ IDEA 的 Goto Declaration编译器错误的帮助,我们可以看到如下为 C 函数生成的声明:

fun accept_fun(f: MyFun? /* = CPointer<CFunction<(Int) -> Int>>? */)
fun supply_fun(): MyFun? /* = CPointer<CFunction<(Int) -> Int>>? */

fun myFun(i: kotlin.Int): kotlin.Int

typealias MyFun = kotlinx.cinterop.CPointer<kotlinx.cinterop.CFunction<(kotlin.Int) -> kotlin.Int>>

typealias MyFunVar = kotlinx.cinterop.CPointerVarOf<lib.MyFun>

我们看到 C 中的函数类型定义已经被转换到了 Kotlin typealias。它使用 CPointer<..> 类型表示指针参数,使用 CFunction<(Int)->Int> 表示函数签名。 这里有一个 invoke 操作符扩展函数,它可以用于所有的 CPointer<CFunction<..> 类型,因此它可以在任何一个我们可以调用其它 Kotlin 函数的地方调用。

将 Kotlin 函数作为 C 函数指针传递

是时候尝试在我们的 Kotlin 程序中使用 C 函数了。我们调用 accept_fun 函数并传递 C 函数指针到一个 Kotlin lambda:

fun myFun() {
  accept_fun(staticCFunction<Int, Int> { it + 1 })
}

我们使用 Kotlin/Native 中的 staticCFunction{..} 辅助函数将一个 Kotlin lambda 函数包装为 C 函数指针。 它只能是非绑定的以及没有发生变量捕捉的 lambda functions。举例来说,它不能使用函数中的局部变量。我们只能使用全局可见的声明。抛出来自 staticCFunction{..} 的异常将导致非确定性的副作用。确保我们不会从中抛出任何意想不到的异常是非常重要的。

在 Kotlin 中使用 C 函数指针

接下来是调用 supply_fun() 获得一个 C 函数指针,并调用它:

fun myFun2() {
  val functionFromC = supply_fun() ?: error("No function is returned")
  
  functionFromC(42)
}

Kotlin 将函数指针返回类型转换到一个可空的 CPointer<CFunction<..> 对象。这里首先需要显式检查 null 值。我们为此使用 elvis(猫王)操作符cinterop 工具帮助我们将一个 C 函数指针转换为一个 Kotlin 中可以简单调用的对象。这就是我们在最后一行所做的事。

修改代码

我们已经看到了所有的声明,所以是时候修改并运行代码了。 我们在 IDE 中运行 runDebugExecutableNative Gradle 任务或使用下面的命令来运行代码:

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

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

import interop.*
import kotlinx.cinterop.*

fun main() {
  println("Hello Kotlin/Native!")
 
  val cFunctionPointer = staticCFunction<Int, Int> { it + 1 }
  accept_fun(cFunctionPointer)

  val funFromC = supply_fun() ?: error("No function is returned")
  funFromC(42)
}

接下来

我们将继续在下面几篇教程中继续探索更多的 C 语言类型及其在 Kotlin/Native 中的表示:

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