改进翻译

Kotlin/Native 开发动态库

作者 Eugene Petrenko,乔禹昂(翻译)
最近更新 2019-04-15
将 Kotlin/Native 编译为动态库

在本教程中,我们将看到如何使用位于已经存在的原生应用程序或库中的代码。为此,我们需要将 Kotlin 代码编译为动态库,例如 .so.dylib 以及 .dll

Kotlin/Native 也可以与 Apple 技术紧密集成。 这篇 Kotlin/Native 开发 Apple Framework 教程解释了如何将代码编译为 Swift 或 Objective-C framework。

在这篇教程中,我们将:

创建 Kotlin 库

Kotlin/Native 编译器可以将我们的 Kotlin 代码编译为一个动态库。 动态库常常带有一个头文件,即 .h 文件, 我们将通过它来调用编译后的 C 代码。

理解这些技术的最佳方法是尝试它们。 我们来创建第一个小型的 Kotlin 库,并在 C 程序中使用它。

我们可以先在 Kotlin 中创建一个库文件,然后将其保存为 hello.kt

package example

object Object {
  val field = "A"
}

class Clazz {
  fun memberFunction(p: Int): ULong = 42UL
}

fun forIntegers(b: Byte, s: Short, i: UInt, l: Long) { }
fun forFloats(f: Float, d: Double) { }

fun strings(str: String) : String? {
  return "That is '$str' from C"
}

val globalString = "A global String"

虽然可以使用命令行或直接通过将它与脚本文件(即 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 {
  linuxX64("native") {
    binaries {
      sharedLib {
        baseName = "native"
      }
    }
  }
}

wrapper {
  gradleVersion = "5.3.1"
  distributionType = "ALL"
}
plugins {
    id 'org.jetbrains.kotlin.multiplatform' version '1.3.21'
}

repositories {
    mavenCentral()
}

kotlin {
  macosX64("native") {
    binaries {
      sharedLib {
        baseName = "native"
      }
    }
  }
}

wrapper {
  gradleVersion = "5.3.1"
  distributionType = "ALL"
}
plugins {
    id 'org.jetbrains.kotlin.multiplatform' version '1.3.21'
}

repositories {
    mavenCentral()
}

kotlin {
  mingwX64("native") {
    binaries {
      sharedLib {
        baseName = "libnative"
      }
    }
  }
}

wrapper {
  gradleVersion = "5.3.1"
  distributionType = "ALL"
}
plugins {
    kotlin("multiplatform") version "1.3.21"
}

repositories {
    mavenCentral()
}

kotlin {
  linuxX64("native") {
    binaries {
      sharedLib {
        baseName = "native"
      }
    }
  }
}

tasks.wrapper {
  gradleVersion = "5.3.1"
  distributionType = Wrapper.DistributionType.ALL
}
plugins {
    kotlin("multiplatform") version "1.3.21"
}

repositories {
    mavenCentral()
}

kotlin {
  macosX64("native") {
    binaries {
      sharedLib {
        baseName = "native"
      }
    }
  }
}

tasks.wrapper {
  gradleVersion = "5.3.1"
  distributionType = Wrapper.DistributionType.ALL
}
plugins {
    kotlin("multiplatform") version "1.3.21"
}

repositories {
    mavenCentral()
}

kotlin {
  mingwX64("native") {
    binaries {
      sharedLib {
        baseName = "libnative"
      }
    }
  }
}

tasks.wrapper {
  gradleVersion = "5.3.1"
  distributionType = Wrapper.DistributionType.ALL
}

这个已经准备好的工程源文件可以从这里直接下载: GitHub GitHub GitHub GitHub GitHub GitHub

我们将工程目录下的源文件移动到 src/nativeMain/kotlin 文件夹下。当使用 kotlin 多平台插件的时候,对于源文件的位置, 这就是默认路径。我们使用以下代码块来指导和配置工程为我们生成动态或共享库:

binaries {
  sharedLib {
    baseName = "native"
  }  
}
binaries {
  sharedLib {
    baseName = "native"
  }  
}
binaries {
  sharedLib {
    baseName = "libnative"
  }  
}

libnative 用作库名,即生成的头文件名前缀。它同样也是该头文件中所有声明的前缀。

现在我们已经准备好在 IntelliJ IDEA 中打开这个工程并且可以看到如何修正这个示例工程。在我们这样做的时候, 我们将会研究 C 函数如何映射为 Kotlin/Native 声明。

我们来运行这个 linkNative Gradle 任务来在 IDE 中构建该库。 或者运行下面这行控制台命令:

./gradlew linkNative
./gradlew linkNative
gradlew.bat linkNative

构建将会在 build/bin/native/debugShared 文件夹下生成以下文件,并取决于目标操作系统:

  • macOS: libnative_api.hlibnative.dylib
  • Linux: libnative_api.hlibnative.so
  • Windows: libnative_api.hlibnative_symbols.def 以及 libnative.dll

Kotlin/Native 编译器用相似的规则在所有的平台上生成 .h 文件。
来看看我们的 Kotlin 库的 C 语言 API。

生成头文件

libnative_api.h 中,我们将发现如下代码。 我们将探讨其中部分代码,以便更容易理解。

注意,Kotlin/Native 的外部符号如有变更,将不会另外说明。

第一部分包含了标准的 C/C++ 头文件的首尾:

#ifndef KONAN_DEMO_H
#define KONAN_DEMO_H
#ifdef __cplusplus
extern "C" {
#endif

/// 生成的代码的其余部分在这里

#ifdef __cplusplus
}  /* extern "C" */
#endif
#endif  /* KONAN_DEMO_H */

libnative_api.h 中的上述内容之后,我们就拥有了一个声明通用类型定义的块:

#ifdef __cplusplus
typedef bool            libnative_KBoolean;
#else
typedef _Bool           libnative_KBoolean;
#endif
typedef unsigned short     libnative_KChar;
typedef signed char        libnative_KByte;
typedef short              libnative_KShort;
typedef int                libnative_KInt;
typedef long long          libnative_KLong;
typedef unsigned char      libnative_KUByte;
typedef unsigned short     libnative_KUShort;
typedef unsigned int       libnative_KUInt;
typedef unsigned long long libnative_KULong;
typedef float              libnative_KFloat;
typedef double             libnative_KDouble;
typedef void*              libnative_KNativePtr;

Kotlin 在已创建的 libnative_api.h 文件中为所有的声明都添加了 libnative_ 前缀。让我们以更容易阅读的方式来查看类型的映射:

Kotlin 定义。 C 类型
libnative_KBoolean bool_Bool
libnative_KChar unsigned short
libnative_KByte signed char
libnative_KShort short
libnative_KInt int
libnative_KLong long long
libnative_KUByte unsigned char
libnative_KUShort unsigned short
libnative_KUInt unsigned int
libnative_KULong unsigned long long
libnative_KFloat float
libnative_KDouble double
libnative_KNativePtr void*

这个定义部分展示了如何将 Kotlin 的原始类型映射为 C 的原始类型。 我们在这篇从 C 语言中映射原始类型教程中讨论了反向映射。

libnative_api.h 文件的下一个部分包含了在该库中使用的类型的定义:

struct libnative_KType;
typedef struct libnative_KType libnative_KType;

typedef struct {
  libnative_KNativePtr pinned;
} libnative_kref_example_Object;

typedef struct {
  libnative_KNativePtr pinned;
} libnative_kref_example_Clazz;

typedef struct { .. } TYPE_NAME 语法在 C 语言中用于声明一个结构体。 这个问题提供了对该模式的更多解释。

我们可以看到其中定义了 Kotlin 的 Object 类映射为 libnative_kref_example_Object,而 Clazz 映射到了 libnative_kref_example_Clazz。 这两个结构体都没有包含任何东西,但是 pinned 字段是一个指针,该字段类型 libnative_KNativePtr 定义在 void* 之上。

C 语言中没有支持命名空间,所以 Kotlin/Native 编译器生成了长名称,以避免与现有原生工程中的其他符号发生任何可能的冲突。

定义的很大一部分位于 libnative_api.h 文件。 它包含了我们的 Kotlin/Native 库世界的定义:

typedef struct {
  /* Service functions. */
  void (*DisposeStablePointer)(libnative_KNativePtr ptr);
  void (*DisposeString)(const char* string);
  libnative_KBoolean (*IsInstance)(libnative_KNativePtr ref, const libnative_KType* type);

  /* User functions. */
  struct {
    struct {
      struct {
        void (*forIntegers)(libnative_KByte b, libnative_KShort s, libnative_KUInt i, libnative_KLong l);
        void (*forFloats)(libnative_KFloat f, libnative_KDouble d);
        const char* (*strings)(const char* str);
        const char* (*get_globalString)();
        struct {
          libnative_KType* (*_type)(void);
          libnative_kref_example_Object (*_instance)();
          const char* (*get_field)(libnative_kref_example_Object thiz);
        } Object;
        struct {
          libnative_KType* (*_type)(void);
          libnative_kref_example_Clazz (*Clazz)();
          libnative_KULong (*memberFunction)(libnative_kref_example_Clazz thiz, libnative_KInt p);
        } Clazz;
      } example;
    } root;
  } kotlin;
} libnative_ExportedSymbols;

这段代码使用了匿名的结构体定义。代码 struct { .. } foo 在结构体外部声明了一个匿名结构体类型,这个类型没有名字。

C 语言同样也不支持对象。人们使用函数指针来模仿对象语义。一个函数指针被声明在 RETURN_TYPE (* FIELD_NAME)(PARAMETERS) 后面。 它的可读性很差,但我们应该能够从上面的结构体中看到函数指针字段。

运行时函数

阅读下面的代码。我们拥有这个 libnative_ExportedSymbols 结构体,它定义了所有 Kotlin/Native 中的以及我们的库提供给我们的函数。它使用嵌套匿名结构,以模仿包。libnative_ 前缀来源于库的名字。

libnative_ExportedSymbols 结构体包含了几个辅助函数:

void (*DisposeStablePointer)(libnative_KNativePtr ptr);
void (*DisposeString)(const char* string);
libnative_KBoolean (*IsInstance)(libnative_KNativePtr ref, const libnative_KType* type);

这些函数用于处理 Kotlin/Native 的对象。调用 DisposeStablePointer 来释放一个 Kotlin 对象,而 DisposeString 用于释放一个 Kotlin 字符串, 该字符串具有 C 中的 char* 类型。IsInstance 函数可以用于检查一个 Kotlin 类型或者一个 libnative_KNativePtr 是否是某个类型的实例。实际的生成操作取决于实际的使用情况。

Kotlin/Native 拥有垃圾回收机制,但是它不能帮助我们处理来源于 C 的 Kotlin 对象。Kotlin/Native 可以与 Objective-C 以及 Swift 进行互操作,并且结合了它们的引用计数。 这篇 Objective-C 互操作包含了更多关于此内容的细节。当然,也可以参考这篇 Kotlin/Native 开发 Apple Framework 文档。

库函数

我们来看看 kotlin.root.example 字段,它使用 kotlin.root. 前缀模仿 Kotlin 代码的包结构。

这里有一个 kotlin.root.example.Clazz 字段用来表示 Kotlin 中的 Clazz。这个 Clazz#memberFunction可以使用 memberFunction 字段访问的。唯一的区别是 memberFunction 接受 this 引用作为第一个参数。 C 语言不支持对象,所以这是为什么明确使用 this 指针访问的原因。

Clazz 字段中有一个构造函数(又名 kotlin.root.example.Clazz.Clazz), 这是创建 Clazz 实例的构造函数。

Kotlin object Object 是可以被 kotlin.root.example.Object 访问的。这里的 _instance 函数可以获取到该对象的唯一实例。

属性会转换为函数。get_set_ 前缀分别用于命名 getter 以及 setter 函数。举例来说, Kotlin 中的只读属性 globalString 在 C 中会转换为 get_globalString 函数。

全局函数 forIntsforFloats 以及 stringskotlin.root.example 匿名结构体中被转换为函数指针。

入口点

我们可以看到 API 是如何创建的。首先,我们需要初始化 libnative_ExportedSymbols 结构体。我们来看看 libnative_api.h 的最新部分:

extern libnative_ExportedSymbols* libnative_symbols(void);

函数 libnative_symbols 允许我们在原生代码中打开 Kotlin/Native 库。这是我们看到的入口点。该库名称被用作函数名称的前缀。

注意,Kotlin/Native 对象引用不支持多线程访问。 可能有必要为每个线程托管返回的 libnative_ExportedSymbols* 指针。

使用 C 中生成的头文件

使用 C 中的头文件非常简单明了。我们通过下面的代码创建了一个 main.c 文件:

#include "libnative_api.h"
#include "stdio.h"

int main(int argc, char** argv) {
  //获取调用 Kotlin/Native 函数的引用
  libnative_ExportedSymbols* lib = libnative_symbols();

  lib->kotlin.root.example.forIntegers(1, 2, 3, 4);
  lib->kotlin.root.example.forFloats(1.0f, 2.0);

  //使用 C 与 Kotlin/Native 的字符串
  const char* str = "Hello from Native!";
  const char* response = lib->kotlin.root.example.strings(str);
  printf("in: %s\nout:%s\n", str, response);
  lib->DisposeString(response);

  //创建 Kotlin 对象实例
  libnative_kref_example_Clazz newInstance = lib->kotlin.root.example.Clazz.Clazz();
  long x = lib->kotlin.root.example.Clazz.memberFunction(newInstance, 42);
  lib->DisposeStablePointer(newInstance.pinned);

  printf("DemoClazz returned %ld\n", x);

  return 0;
}

将示例编译并运行于 Linux 以及 macOS

在 macOS 10.13 的 Xcode 上,我们使用如下命令将 C 代码编译并链接到动态库:

clang main.c libnative.dylib

在 Linux 上我们也使用相似的命令:

gcc main.c libnative.so

编译器生成一个名为 a.out 的可执行文件。我们需要运行它来查看 Kotlin 代码是如何调用 C 库来运行的。在 Linux 上,我们将需要将 . 引入到 LD_LIBRARY_PATH 来使应用程序知晓从当前文件夹加载 libnative.so 库。

将示例编译并运行于 Windows

首先,我们需要安装一个支持 64 位目标操作系统的 Microsoft Visual C++ 编译器。最简单的方法是在我们的 Windows 机器上安装相同版本的 Microsoft Visual Studio。

我们将使用 x64 Native Tools Command Prompt <VERSION> 控制台。我们会看到在开始菜单中打开控制台的快捷方式。它附带一个 Microsoft Visual Studio 包。

在 Windows 上,动态库可以通过生成的静态库包装器以及手动编写代码的形式导入,使用 LoadLibrary 处理或类似的 Win32API 功能。我们将使用第一种选项并为我们自己的 libnative.dll 生成静态包装器。

我们使用工具链中的 lib.exe 来生成静态库包装器 libnative.lib,它可以在代码中自动使用 DLL:

lib /def:libnative_symbols.def /out:libnative.lib

现在我们已经准备好将 main.c 编译为可执行文件。我们将生成的 libnative.lib 导入到构建命令并启动:

cl.exe main.c libnative.lib

这行命令生成了 main.exe 文件供我们执行。

接下来

动态库是从现有程序使用 Kotlin 代码的主要方式。 我们可以使用它们来共享我们的代码到许多平台以及其他语言,包括 JVM、 Python、 iOS、Android 以及其他平台。

Kotlin/Native 同样也可以与 Objective-C 以及 Swift 紧密集成。 这部分内容被包含在 Kotlin/Native 开发 Apple Framework 教程中。