Skip to content

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

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

First step C言語からのプリミティブデータ型マッピング
Second step C言語からの構造体と共用体マッピング
Third step 関数ポインタのマッピング
Fourth step 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ライブラリをすでに作成しています。このステップでは:

  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のGo to declaration (宣言へ移動)コマンド(/)を使用して、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>>?として定義されています。 この型はヌル許容(nullable)であるため、引数値として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エンコードされた文字列を変換する

C言語の文字列バイトをKotlinの文字列として受け取る

今回は、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関数はバイト配列にデータを書き込みます。もう1つの拡張関数であるByteArray.decodeToString()は、UTF-8エンコーディングを仮定して、バイト配列をKotlinの文字列に変換します。

Kotlinコードの更新

C言語の宣言をKotlinコードで使用する方法を学んだので、プロジェクトでそれらを使用してみましょう。 最終的な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)
}

すべてが期待通りに動作することを確認するには、IDErunDebugExecutableNative Gradleタスクを実行するか、以下のコマンドを使用してコードを実行します。

bash
./gradlew runDebugExecutableNative

次のステップ

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