编辑本页

Using C Interop and libcurl for an App

最近更新 2019-04-15
Using C library from Kotlin/Native

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

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

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

生成绑定

An ideal scenario for interop is to call C functions as if we were calling Kotlin functions, that is, following the same signature and conventions. This is precisely what the cinterop tool provides us with. It takes a C library and generates the corresponding Kotlin bindings for it, which then allows us to use the library as if it were Kotlin code.

为了生成这些绑定,我们需要创建一个库定义 .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

A few things are going on in this file, let's go through them one by one. The first entry is headers which is the list of header files that we want to generate Kotlin stubs for. We can add multiple files to this entry, separating each one with a \ on a new line. In our case we only want curl.h. The files we are referencing need to be relative to the folder where the definition file is, or be available on the system path (in our case it would be /usr/include/curl).

The second line is the headerFilter. This is used to denote what exactly we want included. In C, when one file references another file with the #include directive, all the headers are also included. Sometimes this may not be needed, and we can use this parameter, using glob patterns, to fine tune things. Note, that headerFilter is an optional argument and mostly only used when the library we're using is being installed as a system library, and we do not want to fetch external dependencies (such as system stdint.h header) into our interop library. It may be important for both optimizing the library size and fixing potential conflicts between the system and the Kotlin/Native provided compilation environment.

The next lines are about providing linker and compiler options, which can vary depending on different target platforms. In our case, we are defining it for macOS (the .osx suffix) and Linux (the .linux suffix). Parameters without a suffix is also possible (e.g. linkerOpts=) and will be applied to all platforms.

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

Once we have the definition file ready, we can create project files and open the project in an IDE.

While it is possible to use the command line, either directly or by combining it with a script file (i.e., sh or bat file), we should notice, that it does not scale well for big projects that have hundreds of files and libraries. It is then better to use the Kotlin/Native compiler with a build system, as it helps to download and cache the Kotlin/Native compiler binaries and libraries with transitive dependencies and run the compiler and tests. Kotlin/Native can use the Gradle build system through the kotlin-multiplatform plugin.

We covered the basics of setting up an IDE compatible project with Gradle in the A Basic Kotlin/Native Application tutorial. Please check it out if you are looking for detailed first steps and instructions on how to start a new Kotlin/Native project and open it in IntelliJ IDEA. In this tutorial, we'll look at the advanced C interop related usages of Kotlin/Native and multiplatform builds with Gradle.

First, let's create a project folder. All the paths in this tutorial will be relative to this folder. Sometimes the missing directories will have to be created before any new files can be added.

We'll use the following build.gradle build.gradle.kts Gradle build file with the following contents:

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
}

The prepared project sources can be downloaded directly from GitHub GitHub GitHub GitHub GitHub GitHub

The project file configures the C interop as an additional step of the build. Let's move the interop.def file to the src/nativeInterop/cinterop directory. Gradle recommends using conventions instead of configurations, for example, the source files are expected to be in the src/nativeMain/kotlin folder. By default, all the symbols from C are imported to the interop package, we may want to import the whole package in our .kt files. Check out the kotlin-multiplatform plugin documentation to learn about all the different ways you could configure it.

在 Windows 上 curl

You should have the curl library binaries on Windows to make the sample work. You may build curl from sources on Windows (you'll need Visual Studio or Windows SDK Commandline tools), for more details, see the related blog post. Alternatively, you may also want to consider a MinGW/MSYS2 curl binary.

使用生成的 Kotlin API

Now we have our library and Kotlin stubs, we can consume them from our application. To keep things simple, in this tutorial we're going to convert one of the simplest libcurl examples over to 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)
    }
}

As we can see, we've eliminated the explicit variable declarations in the Kotlin version, but everything else is pretty much verbatim to the C version. All the calls we'd expect in the libcurl library are available in their Kotlin equivalent.

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

编译与链接库

The next step is to compile our application. We already covered the basics of compiling a Kotlin/Native application from the command line in the A Basic Kotlin/Native application tutorial. The only difference in this case is that the cinterop generated part is implicitly included into the build: Let's call the following command:

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

If there are no errors during compilation, we should see the result of the execution of our program, which on execution should output the contents of the site http://example.com

Output

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

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