Kotlin 语言参考文档 中文版 Help

教程 - 使用 C Interop 和 libcurl 创建应用程序

本教程演示如何使用 IntelliJ IDEA 创建一个命令行应用程序. 你将学习如何创建一个简单的 HTTP 客户端程序, 它使用 Kotlin/Native 和 libcurl 库, 可以作为原生程序运行在指定的平台上.

输出将是一个可执行的命令行应用程序, 你可以在 macOS 和 Linux 上运行, 发送简单的 HTTP GET 请求.

开始前的准备工作

  1. 下载并安装最新版本的 IntelliJ IDEAKotlin plugin.

  2. 在 IntelliJ IDEA 中选择菜单 File | New | Project from Version Control, 克隆 项目模板.

  3. 查看项目结构:

    Native 应用程序项目结构

    模板创建的项目带有你开始工作时所需要的文件和文件夹. 请注意, 如果代码中不包含与特定平台相关的需求, 那么使用 Kotlin/Native 编写的应用程序可以编译到不同的平台. 你的代码放在 nativeMain 目录中, 此外还有对应的 nativeTest 目录. 在这个教程中, 请不要修改这些文件夹结构.

  4. 打开构建脚本文件 build.gradle.kts, 其中包含项目的设定. 请特别注意构建脚本文件中的以下内容:

    kotlin { val hostOs = System.getProperty("os.name") val isArm64 = System.getProperty("os.arch") == "aarch64" val isMingwX64 = hostOs.startsWith("Windows") val nativeTarget = when { hostOs == "Mac OS X" && isArm64 -> macosArm64("native") hostOs == "Mac OS X" && !isArm64 -> macosX64("native") hostOs == "Linux" && isArm64 -> linuxArm64("native") hostOs == "Linux" && !isArm64 -> linuxX64("native") isMingwX64 -> mingwX64("native") else -> throw GradleException("Host OS is not supported in Kotlin/Native.") } nativeTarget.apply { binaries { executable { entryPoint = "main" } } } }
    • 针对 macOS, Linux, 和 Windows 的编译目标分别通过 macosX64, macosArm64, linuxX64, linuxArm64, 和 mingwX64 定义. 关于所有支持的平台, 请参见 支持的平台.

    • 构建脚本定义一系列属性, 指定二进制文件如何生成, 以及应用程序的入口点. 这些可以使用默认值.

    • 与 C 的交互使用构建中的一个额外步骤来配置. 默认情况下, 来自 C 的所有符号会被导入到 interop 包. 你可能想要在 .kt 文件中导入整个包. 详情请参见 如何配置.

创建一个定义文件

编写原生应用程序时, 你经常需要访问没有包含在 Kotlin 标准库 中的某些功能, 比如发起 HTTP 请求, 读写磁盘, 等等.

Kotlin/Native 帮助你使用 C 的标准库, 使你可以利用 C 的整个生态系统, 其中的功能几乎包含你需要的任何东西. Kotlin/Native 带有一组预构建的 平台库, 提供了标准库之外的一些通用功能.

与 C 交互的理想场景是, 象调用 Kotlin 函数一样调用 C 函数, 使用相同的函数签名和规约. 这就是 cinterop 工具可以帮助你的地方. 它输入一个 C 库, 并生成对应的 Kotlin 绑定, 使得库可以象 Kotlin 代码那样使用.

要生成这些绑定, 要创建一个库定义 .def 文件, 包含一些关于需要的头的信息. 在这个应用程序中, 你需要 libcurl 库来发起 HTTP 调用. 创建一个定义文件的步骤如下:

  1. 选择 src 文件夹, 使用 File | New | Directory 创建一个新目录.

  2. 将新目录命名为 nativeInterop/cinterop. 这是头文件位置的默认约定, 如果你使用不同的位置, 也可以在 build.gradle.kts 文件中修改这个设置.

  3. 选择新建的子文件夹, 使用 File | New | File 创建一个新的 libcurl.def 文件.

  4. 将你的文件内容更新为以下代码:

    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 桩(stub)代码的头文件列表. 你可以在这里添加多个文件, 每个在新行加一个 \ 来分隔. 在这个示例中, 只有 curl.h. 引用的文件路径需要存在于系统路径中(在这个示例中, 是 /usr/include/curl).

    • headerFilter 指定具体包含什么. 在 C 中, 当一个文件使用 #include 指令引用另一个文件时, 所有的头文件都会被包含. 有时这些文件是不必要的, 你可以添加这个参数, 使用全局模式 进行细微调节.

      headerFilter 是一个可选的参数, 当库作为系统库安装时, 经常使用这个参数. 你不希望获取外部的依赖项 (比如系统的 stdint.h 头文件) 到你使用的库中. 对于优化库的大小, 修正系统与 Kotlin/Native 编译环境之间的潜在的冲突, 这个参数可能是很重要的.

    • 后面的行是提供链接器(linker) 和编译器的参数, 在不同的目标平台这些参数可能不同. 在这个示例中, 是针对 macOS (.osx 后缀) 和 Linux (.linux 后缀) 环境的参数. 也可以使用没有后缀的参数(比如, linkerOpts=), 会应用到所有的平台.

规约是, 每个库有自己的定义文件, 通常使用与库相同的名称库. 关于 cinterop 的所有选项, 详情请参见 与 C 代码交互.

向构建过程添加与 C 的交互

要使用头文件, 需要确保在构建过程中生成了它们. 要做到这一点, 请向 build.gradle.kts 文件添加以下内容:

nativeTarget.apply { compilations.getByName("main") { // NL cinterops { // NL val libcurl by creating // NL } // NL } // NL binaries { executable { entryPoint = "main" } } }

新加的行标注了 // NL. 首先, 添加 cinterops, 然后为每个 def 文件添加对应行. 默认情况下, 使用定义文件的名称. 你可以使用额外的参数来修改设定:

val libcurl by creating { defFile(project.file("src/nativeInterop/cinterop/libcurl.def")) packageName("com.jetbrains.handson.http") compilerOpts("-I/path") includeDirs.allHeaders("path") }

关于可用的选项, 请参见 与 C 代码交互.

编写应用程序代码

现在你有了库, 以及对应的 Kotlin 桩代码(stub), 可以在你的应用程序中使用它们了. 本教程将 simple.c 示例代码改写为 Kotlin.

src/nativeMain/kotlin/ 文件夹中, 将你的 Main.kt 文件更新为以下代码:

import kotlinx.cinterop.* import libcurl.* @OptIn(ExperimentalForeignApi::class) fun main(args: Array<String>) { val curl = curl_easy_init() if (curl != null) { curl_easy_setopt(curl, CURLOPT_URL, "https://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 版本一样. 在对应的 Kotlin 代码中, 可以使用 libcurl 库中所有的调用.

编译并运行应用程序

  1. 编译应用程序. 方法是, Gradle 运行的 task 中调用 runDebugExecutableNative, 或者在终端运行以下命令:

    ./gradlew runDebugExecutableNative

    这里, cinterop 的生成部分隐含的包含在构建中.

  2. 如果编译过程中没有错误, 点击main() 方法旁边侧栏中的绿色的 Run 图标, 或在 IntelliJ IDEA 中使用 Alt+Enter 快捷键启动 launch 菜单.

    IntelliJ IDEA 会打开 Run 页面, 并显示输出 — https://example.com 的内容:

    应用程序输出的 HTML 代码

你可以看到实际的输出, 因为调用 curl_easy_perform 会将结果打印到标准输出. 你可以使用 curl_easy_setopt 隐藏这些信息.

最终更新: 2024/10/17