C言語からの文字列マッピング – チュートリアル
これはKotlinとCのデータマッピングチュートリアルシリーズの最終パートです。始める前に、以前のステップを完了していることを確認してください。
C言語からのプリミティブデータ型マッピング
C言語からの構造体と共用体マッピング
関数ポインタのマッピング
C言語からの文字列マッピング
DANGER
CライブラリのインポートはExperimental (実験的)です。cinteropツールによってCライブラリから生成されるすべてのKotlin宣言には、@ExperimentalForeignApi
アノテーションが必要です。
Kotlin/Nativeに同梱されているネイティブプラットフォームライブラリ(Foundation、UIKit、POSIXなど)は、一部のAPIでのみオプトインが必要です。
シリーズの最終パートでは、Kotlin/NativeでC言語の文字列をどのように扱うかを見ていきましょう。
このチュートリアルでは、以下の方法を学びます。
C言語の文字列の操作
C言語には専用の文字列型がありません。特定のコンテキストでchar *
がC言語の文字列を表すかどうかは、メソッドのシグネチャやドキュメントから判断できます。
C言語の文字列はヌル終端(null-terminated)されており、文字列の終わりを示すためにバイトシーケンスの末尾に終端ゼロ文字\0
が追加されます。通常、UTF-8エンコードされた文字列が使用されます。 UTF-8エンコーディングは可変幅文字を使用し、ASCIIとの後方互換性があります。 Kotlin/NativeはデフォルトでUTF-8文字エンコーディングを使用します。
KotlinとC言語の間で文字列がどのようにマッピングされるかを理解するために、まずライブラリヘッダーを作成します。 シリーズの最初のパートで、必要なファイルを含むCライブラリをすでに作成しています。このステップでは:
lib.h
ファイルを、C言語の文字列を扱う以下の関数宣言で更新します。c#ifndef LIB2_H_INCLUDED #define LIB2_H_INCLUDED void pass_string(char* str); char* return_string(); int copy_string(char* str, int size); #endif
この例は、C言語で文字列を渡したり受け取ったりする一般的な方法を示しています。
return_string()
関数の戻り値は慎重に扱ってください。返されたchar*
を解放するために正しいfree()
関数を使用していることを確認してください。interop.def
ファイルの---
セパレータの後に宣言を更新します。c--- void pass_string(char* str) { } char* return_string() { return "C string"; } int copy_string(char* str, int size) { *str++ = 'C'; *str++ = ' '; *str++ = 'K'; *str++ = '/'; *str++ = 'N'; *str++ = 0; return 0; }
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!") pass_string(/*fix me*/) val useMe = return_string() val useMe2 = copy_string(/*fix me*/) }
IntelliJ IDEAのGo to declaration (宣言へ移動)コマンド(/)を使用して、C言語関数用に生成された以下のAPIに移動します。
kotlinfun pass_string(str: kotlinx.cinterop.CValuesRef<kotlinx.cinterop.ByteVarOf<kotlin.Byte> /* from: kotlinx.cinterop.ByteVar */>?) fun return_string(): kotlinx.cinterop.CPointer<kotlinx.cinterop.ByteVarOf<kotlin.Byte> /* from: kotlinx.cinterop.ByteVar */>? fun copy_string(str: kotlinx.cinterop.CValuesRef<kotlinx.cinterop.ByteVarOf<kotlin.Byte> /* from: kotlinx.cinterop.ByteVar */>?, size: kotlin.Int): kotlin.Int
これらの宣言はわかりやすいものです。Kotlinでは、C言語のchar *
ポインタは、引数としてstr: CValuesRef<ByteVarOf>?
に、戻り値の型としてCPointer<ByteVarOf>?
にマッピングされます。Kotlinはchar
型をkotlin.Byte
として表現します。これは通常8ビットの符号付き値だからです。
生成されたKotlin宣言では、str
はCValuesRef<ByteVarOf<Byte>>?
として定義されています。 この型はヌル許容(nullable)であるため、引数値としてnull
を渡すことができます。
Kotlinの文字列をCに渡す
次に、KotlinからAPIを使ってみましょう。まずpass_string()
関数を呼び出します。
import interop.*
import kotlinx.cinterop.ExperimentalForeignApi
import kotlinx.cinterop.cstr
@OptIn(ExperimentalForeignApi::class)
fun passStringToC() {
val str = "This is a Kotlin string"
pass_string(str.cstr)
}
Kotlinの文字列をCに渡すのは、String.cstr
拡張プロパティのおかげで簡単です。 UTF-16文字が関与するケースには、String.wcstr
プロパティもあります。
KotlinでC言語の文字列を読み取る
今度は、return_string()
関数から返されたchar *
を受け取り、それをKotlinの文字列に変換します。
import interop.*
import kotlinx.cinterop.ExperimentalForeignApi
import kotlinx.cinterop.toKString
@OptIn(ExperimentalForeignApi::class)
fun passStringToC() {
val stringFromC = return_string()?.toKString()
println("Returned from C: $stringFromC")
}
ここでは、.toKString()
拡張関数が、return_string()
関数から返されたC言語の文字列をKotlinの文字列に変換します。
Kotlinは、C言語のchar *
文字列をKotlinの文字列に変換するためのいくつかの拡張関数を提供しています。これらはエンコーディングによって異なります。
fun CPointer<ByteVarOf<Byte>>.toKString(): String // UTF-8文字列のための標準関数
fun CPointer<ByteVarOf<Byte>>.toKStringFromUtf8(): String // UTF-8文字列を明示的に変換する
fun CPointer<ShortVarOf<Short>>.toKStringFromUtf16(): String // UTF-16エンコードされた文字列を変換する
fun CPointer<IntVarOf<Int>>.toKStringFromUtf32(): String // UTF-32エンコードされた文字列を変換する
C言語の文字列バイトをKotlinの文字列として受け取る
今回は、copy_string()
C関数を使用して、指定されたバッファにC言語の文字列を書き込みます。これは2つの引数を取ります。文字列を書き込むメモリ位置へのポインタと、許容されるバッファサイズです。
この関数は、成功または失敗を示す何かを返す必要があります。0
が成功、かつ提供されたバッファが十分な大きさであったことを意味すると仮定しましょう。
import interop.*
import kotlinx.cinterop.ExperimentalForeignApi
import kotlinx.cinterop.addressOf
import kotlinx.cinterop.usePinned
@OptIn(ExperimentalForeignApi::class)
fun sendString() {
val buf = ByteArray(255)
buf.usePinned { pinned ->
if (copy_string(pinned.addressOf(0), buf.size - 1) != 0) {
throw Error("Failed to read string from C")
}
}
val copiedStringFromC = buf.decodeToString()
println("Message from C: $copiedStringFromC")
}
ここでは、まずネイティブポインタがC関数に渡されます。.usePinned
拡張関数は、バイト配列のネイティブメモリアドレスを一時的に固定します。C関数はバイト配列にデータを書き込みます。もう1つの拡張関数であるByteArray.decodeToString()
は、UTF-8エンコーディングを仮定して、バイト配列をKotlinの文字列に変換します。
Kotlinコードの更新
C言語の宣言をKotlinコードで使用する方法を学んだので、プロジェクトでそれらを使用してみましょう。 最終的なhello.kt
ファイルは次のようになるでしょう。
import interop.*
import kotlinx.cinterop.*
@OptIn(ExperimentalForeignApi::class)
fun main() {
println("Hello Kotlin/Native!")
val str = "This is a Kotlin string"
pass_string(str.cstr)
val useMe = return_string()?.toKString() ?: error("null pointer returned")
println(useMe)
val copyFromC = ByteArray(255).usePinned { pinned ->
val useMe2 = copy_string(pinned.addressOf(0), pinned.get().size - 1)
if (useMe2 != 0) throw Error("Failed to read a string from C")
pinned.get().decodeToString()
}
println(copyFromC)
}
すべてが期待通りに動作することを確認するには、IDEでrunDebugExecutableNative
Gradleタスクを実行するか、以下のコマンドを使用してコードを実行します。
./gradlew runDebugExecutableNative
次のステップ
より高度なシナリオをカバーするC言語との相互運用ドキュメントで詳細を学びましょう。