Skip to content

Cからの関数ポインタのマッピング – チュートリアル

これは「KotlinとCのマッピング」チュートリアルシリーズの第3部です。次に進む前に、前のステップを完了していることを確認してください。

第1ステップ Cからのプリミティブデータ型のマッピング
第2ステップ Cからの構造体および共用体型のマッピング
第3ステップ Cからの関数ポインタのマッピング
第4ステップ Cからの文字列のマッピング

Cライブラリのインポートはベータ段階です。cinteropツールによってCライブラリから生成されたすべてのKotlin宣言には、@ExperimentalForeignApiアノテーションが付与されている必要があります。

Kotlin/Nativeに同梱されているネイティブプラットフォームライブラリ(Foundation、UIKit、POSIXなど)では、一部のAPIでのみオプトインが必要です。

KotlinからどのC関数ポインタが見えるかを確認し、Kotlin/Nativeとマルチプラットフォーム Gradleビルドの高度なC相互運用(interop)に関するユースケースを調べましょう。

このチュートリアルでは、以下の内容を学びます:

Cからの関数ポインタ型のマッピング

KotlinとCの間のマッピングを理解するために、2つの関数を宣言してみましょう。1つは関数ポインタをパラメータとして受け取り、もう1つは関数ポインタを返す関数です。

シリーズの第1部で、必要なファイルを含むCライブラリをすでに作成しました。このステップでは、---セパレータの後にあるinterop.defファイルの宣言を更新します:

c

---

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の確認

C関数ポインタがKotlin/Nativeにどのようにマッピングされるかを確認し、プロジェクトを更新しましょう:

  1. src/nativeMain/kotlinにある、前のチュートリアルで作成したhello.ktファイルを以下の内容で更新します:

    kotlin
    import interop.*
    import kotlinx.cinterop.ExperimentalForeignApi
    
    @OptIn(ExperimentalForeignApi::class)
    fun main() {
        println("Hello Kotlin/Native!")
       
        accept_fun(/* fix me*/)
        val useMe = supply_fun()
    }
  2. IntelliJ IDEAのGo to declarationコマンド(/)を使用して、C関数用に生成された以下のAPIに移動します:

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

ご覧の通り、C関数ポインタはKotlinではCPointer<CFunction<...>>を使用して表現されます。accept_fun()関数はオプション(nullable)の関数ポインタをパラメータとして取り、supply_fun()は関数ポインタを返します。

CFunction<(Int) -> Int>は関数のシグネチャを表し、CPointer<CFunction<...>>?はnullableな関数ポインタを表します。すべてのCPointer<CFunction<...>>型には、.invoke()演算子の拡張関数が用意されており、これによって関数ポインタを通常のKotlin関数であるかのように呼び出すことができます。

Kotlin関数をC関数ポインタとして渡す

KotlinコードからC関数を使ってみましょう。accept_fun()関数を呼び出し、C関数ポインタとしてKotlinラムダを渡します:

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

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

この呼び出しでは、Kotlin/NativeのstaticCFunction {}ヘルパー関数を使用して、Kotlinラムダ関数をC関数ポインタにラップしています。これには、バインドされていない、キャプチャを行わない(non-capturing)ラムダ関数のみを使用できます。例えば、関数内のローカル変数をキャプチャすることはできず、グローバルにアクセス可能な宣言のみをキャプチャできます。

関数が例外をスローしないように注意してください。staticCFunction {}から例外をスローすると、非決定的な副作用が発生します。

KotlinからC関数ポインタを使用する

次のステップは、supply_fun()の呼び出しから返されたC関数ポインタを呼び出すことです:

kotlin
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は、関数ポインタの戻り値をnullableなCPointer<CFunction<>>オブジェクトに変換します。最初に明示的にnullチェックを行う必要があり、そのため上記のコードではエルビス演算子が使用されています。cinteropツールを使用すると、C関数ポインタを通常のKotlin関数の呼び出し(functionFromC(42))として呼び出すことができます。

Kotlinコードの更新

すべての定義を確認したので、プロジェクトでそれらを使用してみましょう。 hello.ktファイルのコードは以下のようになります:

kotlin
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<Int, Int> { it + 1 }
    accept_fun(cFunctionPointer)

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

すべてが期待通りに動作することを確認するために、IDEで runDebugExecutable<YourTargetName> Gradleタスクを実行するか、ターミナルでコンソールコマンドを使用します。この例では以下の通りです:

bash
./gradlew runDebugExecutableMacosArm64

次のステップ

シリーズの次のパートでは、KotlinとCの間で文字列がどのようにマッピングされるかを学びます:

次のパートへ進む

関連項目

より高度なシナリオをカバーしているCとの相互運用性のドキュメントで詳細を確認してください。