Kotlin/Native 开发动态库
作者 | Eugene Petrenko,乔禹昂(翻译) |
最近更新 | 2019-04-15 |
在本教程中,我们将看到如何使用位于已经存在的原生应用程序或库中的代码。为此,我们需要将
Kotlin 代码编译为动态库,例如 .so
、.dylib
以及 .dll
。
Kotlin/Native 也可以与 Apple 技术紧密集成。 这篇 Kotlin/Native 开发 Apple Framework 教程解释了如何将代码编译为 Swift 或 Objective-C framework。
在这篇教程中,我们将:
- 将 Kotlin 代码编译为动态库
- 生成 C 的头文件
- 在 C 中调用 Kotlin 动态库
- 将示例代码编译并运行于 Linux 与 Mac 以及 Windows
创建 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.h
与libnative.dylib
- Linux:
libnative_api.h
与libnative.so
- Windows:
libnative_api.h
、libnative_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
函数。
全局函数 forInts
、forFloats
以及 strings
在 kotlin.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 教程中。