Cの構造体(struct)および共用体(union)型のマッピング – チュートリアル
これはKotlinとCのマッピングチュートリアルシリーズの第2部です。次に進む前に、前のステップを完了していることを確認してください。
Cのプリミティブデータ型のマッピング
Cの構造体(struct)および共用体(union)型のマッピング
Cの関数ポインタのマッピング
Cの文字列のマッピング
Cライブラリのインポートはベータ版です。cinteropツールによってCライブラリから生成されたすべてのKotlin宣言には、
@ExperimentalForeignApiアノテーションが付与されます。Kotlin/Nativeに同梱されているネイティブプラットフォームライブラリ(Foundation、UIKit、POSIXなど)では、一部のAPIについてのみオプトインが必要です。
どのCの構造体(struct)および共用体(union)の宣言がKotlinから見えるかを確認し、Kotlin/Nativeの高度なC相互運用(interop)に関連するユースケースや、マルチプラットフォームのGradleビルドについて見ていきましょう。
このチュートリアルでは、以下の内容を学びます:
Cの構造体および共用体型のマッピング
Kotlinが構造体(struct)および共用体(union)型をどのようにマッピングするかを理解するために、それらをCで宣言し、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の確認
Cの構造体および共用体型がKotlin/Nativeにどのようにマッピングされるかを確認し、プロジェクトを更新しましょう:
src/nativeMain/kotlinにある 前のチュートリアル のhello.ktファイルを、以下の内容で更新します:kotlinimport 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*/) }コンパイルエラーを避けるために、ビルドプロセスに相互運用性を追加します。そのために、
build.gradle(.kts)ファイルを以下の内容で更新してください:kotlinkotlin { macosArm64() // Apple Silicon搭載のmacOS // linuxArm64() // ARM64プラットフォームのLinux // linuxX64() // x86_64プラットフォームのLinux // mingwX64() // Windows targets.withType<KotlinNativeTarget>().configureEach { val main by compilations.getting val interop by main.cinterops.creating { definitionFile.set(project.file("src/nativeInterop/cinterop/interop.def")) } binaries { executable() } } }groovykotlin { macosArm64() // Apple Silicon搭載のmacOS // linuxArm64() // ARM64プラットフォームのLinux // linuxX64() // x86_64プラットフォームのLinux // mingwX64() // Windows targets.withType(KotlinNativeTarget).configureEach { compilations.main.cinterops { interop { definitionFile = project.file('src/nativeInterop/cinterop/interop.def') } } binaries { executable() } } }IntelliJ IDEAの宣言へ移動 (Go to declaration)コマンド(/)を使用して、Cの関数、構造体、共用体に対して生成された以下のAPIに移動します:
kotlinfun 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側では構造体と共用体の型に違いはありません。cinteropツールは、Cの構造体と共用体の両方の宣言に対してKotlinの型を生成します。
生成されたAPIには、CValue<T> と CValuesRef<T> の完全修飾パッケージ名が含まれており、これらが kotlinx.cinterop に配置されていることを示しています。CValue<T> は値渡しの構造体パラメータを表し、CValuesRef<T>? は構造体または共用体へのポインタを渡すために使用されます。
Kotlinから構造体および共用体型を使用する
生成されたAPIのおかげで、KotlinからCの構造体および共用体型を使用するのは簡単です。唯一の問題は、これらの型の新しいインスタンスをどのように作成するかです。
MyStruct と MyUnion をパラメータとして受け取る生成された関数を見てみましょう。値渡しのパラメータは kotlinx.cinterop.CValue<T> として表現され、ポインタ型のパラメータは kotlinx.cinterop.CValuesRef<T>? を使用します。
Kotlinは、これらの型を作成し操作するための便利なAPIを提供しています。実際にどのように使用するか見ていきましょう。
CValue<T> の作成
CValue<T> 型は、C関数の呼び出しに値渡しのパラメータを渡すために使用されます。CValue<T> インスタンスを作成するには、cValue 関数を使用します。この関数は、基盤となるC型をその場で初期化するために、レシーバ付きラムダ関数を必要とします。関数は次のように宣言されています:
fun <reified T : CStructVar> cValue(initialize: T.() -> Unit): CValue<T>cValue を使用して値渡しのパラメータを渡す方法は以下の通りです:
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)
}CValuesRef<T> として構造体と共用体を作成する
CValuesRef<T> 型は、KotlinでC関数のポインタ型パラメータを渡すために使用されます。ネイティブメモリに MyStruct および MyUnion を割り当てるには、kotlinx.cinterop.NativePlacement 型の以下の拡張関数を使用します:
fun <reified T : kotlinx.cinterop.CVariable> alloc(): TNativePlacement は、malloc や free に似た関数を持つネイティブメモリを表します。NativePlacement にはいくつかの実装があります:
グローバルな実装は
kotlinx.cinterop.nativeHeapですが、使用後にメモリを解放するためにnativeHeap.free()を呼び出す必要があります。より安全な代替手段は
memScoped()です。これは短命なメモリ・スコープを作成し、ブロックの最後ですべての割り当てが自動的に解放されます:kotlinfun <R> 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<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 拡張プロパティは、MyStruct および MyUnion インスタンスをネイティブポインタに変換します。
メモリは memScoped {} ブロック内で管理されるため、ブロックの最後で自動的に解放されます。解放されたメモリへのアクセスを防ぐため、このスコープ外でポインタを使用しないでください。より長寿命の割り当てが必要な場合(例えば、Cライブラリでのキャッシュ用など)は、Arena() または nativeHeap の使用を検討してください。
CValue<T> と CValuesRef<T> の間の変換
ある関数呼び出しでは構造体を値として渡し、別の呼び出しでは同じ構造体を参照として渡す必要がある場合があります。
これを行うには NativePlacement が必要ですが、まずは CValue<T> をポインタに変換する方法を見てみましょう:
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() 拡張関数を呼び出します:
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コードの更新
KotlinコードでCの宣言を使用する方法を学んだので、プロジェクトでそれらを使用してみましょう。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<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で runDebugExecutable<YourTargetName> Gradleタスクを実行するか、ターミナルでコンソールコマンドを使用します(この例の場合):
./gradlew runDebugExecutableMacosArm64次のステップ
シリーズの次のパートでは、KotlinとCの間で関数ポインタがどのようにマッピングされるかを学びます:
関連項目
より高度なシナリオをカバーしている Cとの相互運用性 (Interoperability with C) ドキュメントで詳細を学ぶことができます。
