映射来自 C 语言的结构与联合类型——教程
This is the second part of the Mapping Kotlin and C tutorial series. Before proceeding, make sure you've completed the previous step.
Mapping primitive data types from C
Mapping struct and union types from C
Mapping function pointers
Mapping strings from C
The C libraries import is Experimental. All Kotlin declarations generated by the cinterop tool from C libraries should have the
@ExperimentalForeignApi
annotation.Native platform libraries shipped with Kotlin/Native (like Foundation, UIKit, and POSIX) require opt-in only for some APIs.
{style="warning"}
Let's explore which C struct and union declarations are visible from Kotlin and examine advanced C interop-related use cases of Kotlin/Native and multiplatform Gradle builds.
在本教程中可以学到
映射 C 语言的结构与联合类型
To understand how Kotlin maps struct and union types, let's declare them in C and examine how they are represented in Kotlin.
在之前的教程中,你已经用必要的文件创建了一个 C 库。
对于这一步,请更新 interop.def
文件中 ---
分隔符之后的声明:
---
typedef struct {
int a;
double b;
} MyStruct;
void struct_by_value(MyStruct s) {}
void struct_by_pointer(MyStruct* s) {}
typedef union {
int a;
MyStruct b;
float c;
} MyUnion;
void union_by_value(MyUnion u) {}
void union_by_pointer(MyUnion* u) {}
interop.def
文件提供了在 IDE 中编译、运行或打开应用程序所需的一切。
探查为 C 库生成的 Kotlin API
Let's see how C struct and union types are mapped into Kotlin/Native and update your project:
In
src/nativeMain/kotlin
, update yourhello.kt
file from the previous tutorial with the following content:import interop.* import kotlinx.cinterop.ExperimentalForeignApi @OptIn(ExperimentalForeignApi::class) fun main() { println("Hello Kotlin/Native!") struct_by_value(/* fix me*/) struct_by_pointer(/* fix me*/) union_by_value(/* fix me*/) union_by_pointer(/* fix me*/) }
To avoid compiler errors, add interoperability to the build process. For that, update your
build.gradle(.kts)
build file with the following content:
【Kotlin】
kotlin {
macosArm64("native") { // macOS on Apple Silicon
// macosX64("native") { // macOS on x86_64 platforms
// linuxArm64("native") { // Linux on ARM64 platforms
// linuxX64("native") { // Linux on x86_64 platforms
// mingwX64("native") { // on Windows
val main by compilations.getting
val interop by main.cinterops.creating {
definitionFile.set(project.file("src/nativeInterop/cinterop/interop.def"))
}
binaries {
executable()
}
}
}
【Groovy】
kotlin {
macosArm64("native") { // Apple Silicon macOS
// macosX64("native") { // macOS on x86_64 platforms
// linuxArm64("native") { // Linux on ARM64 platforms
// linuxX64("native") { // Linux on x86_64 platforms
// mingwX64("native") { // Windows
compilations.main.cinterops {
interop {
definitionFile = project.file('src/nativeInterop/cinterop/interop.def')
}
}
binaries {
executable()
}
}
}
Use IntelliJ IDEA's Go to declaration command (
Cmd + B /Ctrl + B ) to navigate to the following generated API for C functions, struct, and union:fun struct_by_value(s: kotlinx.cinterop.CValue
) fun struct_by_pointer(s: kotlinx.cinterop.CValuesRef ?) fun union_by_value(u: kotlinx.cinterop.CValue ) fun union_by_pointer(u: kotlinx.cinterop.CValuesRef ?)
从技术上讲,在 Kotlin 看来结构与联合类型之间没有区别。 The cinterop tool generates Kotlin types for both struct and union C declarations.
The generated API includes fully qualified package names for CValue
and CValuesRef
, reflecting their location
in kotlinx.cinterop
. CValue
表示一个值类型的结构体参数,而 CValuesRef
用于传递一个结构体或联合体的指针。
在 Kotlin 中使用结构与联合类型
Using C struct and union types from Kotlin is straightforward thanks to the generated API. The only question is how to create new instances of these types.
我们来看一看生成的函数,它将 MyStruct
与 MyUnion
作为参数。值类型参数表示为 kotlinx.cinterop.CValue
,而指针类型参数使用 kotlinx.cinterop.CValuesRef
。
Kotlin 提供了一种便利的 API 来创建及处理这些类型。我们来探索如何在实践中使用它。
创建一个 CValue<T>
CValue
类型用来传递一个值类型的参数到 C 函数调用。使用 cValue
函数来创建 CValue
实例。该函数需要一个带接收者的 lambda 函数字面值来就地初始化底层 C 类型。该函数的声明如下所示:
fun cValue(initialize: T.() -> Unit): CValue
以下是如何使用 cValue
并传递值类型参数:
import interop.*
import kotlinx.cinterop.ExperimentalForeignApi
import kotlinx.cinterop.cValue
@OptIn(ExperimentalForeignApi::class)
fun callValue() {
val cStruct = cValue {
a = 42
b = 3.14
}
struct_by_value(cStruct)
val cUnion = cValue {
b.a = 5
b.b = 2.7182
}
union_by_value(cUnion)
}
使用 CValuesRef<T> 创建结构体与联合体
CValuesRef
类型用于在 Kotlin 中将指针类型的参数传递给 C 函数。如需在原生内存中分配 MyStruct
与
MyUnion
,请使用 kotlinx.cinterop.NativePlacement
类型的以下扩展函数:
fun alloc(): T
NativePlacement
代表原生内存,类似于 malloc
与 free
函数。 这里有几个
NativePlacement
的实现:
- 全局的实现是
kotlinx.cinterop.nativeHeap
,但你必须在使用过后调用nativeHeap.free()
来释放内存。 A safer alternative is
memScoped()
, which creates a short-lived memory scope where all allocations are automatically freed at the end of the block:fun
memScoped(block: kotlinx.cinterop.MemScope.() -> R): R
使用 memScoped()
时,调用带指针类型参数的函数的代码看起来会是这样:
import interop.*
import kotlinx.cinterop.ExperimentalForeignApi
import kotlinx.cinterop.memScoped
import kotlinx.cinterop.alloc
import kotlinx.cinterop.ptr
@OptIn(ExperimentalForeignApi::class)
fun callRef() {
memScoped {
val cStruct = alloc()
cStruct.a = 42
cStruct.b = 3.14
struct_by_pointer(cStruct.ptr)
val cUnion = alloc()
cUnion.b.a = 5
cUnion.b.b = 2.7182
union_by_pointer(cUnion.ptr)
}
}
这里的 ptr
扩展属性可在 memScoped {}
代码块中使用,它将 MyStruct
和 MyUnion
实例转换为原生指针。
Since memory is managed inside the memScoped {}
block, it's automatically freed at the end of the block.
Avoid using pointers outside of this scope to prevent accessing deallocated memory. If you need longer-lived allocations
(for example, for caching in a C library), consider using Arena()
or nativeHeap
.
在 CValue<T> 与 CValuesRef<T> 之间转换
有时候,需要在一个函数调用中将结构体作为值传递,然后在另一个调用中将同一个结构体作为引用传递。
To do this, you'll need a NativePlacement
, 但首先,我们来看看如何将 CValue
转换为一个指针:
import interop.*
import kotlinx.cinterop.ExperimentalForeignApi
import kotlinx.cinterop.cValue
import kotlinx.cinterop.memScoped
@OptIn(ExperimentalForeignApi::class)
fun callMix_ref() {
val cStruct = cValue {
a = 42
b = 3.14
}
memScoped {
struct_by_pointer(cStruct.ptr)
}
}
这里再次说明,来自 memScoped {}
的 ptr
扩展属性将 MyStruct
实例转换为原生指针。
这些指针只在 memScoped {}
块内有效。
如需将指针转换回值类型变量,请调用 .readValue()
扩展函数:
import interop.*
import kotlinx.cinterop.alloc
import kotlinx.cinterop.ExperimentalForeignApi
import kotlinx.cinterop.memScoped
import kotlinx.cinterop.readValue
@OptIn(ExperimentalForeignApi::class)
fun callMix_value() {
memScoped {
val cStruct = alloc()
cStruct.a = 42
cStruct.b = 3.14
struct_by_value(cStruct.readValue())
}
}
Update Kotlin code
Now that you've learned how to use C declarations in Kotlin code, try to use them in your project.
hello.kt
文件中的最终代码看起来会是这样:
import interop.*
import kotlinx.cinterop.alloc
import kotlinx.cinterop.cValue
import kotlinx.cinterop.memScoped
import kotlinx.cinterop.ptr
import kotlinx.cinterop.readValue
import kotlinx.cinterop.ExperimentalForeignApi
@OptIn(ExperimentalForeignApi::class)
fun main() {
println("Hello Kotlin/Native!")
val cUnion = cValue {
b.a = 5
b.b = 2.7182
}
memScoped {
union_by_value(cUnion)
union_by_pointer(cUnion.ptr)
}
memScoped {
val cStruct = alloc {
a = 42
b = 3.14
}
struct_by_value(cStruct.readValue())
struct_by_pointer(cStruct.ptr)
}
}
To verify that everything works as expected, run the runDebugExecutableNative
Gradle task in your IDE
or use the following command to run the code:
./gradlew runDebugExecutableNative
接下来
In the next part of the series, you'll learn how function pointers are mapped between Kotlin and C:
See also
Learn more in the Interoperability with C documentation that covers more advanced scenarios.