与 C 代码交互
Kotlin/Native 遵循 Kotlin 的传统, 提供与既有的平台软件的优秀的互操作性. 对于原生程序来说, 最重要的互操作性对象就是与 C 语言库. 因此 Kotlin/Native 附带了 cinterop 工具, 可以用来快速生成与既有的外部库交互时所需要的一切.
与原生库交互时的工作流程如下:
创建一个
.def
文件, 描述需要绑定(binding)的内容.使用 cinterop 工具生成绑定.
运行 Kotlin/Native 编译器, 编译应用程序, 产生最终的可执行文件.
互操作性工具会分析 C 语言头文件, 并产生一个 "自然的" 映射, 将数据类型, 函数, 常数, 引入到 Kotlin 语言的世界. 工具生成的桩代码(stub)可以导入 IDE, 用来帮助代码自动生成, 以及代码跳转.
此外还提供了与 Swift/Objective-C 语言的互操作功能, 详情请参见 与 Swift/Objective-C 的交互.
平台库
注意, 很多情况下不要用到自定义的互操作库创建机制(我们后文将会介绍), 因为对于平台上的标准绑定中的那些 API, 可以使用 平台库. 例如, Linux/macOS 平台上的 POSIX, Windows 平台上的 Win32, macOS/iOS 平台上的以及 Apple 框架, 都可以通过这种方式来使用.
一个简单的示例
首先我们安装 libgit2, 并为 git 库准备桩代码:
编译客户端代码:
运行客户端代码:
为一个新库创建绑定
要对一个新的库创建绑定, 首先要创建并配置一个 定义文件.
绑定
基本的 interop 数据类型
C 中支持的所有数据类型, 都有对应的 Kotlin 类型:
有符号整数, 无符号整数, 以及浮点类型, 会被映射为 Kotlin 中的同样类型, 并且长度相同.
指针和数组映射为
CPointer<T>?
类型.枚举型映射为 Kotlin 的枚举型, 或整数型, 由 heuristic 以及 定义文件中设置 决定.
结构体(Struct)和联合体(Union)映射为通过点号访问的域的形式, 也就是
someStructInstance.field1
的形式.typedef
映射为typealias
.
此外, 任何 C 类型都有对应的 Kotlin 类型来表达这个类型的左值(lvalue), 也就是, 在内存中分配的那个值, 而不是简单的不可变的自包含值. 你可以想想 C++ 的引用, 与这个概念类似. 对于结构体(Struct) (以及指向结构体的 typedef
) 左值类型就是它的主要表达形式, 而且使用与结构体本身相同的名字, 对于 Kotlin 枚举类型, 左值类型名称是 ${type}Var
, 对于 CPointer<T>
, 左值类型名称是 CPointerVar<T>
, 对于大多数其他类型, 左值类型名称是 ${type}Var
.
对于兼有这两种表达形式的类型, 包含 "左值(lvalue)" 的那个类型, 带有一个可变的 .value
属性, 可以用来访问这个左值.
指针类型
CPointer<T>
的类型参数 T
必须是上面介绍的 "左值(lvalue)" 类型之一, 例如, C 类型 struct S*
会被映射为 CPointer<S>
, int8_t*
会被映射为 CPointer<int_8tVar>
, char**
会被映射为 CPointer<CPointerVar<ByteVar>>
.
C 的空指针(null) 在 Kotlin 中表达为 null
, 指针类型 CPointer<T>
是不可为空的, 而 CPointer<T>?
类型则是可为空的. 这种类型的值支持 Kotlin 的所有涉及 null
值处理的操作, 例如 ?:
, ?.
, !!
等等:
由于数组也被映射为 CPointer<T>
, 因此这个类型也支持 []
操作, 可以使用下标来访问数组中的值:
CPointer<T>
的 .pointed
属性返回这个指针指向的那个位置的类型 T
的左值. 相反的操作是 .ptr
: 它接受一个左值, 返回一个指向它的指针.
void*
映射为 COpaquePointer
– 这是一个特殊的指针类型, 它是任何其他指针类型的超类. 因此, 如果 C 函数接受 void*
类型参数, 那么绑定的 Kotlin 函数就可以接受任何 CPointer
类型参数.
可以使用 .reinterpret<T>
来对一个指针进行类型变换(包括 COpaquePointer
), 例如:
或者
和 C 一样, 这样的类型变换是不安全的, 可能导致应用程序发生潜在的内存错误.
对于 CPointer<T>?
和 Long
也有不安全的类型变换方法, 由扩展函数 .toLong()
和 .toCPointer<T>()
实现:
注意, 如果结果类型可以通过上下文确定, 那么类型参数可以省略, 就象 Kotlin 中通常的类型系统一样.
内存分配
可以使用 NativePlacement
接口来分配原生内存, 例如:
或者
内存最 "自然" 的位置就是在 nativeHeap
对象内. 这个操作就相当于使用 malloc
来分配原生内存, 另外还提供了 .free()
操作来释放已分配的内存:
然而, 分配的内存的生命周期通常会限定在一个指明的作用范围内. 可以使用 memScoped { ... }
来定义这样的作用范围. 在括号内部, 可以以隐含的接收者的形式访问到一个临时的内存分配位置, 因此可以使用 alloc
和 allocArray
来分配原生内存, 离开这个作用范围后, 已分配的这些内存会被自动释放.
例如, 如果一个 C 函数, 使用指针参数返回值, 可以用下面这种方式来使用这个函数:
向绑定传递指针
尽管 C 指针被映射为 CPointer<T>
类型, 但 C 函数的指针型参数会被映射为 CValuesRef<T>
类型. 如果向这样的参数传递 CPointer<T>
类型的值, 那么会原样传递给 C 函数. 但是, 也可以直接传递值的序列, 而不是传递指针. 这种情况下, 序列会以值的形式传递(by value), 也就是说, C 函数收到一个指针, 指向这个值序列的一个临时拷贝, 这个临时拷贝只在函数返回之前存在.
CValuesRef<T>
形式表达的指针型参数是为了用来支持 C 数组字面值, 而不必明确地进行内存分配操作. 为了构造一个不可变的自包含的 C 的值的序列, 可以使用下面这些方法:
${type}Array.toCValues()
, 其中type
是 Kotlin 的基本类型Array<CPointer<T>?>.toCValues()
,List<CPointer<T>?>.toCValues()
cValuesOf(vararg elements: ${type})
, 其中type
是基本类型, 或指针
例如:
字符串
与其它指针不同, const char*
类型参数会被表达为 Kotlin 的 String
类型. 因此对于 C 中期望字符串的绑定, 可以传递 Kotlin 的任何字符串值.
还有一些工具, 可以用来在 Kotlin 和 C 的字符串之间进行手工转换:
fun CPointer<ByteVar>.toKString(): String
val String.cstr: CValuesRef<ByteVar>
.
要得到指针, .cstr
应该在原生内存中分配, 例如:
在所有这些场合, C 字符串的编码都是 UTF-8.
要跳过字符串的自动转换, 并确保在绑定中使用原生的指针, 可以在 .def
文件中使用 noStringConversion
语句:
通过这种方式, 任何 CPointer<ByteVar>
类型的值都可以传递给 const char*
类型的参数. 如果需要传递 Kotlin 字符串, 可以使用这样的代码:
作用范围内的局部指针
memScoped { }
内有一个 CValues<T>.ptr
扩展属性, 使用它可以创建一个指向 CValues<T>
的 C 指针, 这个指针被限定在一个作用范围内. 通过它可以使用需要 C 指针的 API, 指针的生命周期限定在特定的 MemScope
内. 例如:
在这个示例程序中, 所有传递给 C API new_menu()
的值, 生命周期都被限定在它所属的最内层的 memScope
之内. 一旦程序的执行离开了 memScoped
作用范围, C 指针就不再存在了.
以值的方式传递和接收结构
如果一个 C 函数以传值的方式接受结构体(Struct)或联合体(Union) T
类型的参数, 或者以传值的方式返回结构体(Struct)或联合体(Union) T
类型的结果, 对应的参数类型或结果类型会被表达为 CValue<T>
.
CValue<T>
是一个不透明(opaque)类型, 因此无法通过适当的 Kotlin 属性访问到 C 结构体的域. 如果 API 以句柄的形式使用结构体, 那么这样是可行的, 但是如果确实需要访问结构体中的域, 那么可以使用以下转换方法:
fun T.readValue(): CValue<T>
. 将(左值)T
转换为一个CValue<T>
. 因此, 如果要构造一个CValue<T>
, 可以先分配T
, 为其中的域赋值, 然后将它转换为CValue<T>
.CValue<T>.useContents(block: T.() -> R): R
. 将CValue<T>
临时放到内存中, 然后使用放置在内存中的这个T
值作为接收者, 来运行参数中指定的 Lambda 表达式. 因此, 如果要读取结构体中一个单独的域, 可以使用下面的代码:val fieldValue = structValue.useContents { field }
回调
如果要将一个 Kotlin 函数转换为一个指向 C 函数的指针, 可以使用 staticCFunction(::kotlinFunction)
. 也可以使用 Lambda 表达式来代替函数引用. 这里的函数或 Lambda 表达式不能捕获任何值.
向回调传递用户数据
C API 经常允许向回调传递一些用户数据. 这些数据通常由用户在设置回调时提供. 数据使用例如 void*
的形式, 传递给某些 C 函数 (或写入到结构体内). 但是, Kotlin 对象的引用无法直接传递给 C. 因此需要在设置回调之前包装这些数据, 然后在回调函数内部将它们解开, 这样才能通过 C 函数来再两段 Kotlin 代码之间传递数据. 这种数据包装可以使用 StableRef
类实现.
要封装一个 Kotlin 对象的引用, 可以使用以下代码:
这里的 voidPtr
是一个 COpaquePointer
类型, 因此可以传递给 C 函数.
要解开这个引用, 可以使用以下代码:
这里的 kotlinReference
就是封装之前的 Kotlin 对象引用.
创建 StableRef
后, 最终需要使用 .dispose()
方法手动释放, 以避免内存泄漏:
释放后, 它就变得不可用了, 因此 voidPtr
也不能再次解开.
更多详情请参见 samples/libcurl
.
宏
每个展开为常数的 C 语言宏, 都会表达为一个 Kotlin 属性. 其他的宏都不支持. 但是, 可以将它们封装在支持的声明中, 这样就可以手动映射这些宏. 例如, 类似于函数的宏 FOO
可以映射为函数 foo
, 方法是向库 添加自定义的声明:
可移植性
有时, C 库中的函数参数, 或结构体的域使用了依赖于平台的数据类型, 例如 long
或 size_t
. Kotlin 本身没有提供隐含的整数类型转换, 也没有提供 C 风格的整数类型转换 (例如, (size_t) intValue
), 因此, 在这种情况下, 为了让编写可以移植的代码变得容易一点, 提供了 convert
方法:
这里, type1
和 type2
都必须是整数类型, 可以是有符号整数, 可以可以是无符号整数.
.convert<${type}>
的含义等同于 .toByte
, .toShort
, .toInt
, .toLong
, .toUByte
, .toUShort
, .toUInt
或 .toULong
方法, 具体等于哪个, 取决于 type
的具体类型.
使用 convert
的示例如下:
而且, 这个函数的类型参数可以自动推定得到, 因此很多情况下可以省略.
对象固定
Kotlin 对象可以固定(pin), 也就是, 确保它们在内存中的位置不会变化, 直到解除固定(unpin)为止, 而且, 指向这些对象的内部数据的指针, 可以传递给 C 函数. 例如:
这里我们使用了服务函数 usePinned
, 它会先固定一个对象, 然后执行一段代码, 最后无论是正常结束还是异常结束, 它都会将对象解除固定.