7.3.4 定义文件
7 分钟阅读
原文链接: https://kotlinlang.org/docs/native-definition-file.html
7.3.4 定义文件
Kotlin/Native 让你可以使用 C 和 Objective-C 库,从而在 Kotlin 中使用它们的功能。一个名为 cinterop 的特殊工具会接收 C 或 Objective-C 库并生成相应的 Kotlin 绑定,使该库的方法可以像平常那样在你的 Kotlin 代码中使用。
要生成这些绑定,每个库都需要一个定义文件,通常与库同名。这是一个属性文件,精确描述了该库应如何被使用。请查看可用属性的完整列表。
下面是使用项目时的一般工作流:
- 创建一个
.def文件,描述要在绑定中包含哪些内容。 - 在你的 Kotlin 代码中使用生成的绑定。
- 运行 Kotlin/Native 编译器以生成最终可执行文件。
创建并配置定义文件
我们来创建一个定义文件并为一个 C 库生成绑定:
- 在你的 IDE 中,选中
src文件夹,并通过 File | New | Directory 创建一个新目录。 - 把新目录命名为
nativeInterop/cinterop。
这是 .def 文件位置的默认约定,但如果你使用其他位置,可以在 build.gradle.kts 文件中覆盖它。
3. 选中新的子文件夹,并通过 File | New | File 创建一个 png.def 文件。
4. 添加必要的属性:
headers = png.h
headerFilter = png.h
package = png
compilerOpts.linux = -I/usr/include -I/usr/include/x86_64-linux-gnu
linkerOpts.osx = -L/opt/local/lib -L/usr/local/opt/png/lib -lpng
linkerOpts.linux = -L/usr/lib/x86_64-linux-gnu -lpng
headers是要为其生成 Kotlin 桩的头文件列表。你可以向该条目添加多个文件,用空格分隔。在这里只有png.h。所引用的文件需要位于指定路径下(在本例中是/usr/include/png)。headerFilter表示具体包含哪些内容。在 C 中,当一个文件通过#include指令引用另一个文件时,所有头文件都会被一并包含。有时这并不必要,你可以使用 glob 模式添加该参数来进行调整。
如果你不想把外部依赖(例如系统 stdint.h 头文件)拉入互操作库,可以使用 headerFilter。它对于优化库体积、修复系统与所提供的 Kotlin/Native 编译环境之间的潜在冲突也可能有用。
- 如果需要修改某个平台的行为,你可以使用
compilerOpts.osx或compilerOpts.linux这样的形式为选项提供平台特定的值。在本例中,它们分别是 macOS(.osx后缀)和 Linux(.linux后缀)。也可以使用不带后缀的参数(例如linkerOpts=),它们会应用于所有平台。
- 要生成绑定,请点击通知中的 Sync Now 来同步 Gradle 文件。

生成绑定之后,IDE 就可以把它们当作原生库的代理视图来使用。
提示: 你也可以在命令行中使用 cinterop 工具来配置绑定的生成。
属性
以下是可在定义文件中使用、用于调整生成产物内容的属性完整列表。更多信息请参阅下面相应的小节。
| 属性 | 说明 |
| headers | 要包含在绑定中的库头文件列表。 |
| modules | 要包含在绑定中的 Objective-C 库的 Clang 模块列表。 |
| language | 指定语言。默认使用 C;如有必要可改为 Objective-C。 |
| compilerOpts | cinterop 工具传递给 C 编译器的编译器选项。 |
| linkerOpts | cinterop 工具传递给链接器的链接器选项。 |
| excludedFunctions | 应被忽略的函数名称列表,以空格分隔。 |
| staticLibraries | 实验性。把静态库包含到 .klib 中。 |
| libraryPaths | 实验性。cinterop 工具在其中搜索要包含到 .klib 的库的目录列表,以空格分隔。 |
| package | 生成的 Kotlin API 的包前缀。 |
| headerFilter | 使用 glob 过滤头文件,导入库时只包含匹配的头文件。 |
| excludeFilter | 导入库时排除特定的头文件,优先级高于 headerFilter。 |
| strictEnums | 应生成为 Kotlin 枚举的枚举列表,以空格分隔。 |
| nonStrictEnums | 应生成为整数值的枚举列表,以空格分隔。 |
| noStringConversion | 其 const char* 参数不应被自动转换为 Kotlin String 的函数列表,以空格分隔。 |
| allowedOverloadsForCFunctions | 默认情况下,假定 C 函数名称是唯一的。如果有多个同名函数,只会选取其中一个。不过,你可以通过在 allowedOverloadsForCFunctions 中指定这些函数来改变这一点。 |
| disableDesignatedInitializerChecks | 禁用“不允许把非指定 Objective-C 初始化器作为 super() 构造器调用”的编译器检查。 |
| foreignExceptionMode | 把来自 Objective-C 代码的异常包装为 Kotlin 中 ForeignException 类型的异常。 |
| userSetupHint | 添加自定义消息,例如帮助用户解决链接器错误。 |
除了属性列表之外,你还可以在定义文件中包含自定义声明。
导入头文件
如果某个 C 库没有 Clang 模块,而是由一组头文件组成,请使用 headers 属性指定应导入的头文件:
headers = curl/curl.h
使用 glob 过滤头文件
你可以使用 .def 文件中的过滤器属性按 glob 过滤头文件。要包含来自头文件的声明,请使用 headerFilter 属性。如果某个头文件匹配任一 glob,它的声明就会被包含在绑定中。
这些 glob 会应用于相对于相应 include 路径元素的头文件路径,例如 time.h 或 curl/curl.h。因此,如果该库通常通过 #include <SomeLibrary/Header.h> 被包含,你大概可以用以下过滤器来过滤头文件:
headerFilter = SomeLibrary/**
如果未提供 headerFilter,则包含所有头文件。不过,我们鼓励你使用 headerFilter 并尽可能精确地指定 glob。这样生成的库只包含必要的声明。这有助于避免在升级 Kotlin 或开发环境中的工具时出现的各种问题。
排除头文件
要排除特定的头文件,请使用 excludeFilter 属性。它有助于移除冗余或有问题的头文件并优化编译,因为指定头文件中的声明不会被包含在绑定中:
excludeFilter = SomeLibrary/time.h
注意: 如果同一个头文件既通过
headerFilter被包含,又被excludeFilter排除,那么该头文件不会被包含在绑定中。
导入模块
如果某个 Objective-C 库有 Clang 模块,请使用 modules 属性指定要导入的模块:
modules = UIKit
设置包名
使用 package 属性为生成的 Kotlin API 指定包前缀:
package = png
如果你不指定该属性,编译器会在根包中生成声明。
注意:
kotlin和kotlinx.cinterop名称是保留的,不能用作包前缀。
传递编译器和链接器选项
使用 compilerOpts 属性把选项传给 C 编译器,它在底层用于分析头文件。要把选项传给用于链接最终可执行文件的链接器,请使用 linkerOpts。例如:
compilerOpts = -DFOO=bar
linkerOpts = -lpng
你也可以指定仅适用于某个特定目标的目标特有选项:
compilerOpts = -DBAR=bar
compilerOpts.linux_x64 = -DFOO=foo1
compilerOpts.macos_x64 = -DFOO=foo2
使用该配置,头文件在 Linux 上会使用 -DBAR=bar -DFOO=foo1 分析,在 macOS 上则使用 -DBAR=bar -DFOO=foo2。请注意,任何定义文件选项都可以同时具有通用部分和平台特有部分。
忽略特定函数
使用 excludedFunctions 属性指定应被忽略的函数名称列表。如果头文件中声明的某个函数不保证可调用,而自动判断这一点又困难或不可能,该属性就很有用。你也可以用它来绕过互操作本身的一个缺陷。
包含静态库
实验性 - 通用
有时随产品一起分发静态库比假定用户环境中已有它更方便。要把静态库包含到 .klib 中,请使用 staticLibrary 和 libraryPaths 属性:
headers = foo.h
staticLibraries = libfoo.a
libraryPaths = /opt/local/lib /usr/local/opt/curl/lib
使用上面的代码片段时,cinterop 工具会在 /opt/local/lib 和 /usr/local/opt/curl/lib 中搜索 libfoo.a,如果找到,就把该库的二进制文件包含到 klib 中。
在你的程序中使用这样的 klib 时,该库会被自动链接。
配置枚举的生成
使用 strictEnums 属性把枚举生成为 Kotlin 枚举,或使用 nonStrictEnums 把它们生成为整数值。如果某个枚举没有包含在这两个列表中,则根据启发式规则生成。
设置字符串转换
使用 noStringConversion 属性禁用把 const char* 函数参数自动转换为 Kotlin String。
允许调用非指定初始化器
默认情况下,Kotlin/Native 编译器不允许把非指定的 Objective-C 初始化器作为 super() 构造器调用。如果该库中没有正确标记指定的 Objective-C 初始化器,这种行为可能会带来不便。要禁用这些编译器检查,请使用 disableDesignatedInitializerChecks 属性。
处理 Objective-C 异常
默认情况下,如果 Objective-C 异常到达 Objective-C 到 Kotlin 的互操作边界并进入 Kotlin 代码,程序会崩溃。
要把 Objective-C 异常传播到 Kotlin,请使用 foreignExceptionMode = objc-wrap 属性启用包装。在这种情况下,Objective-C 异常会被转换为 Kotlin 中 ForeignException 类型的异常。
帮助解决链接器错误
当 Kotlin 库依赖 C 或 Objective-C 库时(例如使用 CocoaPods 集成),可能会出现链接器错误。如果依赖库没有安装在本地机器上,也没有在项目构建脚本中显式配置,就会出现 “Framework not found” 错误。
如果你是库作者,可以通过自定义消息帮助用户解决链接器错误。为此,请在 .def 文件中添加 userSetupHint=message 属性,或向 cinterop 传递 -Xuser-setup-hint 编译器选项。
添加自定义声明
有时需要在生成绑定之前向库中添加自定义 C 声明(例如为了宏)。除了创建包含这些声明的额外头文件之外,你还可以把它们直接写进 .def 文件的末尾,放在仅包含分隔符序列 --- 的分隔行之后:
headers = errno.h
---
static inline int getErrno() {
return errno;
}
请注意,.def 文件的这一部分会被当作头文件的一部分来处理,因此带函数体的函数应声明为 static。这些声明会在包含 headers 列表中的文件之后被解析。
使用命令行生成绑定
除了定义文件之外,你还可以在 cinterop 调用中把相应属性作为选项传入,以指定要在绑定中包含的内容。
下面是一个生成已编译库 png.klib 的命令示例:
| |
请注意,生成的绑定通常是平台特有的,因此如果你要面向多个目标开发,就需要重新生成绑定。
- 对于未包含在 sysroot 搜索路径中的宿主库,可能需要提供头文件。
- 对于带配置脚本的典型 UNIX 库,
compilerOpts很可能包含带--cflags选项的配置脚本输出(可能不含确切路径)。 - 带
--libs的配置脚本输出可以传给linkerOpts属性。