7.3.3.3 把 Kotlin/Native 作为动态库 – 教程
原文链接: https://kotlinlang.org/docs/native-dynamic-libraries.html
7.3.3.3 把 Kotlin/Native 作为动态库 – 教程
你可以创建动态库,以便从现有程序中调用 Kotlin 代码。这让代码可以在许多平台或语言之间共享,包括 JVM、Python、Android 等。
提示: 对于 iOS 和其他 Apple 目标,我们建议生成 framework。请参阅把 Kotlin/Native 作为 Apple framework教程。
你可以在现有的原生应用或库中使用 Kotlin/Native 代码。为此,你需要把 Kotlin 代码编译为 .so、.dylib 或 .dll 格式的动态库。
在本教程中,你将:
你可以使用命令行直接生成 Kotlin 库,也可以通过脚本文件(例如 .sh 或 .bat 文件)来完成。不过,对于包含数百个文件和库的较大项目,这种方式扩展性不好。使用构建系统可以下载并缓存 Kotlin/Native 编译器二进制文件及其传递依赖的库,并负责运行编译器和测试,从而简化整个过程。Kotlin/Native 可以通过 Kotlin Multiplatform 插件使用 Gradle 构建系统。
我们来考察 Kotlin/Native 中与 C 互操作相关的高级用法,以及使用 Gradle 的 Kotlin Multiplatform 构建。
注意: 如果你使用 Mac,并且想为 macOS 或其他 Apple 目标创建并运行应用,还需要先安装 Xcode Command Line Tools、启动它并接受许可条款。
创建 Kotlin 库
Kotlin/Native 编译器可以从 Kotlin 代码生成动态库。动态库通常附带一个 .h 头文件,用于从 C 中调用编译后的代码。
我们来创建一个 Kotlin 库,并从一个 C 程序中使用它。
提示: 关于详细的第一步以及如何创建新的 Kotlin/Native 项目并在 IntelliJ IDEA 中打开它,请参阅 Kotlin/Native 入门教程。
- 进入
src/nativeMain/kotlin 目录,创建包含以下库内容的 lib.kt 文件:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| 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"
|
- 用以下内容更新你的
build.gradle(.kts) Gradle 构建文件:
Kotlin
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
| import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget
plugins {
kotlin("multiplatform") version "2.4.20"
}
repositories {
mavenCentral()
}
kotlin {
macosArm64() // 在 Apple 芯片上的 macOS
// linuxArm64() // ARM64 平台上的 Linux
// linuxX64() // x86_64 平台上的 Linux
// mingwX64() // 在 Windows 上
targets.withType<KotlinNativeTarget>().configureEach {
binaries {
sharedLib {
baseName = "native" // macOS
// baseName = "native" // Linux
// baseName = "libnative" // Windows
}
}
}
}
tasks.wrapper {
gradleVersion = "9.7.0"
distributionType = Wrapper.DistributionType.ALL
}
|
Groovy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
| import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget
plugins {
id 'org.jetbrains.kotlin.multiplatform' version '2.4.20'
}
repositories {
mavenCentral()
}
kotlin {
macosArm64() // Apple 芯片的 macOS
// linuxArm64() // ARM64 平台上的 Linux
// linuxX64() // x86_64 平台上的 Linux
// mingwX64() // Windows
targets.withType(KotlinNativeTarget).configureEach {
binaries {
sharedLib {
baseName = "native" // macOS
// baseName = "native" // Linux
// baseName = "libnative" // Windows
}
}
}
}
wrapper {
gradleVersion = "9.7.0"
distributionType = "ALL"
}
|
binaries {} 块把项目配置为生成动态库(共享库)。libnative 被用作库名,它是生成的头文件名的前缀,也会作为头文件中所有声明的前缀。
- 要构建该库,请在你的 IDE 中运行
linkDebugShared<YourTargetName> Gradle 任务,或在终端中使用控制台命令,例如:
1
| ./gradlew linkDebugSharedMacosArm64
|
构建会把库生成到 build/bin/<yourTargetName>/debugShared 目录中,包含以下文件:
- macOS:
libnative_api.h 和 libnative.dylib - Linux:
libnative_api.h 和 libnative.so - Windows:
libnative_api.h、libnative.def 和 libnative.dll
提示: 你也可以使用 linkNative Gradle 任务同时生成库的 debug 和 release 两种变体。
Kotlin/Native 编译器为所有平台生成 .h 文件时使用相同的规则。我们来看看该 Kotlin 库的 C API。
我们来考察 Kotlin/Native 声明如何映射到 C 函数。
在 build/bin/<yourTargetName>/debugShared 目录中,打开 libnative_api.h 头文件。最前面的部分包含标准的 C/C++ 头部和尾部:
1
2
3
4
5
6
7
8
9
10
11
12
| #ifndef KONAN_LIBNATIVE_H
#define KONAN_LIBNATIVE_H
#ifdef __cplusplus
extern "C" {
#endif
/// 其余生成的代码
#ifdef __cplusplus
} /* extern "C" */
#endif
#endif /* KONAN_LIBNATIVE_H */
|
在此之后,libnative_api.h 包含一个带有通用类型定义的块:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| #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 float __attribute__ ((__vector_size__ (16))) libnative_KVector128;
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_KVector128 | float __attribute__ ((__vector_size__ (16)) |
| libnative_KNativePtr | void* |
libnative_api.h 文件的定义部分展示了 Kotlin 原始类型如何映射到 C 原始类型。Kotlin/Native 编译器会为每个库自动生成这些条目。反向映射在从 C 映射原始数据类型教程中有说明。
在自动生成的类型定义之后,你会看到你的库中使用的单独类型定义:
1
2
3
4
5
6
7
8
9
10
11
| 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;
|
在 C 中,typedef struct { ... } TYPE_NAME 语法用于声明结构体。
提示: 关于这种模式的更多解释,请参阅这个 StackOverflow 讨论串。
从这些定义可以看出,Kotlin 类型使用相同的模式映射:Object 映射为 libnative_kref_example_Object,Clazz 映射为 libnative_kref_example_Clazz。所有结构体都只包含一个 pinned 字段(一个指针)。字段类型 libnative_KNativePtr 在文件前面定义为 void*。
由于 C 不支持命名空间,Kotlin/Native 编译器会生成很长的名称,以避免与现有原生项目中的其他符号发生任何可能的冲突。
服务运行时函数
libnative_ExportedSymbols 结构体定义了 Kotlin/Native 和你的库提供的所有函数。它大量使用嵌套的匿名结构体来模拟包。libnative_ 前缀来自库名。
libnative_ExportedSymbols 在头文件中包含若干辅助函数:
1
2
3
4
| typedef struct {
/* 服务函数。 */
void (*DisposeStablePointer)(libnative_KNativePtr ptr);
void (*DisposeString)(const char* string);
|
这些函数用于处理 Kotlin/Native 对象。DisposeStablePointer 用于释放对 Kotlin 对象的引用,DisposeString 用于释放 Kotlin 字符串(在 C 中其类型为 char*)。
libnative_api.h 文件的下一部分由运行时函数的结构体声明组成:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
| libnative_KBoolean (*IsInstance)(libnative_KNativePtr ref, const libnative_KType* type);
libnative_KBoolean (*IsInstance)(libnative_KNativePtr ref, const libnative_KType* type);
libnative_kref_kotlin_Byte (*createNullableByte)(libnative_KByte);
libnative_KByte (*getNonNullValueOfByte)(libnative_kref_kotlin_Byte);
libnative_kref_kotlin_Short (*createNullableShort)(libnative_KShort);
libnative_KShort (*getNonNullValueOfShort)(libnative_kref_kotlin_Short);
libnative_kref_kotlin_Int (*createNullableInt)(libnative_KInt);
libnative_KInt (*getNonNullValueOfInt)(libnative_kref_kotlin_Int);
libnative_kref_kotlin_Long (*createNullableLong)(libnative_KLong);
libnative_KLong (*getNonNullValueOfLong)(libnative_kref_kotlin_Long);
libnative_kref_kotlin_Float (*createNullableFloat)(libnative_KFloat);
libnative_KFloat (*getNonNullValueOfFloat)(libnative_kref_kotlin_Float);
libnative_kref_kotlin_Double (*createNullableDouble)(libnative_KDouble);
libnative_KDouble (*getNonNullValueOfDouble)(libnative_kref_kotlin_Double);
libnative_kref_kotlin_Char (*createNullableChar)(libnative_KChar);
libnative_KChar (*getNonNullValueOfChar)(libnative_kref_kotlin_Char);
libnative_kref_kotlin_Boolean (*createNullableBoolean)(libnative_KBoolean);
libnative_KBoolean (*getNonNullValueOfBoolean)(libnative_kref_kotlin_Boolean);
libnative_kref_kotlin_Unit (*createNullableUnit)(void);
libnative_kref_kotlin_UByte (*createNullableUByte)(libnative_KUByte);
libnative_KUByte (*getNonNullValueOfUByte)(libnative_kref_kotlin_UByte);
libnative_kref_kotlin_UShort (*createNullableUShort)(libnative_KUShort);
libnative_KUShort (*getNonNullValueOfUShort)(libnative_kref_kotlin_UShort);
libnative_kref_kotlin_UInt (*createNullableUInt)(libnative_KUInt);
libnative_KUInt (*getNonNullValueOfUInt)(libnative_kref_kotlin_UInt);
libnative_kref_kotlin_ULong (*createNullableULong)(libnative_KULong);
libnative_KULong (*getNonNullValueOfULong)(libnative_kref_kotlin_ULong);
|
你可以使用 IsInstance 函数检查某个 Kotlin 对象(通过其 .pinned 指针引用)是否是某个类型的实例。实际生成的操作集合取决于实际使用情况。
提示: Kotlin/Native 有自己的垃圾回收器,但它不管理从 C 访问的 Kotlin 对象。不过,Kotlin/Native 提供与 Swift/Objective-C 的互操作,并且垃圾回收器与 Swift/Objective-C ARC 集成。
你的库函数
我们来看看你的库中使用的单独结构体声明。libnative_kref_example 字段用 libnative_kref. 前缀模拟你的 Kotlin 代码的包结构:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| typedef struct {
/* 用户函数。 */
struct {
struct {
struct {
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;
const char* (*get_globalString)();
void (*forFloats)(libnative_KFloat f, libnative_KDouble d);
void (*forIntegers)(libnative_KByte b, libnative_KShort s, libnative_KUInt i, libnative_KLong l);
const char* (*strings)(const char* str);
} example;
} root;
} kotlin;
} libnative_ExportedSymbols;
|
这段代码使用了匿名结构体声明。这里的 struct { ... } foo 在外层结构体中声明了一个字段,其类型是没有名字的匿名结构体类型。
由于 C 也不支持对象,因此使用函数指针来模拟对象语义。函数指针的声明形式为 RETURN_TYPE (* FIELD_NAME)(PARAMETERS)。
libnative_kref_example_Clazz 字段表示 Kotlin 中的 Clazz。可以通过 memberFunction 字段访问 libnative_KULong。唯一的区别是 memberFunction 把 thiz 引用作为第一个参数。由于 C 不支持对象,thiz 指针是显式传入的。
Clazz 字段中有一个构造器(也就是 libnative_kref_example_Clazz_Clazz),它充当创建 Clazz 实例的构造函数。
Kotlin 的 object Object 可以通过 libnative_kref_example_Object 访问。_instance 函数获取该对象的唯一实例。
属性会被转换为函数。get_ 和 set_ 前缀分别命名 getter 和 setter 函数。例如,Kotlin 中的只读属性 globalString 在 C 中变为 get_globalString 函数。
全局函数 forFloats、forIntegers 和 strings 在 libnative_kref_example 匿名结构体中被转换为函数指针。
入口点
现在你知道了 API 是如何创建的,而 libnative_ExportedSymbols 结构体的初始化就是起点。接下来我们看看 libnative_api.h 的最后一部分:
1
| extern libnative_ExportedSymbols* libnative_symbols(void);
|
libnative_symbols 函数让你可以从原生代码打开通往 Kotlin/Native 库的入口。这是访问该库的入口点。库名被用作函数名的前缀。
注意: 可能需要按线程保存返回的 libnative_ExportedSymbols* 指针。
在 C 中使用生成的头文件很直接。在库目录中创建包含以下代码的 main.c 文件:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
| #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;
}
|
编译并运行项目
在 macOS 上
要编译 C 代码并与动态库链接,请进入库目录并运行以下命令:
1
| clang main.c libnative.dylib
|
编译器会生成一个名为 a.out 的可执行文件。运行它即可从 C 库中执行 Kotlin 代码。
在 Linux 上
要编译 C 代码并与动态库链接,请进入库目录并运行以下命令:
1
| gcc main.c libnative.so
|
编译器会生成一个名为 a.out 的可执行文件。运行它即可从 C 库中执行 Kotlin 代码。在 Linux 上,你需要把 . 加入 LD_LIBRARY_PATH,让应用知道从当前文件夹加载 libnative.so 库。
在 Windows 上
首先,你需要安装支持 x64_64 目标的 Microsoft Visual C++ 编译器。
最简单的方式是在 Windows 机器上安装 Microsoft Visual Studio。安装期间,选择使用 C++ 所需的组件,例如 Desktop development with C++。
在 Windows 上,你可以通过生成静态库包装器来包含动态库,也可以使用 LoadLibrary 或类似的 Win32API 函数手动包含。
我们采用第一种方式,为 libnative.dll 生成静态包装库:
- 从工具链调用
lib.exe,生成自动化 DLL 使用的静态库包装器 libnative.lib:
1
| lib /def:libnative.def /out:libnative.lib
|
- 把
main.c 编译为可执行文件。在构建命令中包含生成的 libnative.lib 并启动:
1
| cl.exe main.c libnative.lib
|
该命令会生成 main.exe 文件,你可以运行它。
接下来做什么