メインコンテンツまでスキップ

Cからの文字列のマッピング – チュートリアル

備考

これは、KotlinとCのマッピングチュートリアルシリーズの最終パートです。先に進む前に、前の手順を完了していることを確認してください。

First stepCからのプリミティブデータ型のマッピング
Second stepCからの構造体と共用体のマッピング
Third step関数ポインタのマッピング
Fourth stepCからの文字列のマッピング

注意

CライブラリのインポートはExperimentalです。cinteropツールによってCライブラリから生成されたすべてのKotlinの宣言には、@ExperimentalForeignApiアノテーションが必要です。

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

このシリーズの最終パートでは、Kotlin/NativeでCの文字列を扱う方法を見ていきましょう。

このチュートリアルでは、次の方法を学びます。

Cの文字列の操作

Cには専用の文字列型がありません。メソッドのシグネチャまたはドキュメントは、特定のコンテキストで与えられたchar *がCの文字列を表すかどうかを識別するのに役立ちます。

C言語の文字列はnull終端されているため、文字列の終わりを示すために、バイトシーケンスの末尾にトレーリングゼロ文字\0が追加されます。通常、UTF-8エンコード文字列が使用されます。UTF-8エンコーディングは可変幅の文字を使用し、ASCIIとの下位互換性があります。Kotlin/NativeはデフォルトでUTF-8文字エンコーディングを使用します。

文字列がKotlinとCの間でどのようにマッピングされるかを理解するために、最初にライブラリヘッダーを作成します。シリーズの最初のパートでは、必要なファイルを含むCライブラリを既に作成しています。このステップでは:

  1. Cの文字列を扱う次の関数宣言でlib.hファイルを更新します。

    #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()関数を使用していることを確認してください。

  2. ---セパレーターの後にinterop.defファイルの宣言を更新します。

    ---

    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にどのようにマッピングされるかを見てみましょう。

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

    import 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*/)
    }
  2. IntelliJ IDEAの宣言へ移動 コマンド(Cmd + B/Ctrl + B)を使用して、C関数のために生成された次のAPIに移動します。

    fun 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の宣言では、strCValuesRef<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エンコードされた文字列を変換します

KotlinからCの文字列のバイト列を受け取る

今回は、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関数はバイト配列にデータを入力します。別の拡張関数ByteArray.decodeToString()は、UTF-8エンコーディングを想定して、バイト配列をKotlinの文字列に変換します。

Kotlinコードを更新する

KotlinコードでCの宣言を使用する方法を学んだので、プロジェクトでそれらを使用してみてください。 最後の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との相互運用性ドキュメントで詳細をご覧ください。