Skip to content

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

このチュートリアルは、KotlinとCのマッピングシリーズの最終パートです。始める前に、以前のステップを完了していることを確認してください。

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

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

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

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

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

C文字列の操作

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

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

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

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

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

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

    kotlin
    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の宣言へ移動コマンド(/)を使用して、C関数用に生成された次のAPIに移動します。

    kotlin
    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>>?として定義されています。 この型はヌル許容なので、引数値としてnullを渡すことができます。

Kotlin文字列をCに渡す

KotlinからAPIを使用してみましょう。最初にpass_string()関数を呼び出します。

kotlin
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文字列に変換します。

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文字列に変換するためのいくつかの拡張関数が、エンコーディングに応じて提供されています。

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は成功し、提供されたバッファが十分な大きさであったことを意味すると仮定しましょう。

kotlin
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ファイルは次のようになります。

kotlin
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タスクを実行するか、次のコマンドを使用してコードを実行します。

bash
./gradlew runDebugExecutableNative

次のステップ

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