改进翻译

映射来自 C 语言的字符串

作者 Eugene Petrenko,乔禹昂(翻译)
最近更新 2019-04-15
来自 C 语言的字符串及其在 Kotlin/Native 中的样子

这是本系列的最后一篇教程。本系列的第一篇教程是映射来自 C 语言的原始数据类型。 系列其余教程包括映射来自 C 语言的结构与联合类型映射来自 C 语言的函数指针

在本教程中我们将看到如何在 Kotlin/Native 中处理 C 语言的字符串。 我们将学习到如何:

使用 C 语言字符串

在 C 语言中,没有专门用于字符串的类型。开发者需要从方法签名或文档中才能得知给定的 char * 在上下文中是否表示 C 字符串。 C 语言中的字符串以空值终止,即在字节序列尾部添加零字符 \0 来标记字符串终止。 通常,使用 UTF-8 编码字符串。UTF-8 编码使用可变长度的字符,并且它向后兼容 ASCII。 Kotlin/Native 默认使用 UTF-8 字符编码。

理解在 C 语言与 Kotlin 之间进行映射的最好方式是尝试编写一个小型示例。我们将为此创建一个小型库的头文件。首先,我们需要为以下处理 C 字符串的函数声明创建一个 lib.h 文件。

#ifndef LIB2_H_INCLUDED
#define LIB2_H_INCLUDED

void pass_string(char* str);
char* return_string();
int copy_string(char* str, int size);

#endif

在示例中,我们使用了大多数情况下受欢迎的方式来传递或接收 C 语言中的字符串。我们应该小心地返回 return_string。通常,最好确保我们使用正确的函数来处理被正确调用 free(..) 函数返回的 char*

Kotlin/Native 附带 cinterop 工具;该工具可以生成 C 语言与 Kotlin 之间的绑定。 它使用一个 .def 文件指定一个 C 库来导入。更多的细节将在与 C 库互操作教程中讨论。 最快速的尝试 C API 映射的方法是将所有的 C 声明写到 interop.def 文件,而不用创建任何 .h.c 文件。在 .def 文件中, 所有的 C 声明都在特殊的 --- 分割行之后。

headers = lib.h
---

void pass_string(char* str) {
}

char* return_string() {
  return "C stirng";
}

int copy_string(char* str, int size) {
  *str++ = 'C';
  *str++ = ' ';
  *str++ = 'K';
  *str++ = '/';
  *str++ = 'N';
  *str++ = 0;
  return 0;
}

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!")
  
  pass_string(/*fix me*/)
  val useMe = return_string()
  val useMe2 = copy_string(/*fix me*/)
}

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

Kotlin 中的原始类型

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

fun pass_string(str: CValuesRef<ByteVar /* = ByteVarOf<Byte> */>?)
fun return_string(): CPointer<ByteVar /* = ByteVarOf<Byte> */>?
fun copy_string(str: CValuesRef<ByteVar /* = ByteVarOf<Byte> */>?, size: Int): Int

这些声明看起来很清晰。所有的 char * 指针在参数处都被转换为 str: CValuesRef<ByteVar>? 而返回值类型则被转换为 CPointer<ByteVar>?。Kotlin 将 char 类型转换为 kotlin.Byte 类型, 因为它通常是 8 位有符号值。

在生成的 Kotlin 声明中,我们看到 strCValuesRef<ByteVar/>? 表示。该类型是可空的,我们可以简单地将 Kotlin null 作为参数值。

将 Kotlin 字符串传递给 C

我们来尝试在 Kotlin 中使用这些 API。首先我们调用 pass_string

fun passStringToC() {
  val str = "this is a Kotlin String"
  pass_string(str.cstr)
}

将 Kotlin 字符串传递到 C 非常简单,幸亏事实上在 Kotlin 中我们拥有 String.cstr 扩展属性来应对这种情况。在我们需要 UTF-16 字符编码的地方, 我们也有 String.wcstr 来应对这种案例。

在 Kotlin 中读取 C 字符串

这次我们将从 return_string 函数获取一个返回的 char * 并将其转换为一个 Kotlin 字符串。为此我们在 Kotlin 中做了如下这些事:

fun passStringToC() {
  val stringFromC = return_string()?.toKString()
  
  println("Returned from C: $stringFromC")
}

在上面的示例中我们使用了 toKString() 扩展函数。请不要与 toString() 函数混淆。toKString() 在 Kotlin 中拥有两个版本的重载扩展函数:

fun CPointer<ByteVar>.toKString(): String
fun CPointer<ShortVar>.toKString(): String

第一个重载扩展函数将得到一个 char * 作为 UTF-8 字符串并转换到 String。 第二个重载函数做了相同的事,但是它针对 UTF-16 字符串。

在 Kotlin 中接收 C 字符串字节

这次我们将要求 C 函数将 C 字符串写入给定的缓冲区。这个函数被称为 copy_string。它需要一个指针来定位写入字符的位置并分配缓冲区的大小。该函数返回一些内容以指示它是成功还是失败。 我们假设 0 表示成功,并且提供的缓冲区足够大:

fun sendString() {
  val buf = ByteArray(255)
  buf.usePinned { pinned ->
    if (copy_string(pinned.addressOf(0), buf.size - 1) != 0) {
      throw Error("Failed to read string from C")
    }
  }

  val copiedStringFromC = buf.toKString()
  println("Message from C: $copiedStringFromC")
}

首先,我们需要拥有一个原生的指针来传递这个 C 函数。我们使用 usePinned 扩展函数来临时固定字节数组的原生内存地址。该 C 函数填充了带数据的字节数组。我们使用另一个扩展函数 ByteArray.toKString() 将字节数组转换为一个 Kotlin String,假设它是 UTF-8 编码的。

修改代码

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

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

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

import interop.*
import kotlinx.cinterop.*

fun main() {
  println("Hello Kotlin/Native!")

  val str = "this is a Kotlin String"
  pass_string(str.cstr)

  val useMe = return_string()?.toKString() ?: error("null pointer returned")
  println(useMe)

  val copyFromC = ByteArray(255).usePinned { pinned ->

    val useMe2 = copy_string(pinned.addressOf(0), pinned.get().size - 1)
    if (useMe2 != 0) throw Error("Failed to read string from C")
    pinned.get().toKString()
  }

  println(copyFromC)
}

接下来

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

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