Skip to content

Cのstruct型とunion型をマッピングする – チュートリアル

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

最初のステップ Cのプリミティブデータ型をマッピングする
2番目のステップ Cのstruct型とunion型をマッピングする
3番目のステップ 関数ポインタをマッピングする
4番目のステップ Cの文字列をマッピングする

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

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

KotlinからどのようなCのstructおよびunion宣言が可視であるかを探り、Kotlin/NativeおよびマルチプラットフォームGradleビルドにおける高度なC interop関連のユースケースを調べましょう。

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

Cのstruct型とunion型のマッピング

Kotlinがstruct型とunion型をどのようにマッピングするかを理解するために、Cでそれらを宣言し、Kotlinでどのように表現されるかを調べましょう。

前のチュートリアルで、必要なファイルを含むCライブラリを既に作成しました。このステップでは、---区切り文字の後にinterop.defファイルの宣言を更新します。

c

---

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を検査する

Cのstruct型とunion型がKotlin/Nativeにどのようにマッピングされるかを見て、プロジェクトを更新しましょう。

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

    kotlin
    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*/)
    }
  2. コンパイラエラーを避けるために、ビルドプロセスに相互運用性を追加します。そのためには、build.gradle(.kts)ビルドファイルを以下の内容で更新します。

    kotlin
    kotlin {
        macosArm64("native") {    // Apple Silicon搭載macOS
        // macosX64("native") {   // x86_64プラットフォーム上のmacOS
        // linuxArm64("native") { // ARM64プラットフォーム上のLinux
        // linuxX64("native") {   // x86_64プラットフォーム上のLinux
        // mingwX64("native") {   // 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") {   // x86_64プラットフォーム上のmacOS
        // linuxArm64("native") { // ARM64プラットフォーム上のLinux
        // linuxX64("native") {   // x86_64プラットフォーム上のLinux
        // mingwX64("native") {   // Windows
            compilations.main.cinterops {
                interop {   
                    definitionFile = project.file('src/nativeInterop/cinterop/interop.def')
                }
            }
        
            binaries {
                executable()
            }
        }
    }
  3. IntelliJ IDEAの宣言へ移動コマンド(/)を使用して、C関数、struct、union用に生成された以下のAPIに移動します。

    kotlin
    fun struct_by_value(s: kotlinx.cinterop.CValue<interop.MyStruct>)
    fun struct_by_pointer(s: kotlinx.cinterop.CValuesRef<interop.MyStruct>?)
    
    fun union_by_value(u: kotlinx.cinterop.CValue<interop.MyUnion>)
    fun union_by_pointer(u: kotlinx.cinterop.CValuesRef<interop.MyUnion>?)

技術的には、Kotlin側ではstruct型とunion型に違いはありません。cinteropツールは、structとunionの両方のC宣言に対してKotlin型を生成します。

生成されたAPIには、CValue<T>CValuesRef<T>の完全修飾パッケージ名が含まれており、それらがkotlinx.cinteropに位置することを反映しています。CValue<T>は値渡し構造体パラメータを表し、CValuesRef<T>?は構造体またはunionへのポインタを渡すために使用されます。

Kotlinからstruct型とunion型を使用する

生成されたAPIのおかげで、Cのstruct型とunion型をKotlinから使用することは簡単です。唯一の問題は、これらの型の新しいインスタンスをどのように作成するかです。

MyStructMyUnionをパラメータとして受け取る生成された関数を見てみましょう。値渡しパラメータはkotlinx.cinterop.CValue<T>として表現され、ポインタ型のパラメータはkotlinx.cinterop.CValuesRef<T>?を使用します。

Kotlinはこれらの型を作成し、操作するための便利なAPIを提供しています。実際にどのように使用するかを見てみましょう。

CValue<T>を作成する

CValue<T>型は、値渡しパラメータをC関数呼び出しに渡すために使用されます。cValue関数を使用してCValue<T>インスタンスを作成します。この関数は、基となるC型をインプレースで初期化するために、レシーバ付きラムダ関数を必要とします。この関数は次のように宣言されています。

kotlin
fun <reified T : CStructVar> cValue(initialize: T.() -> Unit): CValue<T>

以下にcValueの使用方法と、値渡しパラメータの渡し方を示します。

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

@OptIn(ExperimentalForeignApi::class)
fun callValue() {

    val cStruct = cValue<MyStruct> {
        a = 42
        b = 3.14
    }
    struct_by_value(cStruct)

    val cUnion = cValue<MyUnion> {
        b.a = 5
        b.b = 2.7182
    }

    union_by_value(cUnion)
}

structとunionをCValuesRef<T>として作成する

CValuesRef<T>型は、KotlinでC関数のポインタ型パラメータを渡すために使用されます。ネイティブメモリにMyStructMyUnionを割り当てるには、kotlinx.cinterop.NativePlacement型に対する以下の拡張関数を使用します。

kotlin
fun <reified T : kotlinx.cinterop.CVariable> alloc(): T

NativePlacementは、mallocfreeに似た関数を持つネイティブメモリを表します。NativePlacementにはいくつかの実装があります。

  • グローバルな実装はkotlinx.cinterop.nativeHeapですが、使用後にメモリを解放するためにnativeHeap.free()を呼び出す必要があります。

  • より安全な代替手段はmemScoped()で、これは短命なメモリスコープを作成し、そのブロックの終わりにすべての割り当てが自動的に解放されます。

    kotlin
    fun <R> memScoped(block: kotlinx.cinterop.MemScope.() -> R): R

memScoped()を使用すると、ポインタを持つ関数を呼び出すコードは次のようになります。

kotlin
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<MyStruct>()
        cStruct.a = 42
        cStruct.b = 3.14

        struct_by_pointer(cStruct.ptr)

        val cUnion = alloc<MyUnion>()
        cUnion.b.a = 5
        cUnion.b.b = 2.7182

        union_by_pointer(cUnion.ptr)
    }
}

ここで、memScoped {}ブロック内で利用可能なptr拡張プロパティは、MyStructMyUnionのインスタンスをネイティブポインタに変換します。

メモリはmemScoped {}ブロック内で管理されるため、ブロックの終わりに自動的に解放されます。解放されたメモリへのアクセスを防ぐため、このスコープ外でポインタを使用することは避けてください。より寿命の長い割り当て(たとえば、Cライブラリでのキャッシングなど)が必要な場合は、Arena()またはnativeHeapの使用を検討してください。

CValue<T>とCValuesRef<T>の変換

ある関数呼び出しではstructを値として渡し、別の関数呼び出しでは同じstructを参照として渡す必要がある場合があります。

これを行うにはNativePlacementが必要ですが、まずCValue<T>がどのようにポインタに変換されるかを見てみましょう。

kotlin
import interop.*
import kotlinx.cinterop.ExperimentalForeignApi
import kotlinx.cinterop.cValue
import kotlinx.cinterop.memScoped

@OptIn(ExperimentalForeignApi::class)
fun callMix_ref() {
    val cStruct = cValue<MyStruct> {
        a = 42
        b = 3.14
    }

    memScoped {
        struct_by_pointer(cStruct.ptr)
    }
}

ここでも、memScoped {}ptr拡張プロパティは、MyStructインスタンスをネイティブポインタに変換します。これらのポインタはmemScoped {}ブロック内でのみ有効です。

ポインタを値渡し変数に戻すには、.readValue()拡張関数を呼び出します。

kotlin
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<MyStruct>()
        cStruct.a = 42
        cStruct.b = 3.14

        struct_by_value(cStruct.readValue())
    }
}

Kotlinコードの更新

C宣言をKotlinコードで使用する方法を学んだので、プロジェクトでそれらを使用してみてください。hello.ktファイルの最終的なコードは次のようになります。

kotlin
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<MyUnion> {
        b.a = 5
        b.b = 2.7182
    }

    memScoped {
        union_by_value(cUnion)
        union_by_pointer(cUnion.ptr)
    }

    memScoped {
        val cStruct = alloc<MyStruct> {
            a = 42
            b = 3.14
        }

        struct_by_value(cStruct.readValue())
        struct_by_pointer(cStruct.ptr)
    }
}

すべてが期待どおりに動作することを確認するには、IDEでrunDebugExecutableNative Gradleタスクを実行するか、以下のコマンドを使用してコードを実行します。

bash
./gradlew runDebugExecutableNative

次のステップ

シリーズの次のパートでは、KotlinとCの間で関数ポインタがどのようにマッピングされるかを学びます。

次のパートに進む

関連項目

より高度なシナリオをカバーするCとの相互運用性のドキュメントで詳細を学びましょう。