Skip to content

Cとの相互運用

DANGER

Cライブラリのインポートは実験的機能です。

cinteropツールによってCライブラリから生成されるすべてのKotlin宣言には、

@ExperimentalForeignApi アノテーションが付与されます。

Kotlin/Nativeに同梱されているネイティブプラットフォームライブラリ(Foundation、UIKit、POSIXなど)は、

一部のAPIでのみオプトインが必要です。

このドキュメントでは、KotlinとCの相互運用の一般的な側面について説明します。Kotlin/Nativeにはcinteropツールが付属しており、これを使用すると、外部のCライブラリと連携するために必要なすべてを迅速に生成できます。

このツールはCヘッダーを解析し、Cの型、関数、定数をKotlinに直接マッピングします。生成されたスタブはIDEにインポートでき、コード補完とナビゲーションが可能になります。

TIP

KotlinはObjective-Cとの相互運用も提供します。Objective-Cライブラリもcinteropツールを介してインポートされます。

詳細については、Swift/Objective-Cの相互運用を参照してください。

プロジェクトのセットアップ

Cライブラリを使用する必要があるプロジェクトで作業する場合の一般的なワークフローは次のとおりです。

  1. 定義ファイルを作成し、構成します。これはcinteropツールがKotlinのバインディングに何を含めるべきかを記述します。
  2. Gradleビルドファイルを設定して、ビルドプロセスにcinteropを含めます。
  3. プロジェクトをコンパイルして実行し、最終的な実行可能ファイルを生成します。

NOTE

実践的な経験を積むには、C interopを使用したアプリの作成チュートリアルを完了してください。

多くの場合、Cライブラリとのカスタム相互運用を構成する必要はありません。代わりに、プラットフォームライブラリと呼ばれるプラットフォーム標準化されたバインディングで利用可能なAPIを使用できます。たとえば、Linux/macOSプラットフォームのPOSIX、WindowsプラットフォームのWin32、またはmacOS/iOSのAppleフレームワークは、この方法で利用可能です。

バインディング

基本的な相互運用型

サポートされているすべてのC型には、Kotlinに対応する表現があります。

  • 符号付き整数型、符号なし整数型、および浮動小数点型は、同じ幅のKotlinの対応型にマッピングされます。
  • ポインタと配列はCPointer<T>?にマッピングされます。
  • Enumは、ヒューリスティックおよび定義ファイルの設定に応じて、Kotlinのenumまたは整数値のいずれかにマッピングできます。
  • 構造体と共用体は、ドット表記(例: someStructInstance.field1)でフィールドにアクセスできる型にマッピングされます。
  • typedeftypealiasとして表現されます。

また、すべてのC型には、その型のlvalueを表すKotlin型があります。これは、単純な変更不能な自己完結型の値ではなく、メモリに存在する値を意味します。C++のリファレンスを類似の概念と考えてください。構造体(および構造体へのtypedef)の場合、この表現が主要なものであり、構造体自体と同じ名前を持ちます。Kotlinのenumの場合、${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>はnull許容ではありませんが、CPointer<T>?はnull許容です。この型の値は、?:?.!!など、nullの処理に関連するすべてのKotlin演算をサポートします。

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

配列もCPointer<T>にマッピングされるため、インデックスによる値へのアクセスに[]演算子をサポートします。

kotlin
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型のlvalueを返します。逆の操作は.ptrで、lvalueを受け取り、それへのポインタを返します。

void*COpaquePointerにマッピングされます。これは、他のポインタ型のスーパタイプである特別なポインタ型です。したがって、C関数がvoid*を取る場合、Kotlinバインディングは任意のCPointerを受け入れます。

ポインタ(COpaquePointerを含む)のキャストは、.reinterpret<T>で行うことができます。例:

kotlin
import kotlinx.cinterop.*

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

または:

kotlin
import kotlinx.cinterop.*

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

Cと同様に、これらの.reinterpretキャストは安全ではなく、アプリケーションで微妙なメモリ問題を引き起こす可能性があります。

また、CPointer<T>?Longの間で利用可能な安全でないキャストがあり、.toLong().toCPointer<T>()拡張メソッドによって提供されます。

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

TIP

結果の型がコンテキストから既知の場合、型推論のおかげで型引数を省略できます。

メモリ割り当て

ネイティブメモリは、NativePlacementインターフェースを使用して割り当てることができます。例:

kotlin
import kotlinx.cinterop.*

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

または:

kotlin
import kotlinx.cinterop.*

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

最も論理的なプレースメントはnativeHeapオブジェクト内です。これはmallocによるネイティブメモリの割り当てに対応し、割り当てられたメモリを解放するための追加の.free()操作を提供します。

kotlin
import kotlinx.cinterop.*

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

nativeHeapはメモリを手動で解放する必要があります。ただし、字句スコープに寿命が紐付けられたメモリを割り当てることは、しばしば有用です。このようなメモリが自動的に解放されると便利です。

これに対処するため、memScoped { }を使用できます。波括弧内では、一時的なプレースメントが暗黙のレシーバーとして利用できるため、allocおよびallocArrayでネイティブメモリを割り当てることができ、割り当てられたメモリはスコープを離れた後に自動的に解放されます。

たとえば、ポインタパラメータを介して値を返すC関数は次のように使用できます。

kotlin
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> typeにマッピングされますが、C関数のポインタ型パラメータはCValuesRef<T>にマッピングされます。CPointer<T>をこのようなパラメータの値として渡す場合、そのままC関数に渡されます。ただし、ポインタの代わりに値のシーケンスを渡すこともできます。この場合、シーケンスは「値渡し」され、つまり、C関数はシーケンスの一時的なコピーへのポインタを受け取ります。これは関数が戻るまでのみ有効です。

ポインタパラメータのCValuesRef<T>表現は、明示的なネイティブメモリ割り当てなしにC配列リテラルをサポートするように設計されています。C値の変更不能な自己完結型シーケンスを構築するために、次のメソッドが提供されています。

  • ${type}Array.toCValues()typeはKotlinのプリミティブ型)
  • Array<CPointer<T>?>.toCValues()List<CPointer<T>?>.toCValues()
  • cValuesOf(vararg elements: ${type})typeはプリミティブまたはポインタ)

例:

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

foo(cValuesOf(1, 2, 3), 3)

文字列

他のポインタとは異なり、const char*型のパラメータはKotlinのStringとして表現されます。そのため、任意のKotlin文字列をC文字列を期待するバインディングに渡すことができます。

Kotlin文字列とC文字列の間を手動で変換するためのツールもいくつか利用できます。

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

ポインタを取得するには、.cstrをネイティブメモリに割り当てる必要があります。例:

kotlin
val cString = kotlinString.cstr.getPointer(nativeHeap)

すべての場合において、C文字列はUTF-8でエンコードされていると想定されています。

自動変換をスキップし、バインディングで生ポインタが使用されるようにするには、noStringConversionプロパティ.defファイルに追加します。

c
noStringConversion = LoadCursorA LoadCursorW

これにより、CPointer<ByteVar>型の任意の値がconst char*型の引数として渡すことができます。Kotlin文字列を渡す必要がある場合、次のようなコードを使用できます。

kotlin
import kotlinx.cinterop.*

@OptIn(kotlinx.cinterop.ExperimentalForeignApi::class)
memScoped {
    LoadCursorA(null, "cursor.bmp".cstr.ptr)  // for ASCII or UTF-8 version
    LoadCursorW(null, "cursor.bmp".wcstr.ptr) // for UTF-16 version
}

スコープローカルポインタ

memScoped {}内で利用可能なCValues<T>.ptr拡張プロパティを使用して、CValues<T>インスタンスのC表現のスコープ安定ポインタを作成することができます。これにより、特定のMemScopeに寿命が紐付けられたCポインタを必要とするAPIを使用できます。例:

kotlin
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関数が構造体/共用体Tを値として受け取ったり返したりする場合、対応する引数型または戻り値型はCValue<T>として表現されます。

CValue<T>は不透明な型であるため、構造体フィールドには適切なKotlinプロパティでアクセスできません。APIが構造体を不透明なハンドルとして使用している場合はこれで問題ありません。ただし、フィールドアクセスが必要な場合は、次の変換メソッドが利用可能です。

  • fun T.readValue(): CValue<T>は(lvalueの)TCValue<T>に変換します。したがって、CValue<T>を構築するには、Tを割り当てて、値を設定し、CValue<T>に変換することができます。
  • CValue<T>.useContents(block: T.() -> R): Rは、CValue<T>を一時的にメモリに格納し、その配置された値Tをレシーバーとして渡されたラムダを実行します。したがって、単一のフィールドを読み取るには、次のコードを使用できます。
kotlin
val fieldValue = structValue.useContents { field }

コールバック

Kotlin関数をC関数へのポインタに変換するには、staticCFunction(::kotlinFunction)を使用します。関数参照の代わりにラムダを指定することも可能です。関数またはラムダは値をキャプチャしてはいけません。

ユーザーデータをコールバックに渡す

しばしばC APIは、コールバックに何らかのユーザーデータを渡すことを許可します。そのようなデータは通常、コールバックを構成する際にユーザーによって提供されます。それはvoid*としてC関数に渡されたり(または構造体に書き込まれたり)します。しかし、Kotlinオブジェクトへの参照をCに直接渡すことはできません。そのため、コールバックを構成する前にラップし、Cの世界を安全にKotlinからKotlinへ移動できるように、コールバック自体でアンラップする必要があります。このようなラッピングはStableRefクラスで可能です。

参照をラップするには:

kotlin
import kotlinx.cinterop.*

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

ここで、voidPtrCOpaquePointerであり、C関数に渡すことができます。

参照をアンラップするには:

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

ここで、kotlinReferenceは元のラップされた参照です。

作成されたStableRefは、メモリリークを防ぐため、最終的に.dispose()メソッドを使用して手動で破棄する必要があります。

kotlin
stableRef.dispose()

その後、無効になり、voidPtrをアンラップできなくなります。

マクロ

定数に展開されるすべてのCマクロは、Kotlinプロパティとして表現されます。

パラメータのないマクロは、コンパイラが型を推論できる場合にサポートされます。

c
int foo(int);
#define FOO foo(42)

この場合、FOOはKotlinで利用できます。

他のマクロをサポートするには、サポートされている宣言でラップして手動で公開できます。たとえば、関数のようなマクロFOOは、ライブラリにカスタム宣言を追加することで、関数foo()として公開できます。

c
headers = library/base.h

---

static inline int foo(int arg) {
    return FOO(arg);
}

ポータビリティ

Cライブラリには、longsize_tなど、プラットフォーム依存の型の関数パラメータや構造体フィールドを持つ場合があります。Kotlin自体は暗黙の整数キャストやCスタイルの整数キャスト(例: (size_t) intValue)を提供しないため、そのような場合にポータブルなコードを書きやすくするために、convertメソッドが提供されています。

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

ここで、type1type2のそれぞれは、符号付きまたは符号なしのいずれかの整数型である必要があります。

.convert<${type}>は、typeに応じて、.toByte.toShort.toInt.toLong.toUByte.toUShort.toUInt、または.toULongメソッドのいずれかと同じセマンティクスを持ちます。

convertの使用例:

kotlin
import kotlinx.cinterop.*
import platform.posix.*

@OptIn(ExperimentalForeignApi::class)
fun zeroMemory(buffer: COpaquePointer, size: Int) {
    memset(buffer, 0, size.convert<size_t>())
}

また、型パラメータは自動的に推論されるため、場合によっては省略できます。

オブジェクトのピン止め

Kotlinオブジェクトはピン止めできます。つまり、メモリ内の位置がアンピンされるまで安定していることが保証され、そのようなオブジェクトの内部データへのポインタをC関数に渡すことができます。

いくつかのアプローチがあります。

  • usePinnedサービス関数を使用します。これはオブジェクトをピン止めし、ブロックを実行し、通常パスと例外パスでピン止めを解除します。
kotlin
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
            }
            // Now `buffer` has raw data obtained from the `recv()` call.
        }
    }
}

ここで、pinnedは特殊な型Pinned<T>のオブジェクトです。これは、ピン止めされた配列本体のアドレスを取得できるaddressOfのような便利な拡張機能を提供します。

  • refTo()は、内部的に同様の機能を持っていますが、特定のケースでは、ボイラープレートコードを減らすのに役立つ場合があります。
kotlin
import kotlinx.cinterop.*
import platform.posix.*

@OptIn(ExperimentalForeignApi::class)
fun readData(fd: Int) {
    val buffer = ByteArray(1024)
    while (true) {
        val length = recv(fd, buffer.refTo(0), buffer.size.convert(), 0).toInt()

        if (length <= 0) {
            break
        }
        // Now `buffer` has raw data obtained from the `recv()` call.
    }
}

ここで、buffer.refTo(0)CValuesRef型であり、recv()関数に入る前に配列をピン止めし、そのゼロ番目の要素のアドレスを関数に渡し、終了後に配列のピン止めを解除します。

順方向宣言

順方向宣言をインポートするには、cnamesパッケージを使用します。たとえば、library.packageを持つCライブラリで宣言されたcstructName順方向宣言をインポートするには、特別な順方向宣言パッケージimport cnames.structs.cstructNameを使用します。

構造体の順方向宣言を持つライブラリと、別のパッケージに実際の実装を持つライブラリの2つのcinteropライブラリを考えてみましょう。

C
// First C library
#include <stdio.h>

struct ForwardDeclaredStruct;

void consumeStruct(struct ForwardDeclaredStruct* s) {
    printf("Struct consumed
");
}
C
// Second C library
// Header:
#include <stdlib.h>

struct ForwardDeclaredStruct {
    int data;
};

// Implementation:
struct ForwardDeclaredStruct* produceStruct() {
    struct ForwardDeclaredStruct* s = malloc(sizeof(struct ForwardDeclaredStruct));
    s->data = 42;
    return s;
}

2つのライブラリ間でオブジェクトを転送するには、Kotlinコードで明示的なasキャストを使用します。

kotlin
// Kotlin code:
fun test() {
    consumeStruct(produceStruct() as CPointer<cnames.structs.ForwardDeclaredStruct>)
}

次のステップ

次のチュートリアルを完了して、KotlinとCの間で型、関数、定数がどのようにマッピングされるかを学びましょう。