Kotlin 语言参考文档 中文版 Help

与 C 代码交互

Kotlin/Native 遵循 Kotlin 的传统, 提供与既有的平台软件的优秀的互操作性. 对于原生程序来说, 最重要的互操作性对象就是与 C 语言库. 因此 Kotlin/Native 附带了 cinterop 工具, 可以用来快速生成与既有的外部库交互时所需要的一切.

与原生库交互时的工作流程如下:

  1. 创建一个 .def 文件, 描述需要绑定(binding)的内容.

  2. 使用 cinterop 工具生成绑定.

  3. 运行 Kotlin/Native 编译器, 编译应用程序, 产生最终的可执行文件.

互操作性工具会分析 C 语言头文件, 并产生一个 "自然的" 映射, 将数据类型, 函数, 常数, 引入到 Kotlin 语言的世界. 工具生成的桩代码(stub)可以导入 IDE, 用来帮助代码自动生成, 以及代码跳转.

此外还提供了与 Swift/Objective-C 语言的互操作功能, 详情请参见 与 Swift/Objective-C 的交互.

平台库

注意, 很多情况下不要用到自定义的互操作库创建机制(我们后文将会介绍), 因为对于平台上的标准绑定中的那些 API, 可以使用 平台库. 例如, Linux/macOS 平台上的 POSIX, Windows 平台上的 Win32, macOS/iOS 平台上的以及 Apple 框架, 都可以通过这种方式来使用.

一个简单的示例

首先我们安装 libgit2, 并为 git 库准备桩代码:

cd samples/gitchurn ../../dist/bin/cinterop -def src/nativeInterop/cinterop/libgit2.def \ -compiler-option -I/usr/local/include -o libgit2

编译客户端代码:

../../dist/bin/kotlinc src/gitChurnMain/kotlin \ -library libgit2 -o GitChurn

运行客户端代码:

./GitChurn.kexe ../..

为一个新库创建绑定

要对一个新的库创建绑定, 首先要创建并配置一个 定义文件.

绑定

基本的 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 值处理的操作, 例如 ?:, ?., !! 等等:

val path = getenv("PATH")?.toKString() ?: ""

由于数组也被映射为 CPointer<T>, 因此这个类型也支持 [] 操作, 可以使用下标来访问数组中的值:

import kotlinx.cinterop.* @OptIn(ExperimentalForeignApi::class) fun shift(ptr: CPointer<ByteVar>, length: Int) { for (index in 0 .. length - 2) { ptr[index] = ptr[index + 1] } }

CPointer<T>.pointed 属性返回这个指针指向的那个位置的类型 T 的左值. 相反的操作是 .ptr: 它接受一个左值, 返回一个指向它的指针.

void* 映射为 COpaquePointer – 这是一个特殊的指针类型, 它是任何其他指针类型的超类. 因此, 如果 C 函数接受 void* 类型参数, 那么绑定的 Kotlin 函数就可以接受任何 CPointer 类型参数.

可以使用 .reinterpret<T> 来对一个指针进行类型变换(包括 COpaquePointer), 例如:

import kotlinx.cinterop.* @OptIn(ExperimentalForeignApi::class) val intPtr = bytePtr.reinterpret<IntVar>()

或者

import kotlinx.cinterop.* @OptIn(ExperimentalForeignApi::class) val intPtr: CPointer<IntVar> = bytePtr.reinterpret()

和 C 一样, 这样的类型变换是不安全的, 可能导致应用程序发生潜在的内存错误.

对于 CPointer<T>?Long 也有不安全的类型变换方法, 由扩展函数 .toLong().toCPointer<T>() 实现:

val longValue = ptr.toLong() val originalPtr = longValue.toCPointer<T>()

注意, 如果结果类型可以通过上下文确定, 那么类型参数可以省略, 就象 Kotlin 中通常的类型系统一样.

内存分配

可以使用 NativePlacement 接口来分配原生内存, 例如:

import kotlinx.cinterop.* @OptIn(ExperimentalForeignApi::class) val byteVar = placement.alloc<ByteVar>()

或者

import kotlinx.cinterop.* @OptIn(ExperimentalForeignApi::class) val bytePtr = placement.allocArray<ByteVar>(5)

内存最 "自然" 的位置就是在 nativeHeap 对象内. 这个操作就相当于使用 malloc 来分配原生内存, 另外还提供了 .free() 操作来释放已分配的内存:

import kotlinx.cinterop.* @OptIn(kotlinx.cinterop.ExperimentalForeignApi::class) fun main() { val size: Long = 0 val buffer = nativeHeap.allocArray<ByteVar>(size) nativeHeap.free(buffer) }

然而, 分配的内存的生命周期通常会限定在一个指明的作用范围内. 可以使用 memScoped { ... } 来定义这样的作用范围. 在括号内部, 可以以隐含的接收者的形式访问到一个临时的内存分配位置, 因此可以使用 allocallocArray 来分配原生内存, 离开这个作用范围后, 已分配的这些内存会被自动释放.

例如, 如果一个 C 函数, 使用指针参数返回值, 可以用下面这种方式来使用这个函数:

import kotlinx.cinterop.* import platform.posix.* @OptIn(ExperimentalForeignApi::class) val fileSize = memScoped { val statBuf = alloc<stat>() val error = stat("/", statBuf.ptr) statBuf.st_size }

向绑定传递指针

尽管 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 是基本类型, 或指针

例如:

// C 代码: void foo(int* elements, int count); ... int elements[] = {1, 2, 3}; foo(elements, 3);
// Kotlin 代码: foo(cValuesOf(1, 2, 3), 3)

字符串

与其它指针不同, const char* 类型参数会被表达为 Kotlin 的 String 类型. 因此对于 C 中期望字符串的绑定, 可以传递 Kotlin 的任何字符串值.

还有一些工具, 可以用来在 Kotlin 和 C 的字符串之间进行手工转换:

  • fun CPointer<ByteVar>.toKString(): String

  • val String.cstr: CValuesRef<ByteVar>.

要得到指针, .cstr 应该在原生内存中分配, 例如:

val cString = kotlinString.cstr.getPointer(nativeHeap)

在所有这些场合, C 字符串的编码都是 UTF-8.

要跳过字符串的自动转换, 并确保在绑定中使用原生的指针, 可以在 .def 文件中使用 noStringConversion 语句:

noStringConversion = LoadCursorA LoadCursorW

通过这种方式, 任何 CPointer<ByteVar> 类型的值都可以传递给 const char* 类型的参数. 如果需要传递 Kotlin 字符串, 可以使用这样的代码:

import kotlinx.cinterop.* @OptIn(kotlinx.cinterop.ExperimentalForeignApi::class) memScoped { LoadCursorA(null, "cursor.bmp".cstr.ptr) // 对这个函数的 ASCII 版 LoadCursorW(null, "cursor.bmp".wcstr.ptr) // 对这个函数的 Unicode 版 }

作用范围内的局部指针

memScoped { } 内有一个 CValues<T>.ptr 扩展属性, 使用它可以创建一个指向 CValues<T> 的 C 指针, 这个指针被限定在一个作用范围内. 通过它可以使用需要 C 指针的 API, 指针的生命周期限定在特定的 MemScope 内. 例如:

import kotlinx.cinterop.* @OptIn(kotlinx.cinterop.ExperimentalForeignApi::class) memScoped { items = arrayOfNulls<CPointer<ITEM>?>(6) arrayOf("one", "two").forEachIndexed { index, value -> items[index] = value.cstr.ptr } menu = new_menu("Menu".cstr.ptr, items.toCValues().ptr) // ... }

在这个示例程序中, 所有传递给 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 对象的引用, 可以使用以下代码:

import kotlinx.cinterop.* @OptIn(ExperimentalForeignApi::class) val stableRef = StableRef.create(kotlinReference) val voidPtr = stableRef.asCPointer()

这里的 voidPtr 是一个 COpaquePointer 类型, 因此可以传递给 C 函数.

要解开这个引用, 可以使用以下代码:

@OptIn(ExperimentalForeignApi::class) val stableRef = voidPtr.asStableRef<KotlinClass>() val kotlinReference = stableRef.get()

这里的 kotlinReference 就是封装之前的 Kotlin 对象引用.

创建 StableRef 后, 最终需要使用 .dispose() 方法手动释放, 以避免内存泄漏:

stableRef.dispose()

释放后, 它就变得不可用了, 因此 voidPtr 也不能再次解开.

更多详情请参见 samples/libcurl.

每个展开为常数的 C 语言宏, 都会表达为一个 Kotlin 属性. 其他的宏都不支持. 但是, 可以将它们封装在支持的声明中, 这样就可以手动映射这些宏. 例如, 类似于函数的宏 FOO 可以映射为函数 foo, 方法是向库 添加自定义的声明:

headers = library/base.h --- static inline int foo(int arg) { return FOO(arg); }

可移植性

有时, C 库中的函数参数, 或结构体的域使用了依赖于平台的数据类型, 例如 longsize_t. Kotlin 本身没有提供隐含的整数类型转换, 也没有提供 C 风格的整数类型转换 (例如, (size_t) intValue), 因此, 在这种情况下, 为了让编写可以移植的代码变得容易一点, 提供了 convert 方法:

fun ${type1}.convert<${type2}>(): ${type2}

这里, type1type2 都必须是整数类型, 可以是有符号整数, 可以可以是无符号整数.

.convert<${type}> 的含义等同于 .toByte, .toShort, .toInt, .toLong, .toUByte, .toUShort, .toUInt.toULong 方法, 具体等于哪个, 取决于 type 的具体类型.

使用 convert 的示例如下:

import kotlinx.cinterop.* import platform.posix.* @OptIn(ExperimentalForeignApi::class) fun zeroMemory(buffer: COpaquePointer, size: Int) { memset(buffer, 0, size.convert<size_t>()) }

而且, 这个函数的类型参数可以自动推定得到, 因此很多情况下可以省略.

对象固定

Kotlin 对象可以固定(pin), 也就是, 确保它们在内存中的位置不会变化, 直到解除固定(unpin)为止, 而且, 指向这些对象的内部数据的指针, 可以传递给 C 函数. 例如:

import kotlinx.cinterop.* import platform.posix.* @OptIn(ExperimentalForeignApi::class) fun readData(fd: Int) { val buffer = ByteArray(1024) buffer.usePinned { pinned -> while (true) { val length = recv(fd, pinned.addressOf(0), buffer.size.convert(), 0).toInt() if (length <= 0) { break } // 现在 `buffer` 中包含了从 `recv()` 函数调用得到的原生数据. } } }

这里我们使用了服务函数 usePinned, 它会先固定一个对象, 然后执行一段代码, 最后无论是正常结束还是异常结束, 它都会将对象解除固定.

最终更新: 2024/12/17