编辑本页

在应用中使用 C 语言互操作与 libcurl

作者 Hadi Hariri,乔禹昂(翻译)
最近更新 2019-04-15
在 Kotlin/Native 中使用 C 库

当编写一个原生应用程序时,我们时常需要访问某些没有被包含在 Kotlin 标准库中的函数,例如发起 HTTP 请求,读写磁盘等等。

Kotlin/Native 给我们提供了操作 C 语言标准库的能力,这样就开放了存在于整个生态系统中几乎所有我们需要的功能。事实上,Kotlin/Native 已经预装了一套预制的多平台库来提供一些标准库不包含的通用功能。

然而在本教程中,我们将看到如何使用一些诸如 libcurl 这样的具体的库。我们将学到

生成绑定

调用 C 函数的互操作的理想方案是就好像我们调用 Kotlin 函数一样,也就是说,遵循相同的签名和约定。这也恰恰是 cinterop 工具为我们提供的。它会为 C 库生成相应的 Kotlin 绑定,然后允许我们像使用 Kotlin 代码一样使用该库。

为了生成这些绑定,我们需要创建一个库定义 .def 文件,其中包含一些我们需要生成的头信息。在我们的案例中,我们想使用著名 libcurl 库来发起一些 HTTP 调用,所以我们将创建一个名为 libcurl.def 的文件,其中包含以下内容

headers = curl/curl.h
headerFilter = curl/*

compilerOpts.linux = -I/usr/include -I/usr/include/x86_64-linux-gnu
linkerOpts.osx = -L/opt/local/lib -L/usr/local/opt/curl/lib -lcurl
linkerOpts.linux = -L/usr/lib/x86_64-linux-gnu -lcurl

这个文件中正在进行一些操作,让我们逐个浏览它们。第一个条目是 headers,它是我们想要生成的头文件列表的 Kotlin 存根。我们可以在这个条目中添加多个文件,用新行上的 \ 分隔每个文件。在我们的例子中,我们只想要curl.h。我们引用的文件 需要相对于定义文件所在的文件夹,或者在系统路径上可用(在我们的例子中它将是 /usr/include/curl)。

第二行是 headerFilter。这用于表示我们想要导入的内容。在 C 中,当一个文件使用 #include 指示引用另一个文件的时候, 所有的头文件都将被导入。有时这也许是不必要的,这时我们可以使用这个方法:使用全局参数,来进行微调。 注意,headerFilter 是一个可选参数,主要仅在我们使用的库作为系统库安装时使用,我们不想获取外部依赖项(比如系统 stdint.h header)进入我们的互操作库。这也许对优化库的大小以及修正系统与 Kotlin/Native 提供的编译环境之间的潜在冲突是非常重要的。

下一行是有关连接与编译配置项的,它可以根据不同的目标平台进行变化。在我们的案例中,我们将它定义为 macOS(使用 .osx 后缀)以及 Linux(使用 .linux 后缀)。 没有后缀的参数也是可能的(例如 linkerOpts =),这将应用于所有平台。

惯例是每个库都有自己的定义文件,经常被命名为与库中的相同。关于所有 cinterop 的配置的更多信息,请查看互操作文档

一旦我们准备好定义文件,我们就可以创建工程文件并在 IDE 中打开这个工程。

虽然可以使用命令行或直接通过将它与脚本文件(即 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 多平台插件文档来学习所有不同的配置方式。

在 Windows 上 curl

你应该在 Windows 上使用 curl 库二进制文件来使示例工作。 你可以在 Windows 上,从源代码构建 curl(你将需要 Visual Studio 或者 Windows SDK 命令行工具),关于更多 细节,请查看这篇相关博客。 或者,你也可以考虑使用 MinGW/MSYS2 curl 二进制文件。

使用生成的 Kotlin API

现在我们有了自己的库以及 Kotlin 存根,我们可以在一个应用程序中使用它们。为了让事情简单,在本教程中,我们将转换最简单的一个 libcurl 示例到 Kotlin。

有问题的代码来自示例(为简洁起见删除了评论)

#include <stdio.h>
#include <curl/curl.h>

int main(void)
{
  CURL *curl;
  CURLcode res;

  curl = curl_easy_init();
  if(curl) {
    curl_easy_setopt(curl, CURLOPT_URL, "http://example.com");
    curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);

    res = curl_easy_perform(curl);
    if(res != CURLE_OK)
      fprintf(stderr, "curl_easy_perform() failed: %s\n",
              curl_easy_strerror(res));
    curl_easy_cleanup(curl);
  }
  return 0;
}

第一件事情是我们将需要一个定义了 main 函数的 Kotlin 文件,并起名为 src/nativeMain/kotlin/hello.kt 接下来将继续转译每一行

import interop.*
import kotlinx.cinterop.*

fun main(args: Array<String>) {
    val curl = curl_easy_init()
    if (curl != null) {
        curl_easy_setopt(curl, CURLOPT_URL, "http://example.com")
        curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L)
        val res = curl_easy_perform(curl)
        if (res != CURLE_OK) {
            println("curl_easy_perform() failed ${curl_easy_strerror(res)?.toKString()}")
        }
        curl_easy_cleanup(curl)
    }
}

我们可以看到,我们已经消除了 Kotlin 版本中的显式变量声明,但其他所有内容都是 C 版本的逐行转译。我们期望所有对 libcurl 库的调用都可以转换为它们在 Kotlin 中的等价物。

注意,出于本教程的目的,我们对逐行进行了直译。显然,我们可以用更 Kotlin 的惯用方式来编写这个例子。

编译与链接库

下一步是编译我们的应用程序。基本 Kotlin/Native 应用程序这篇教程涵盖了使用命令行编译 Kotlin/Native 应用程序的基础知识。 在本案例中唯一不同的地方是 cinterop 生成的部分隐式包含在构建中: 让我们输入下面的命令:

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

如果在编译期间没有错误,我们应该看到程序执行的结果,它应该输出网站 http://example.com 的内容

Output

我们看到实际输出的原因是因为调用 curl_easy_perform 将结果打印到标准输出。我们应该使用 curl_easy_setopt 隐藏它。

有关使用 libcurl 的更完整示例,libcurl 在 Kotlin/Native 项目中的示例展示了如何将代码抽象为 Kotlin 类以及显示标题。它还演示了如何通过将它们组合到 shell 脚本或 Gradle 构建脚本中来使步骤更简洁一些。我们将在后续教程中介绍这些主题的更多细节。