映射来自 C 语言的函数指针——教程

This is the third part of the Mapping Kotlin and C tutorial series. Before proceeding, make sure you've completed the previous steps.

First step Mapping primitive data types from C
Second step Mapping struct and union types from C
Third step Mapping function pointers
Fourth step 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 function pointers are visible from Kotlin and examine advanced C interop-related use cases of Kotlin/Native and multiplatform Gradle builds.

在本教程中会:

映射 C 中的函数指针类型

理解在 Kotlin 与 C 之间进行映射的方式是,我们来声明两个函数:一个接收函数指针作为参数,另一个返回一个函数指针。

In the first part of the series of the series, you've already created a C library with the necessary files. For this step, update the declarations in the interop.def file after the --- separator:


---

int myFun(int i) {
  return i+1;
}

typedef int  (*MyFun)(int);

void accept_fun(MyFun f) {
  f(42);
}

MyFun supply_fun() {
  return myFun;
}

interop.def 文件提供了在 IDE 中编译、运行或打开应用程序所需的一切。

探查为 C 库生成的 Kotlin API

Let's see how C function pointers are mapped into Kotlin/Native and update your project:

  1. In src/nativeMain/kotlin, update your hello.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!")
    
        accept_fun(/* fix me*/)
        val useMe = supply_fun()
    }
    
  2. Use IntelliJ IDEA's Go to declaration command (Cmd + B/Ctrl + B) to navigate to the following generated API for C functions:

    fun myFun(i: kotlin.Int): kotlin.Int
    fun accept_fun(f: kotlinx.cinterop.CPointer kotlin.Int>>? /* from: interop.MyFun? */)
    fun supply_fun(): kotlinx.cinterop.CPointer kotlin.Int>>? /* from: interop.MyFun? */
    

As you can see, C function pointers are represented in Kotlin using CPointer>. The accept_fun() function takes an optional function pointer as a parameter, while supply_fun() returns a function pointer.

CFunction<(Int) -> Int> represents the function signature, and CPointer>? represents a nullable function pointer. There is an invoke operator extension function available for all CPointer> types, allowing you to call function pointers as if they were regular Kotlin functions.

将 Kotlin 函数作为 C 函数指针传递

是时候尝试在 Kotlin 代码中使用 C 函数了。调用 accept_fun() 函数并传递 C 函数指针到一个 Kotlin lambda 表达式:

import interop.*
import kotlinx.cinterop.staticCFunction
import kotlinx.cinterop.ExperimentalForeignApi

@OptIn(ExperimentalForeignApi::class)
fun myFun() {
    accept_fun(staticCFunction { it + 1 })
}

该调用使用 Kotlin/Native 中的 staticCFunction {} 辅助函数将一个 Kotlin lambda 函数包装为 C 函数指针。它只能是非绑定的以及没有发生变量捕捉的 lambda functions。举例来说,它不能捕获函数中的局部变量,只能捕获全局可见的声明。

Ensure that the function doesn't throw any exceptions. 在 staticCFunction {} 中抛出异常会导致非确定性的副作用。

在 Kotlin 中使用 C 函数指针

接下来是调用 supply_fun() 返回的 C 函数指针:

import interop.*
import kotlinx.cinterop.ExperimentalForeignApi
import kotlinx.cinterop.invoke

@OptIn(ExperimentalForeignApi::class)
fun myFun2() {
    val functionFromC = supply_fun() ?: error("No function is returned")

    functionFromC(42)
}

Kotlin 将函数指针返回类型转换为一个可空的 CPointer 对象。需要首先显式检测 null 值,这就是为什么上述代码中使用了 Elvis 操作符cinterop 工具让你可以像调用常规 Kotlin 函数一样调用 C 函数指针:functionFromC(42)

Update Kotlin code

Now that you've seen all the definitions, try to use them in your project. hello.kt 文件中的代码最终看起来会是这样的:

import interop.*
import kotlinx.cinterop.ExperimentalForeignApi
import kotlinx.cinterop.invoke
import kotlinx.cinterop.staticCFunction

@OptIn(ExperimentalForeignApi::class)
fun main() {
    println("Hello Kotlin/Native!")

    val cFunctionPointer = staticCFunction { it + 1 }
    accept_fun(cFunctionPointer)

    val funFromC = supply_fun() ?: error("No function is returned")
    funFromC(42)
}

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 strings are mapped between Kotlin and C:

Proceed to the next part

See also

Learn more in the Interoperability with C documentation that covers more advanced scenarios.