Cからの文字列のマッピング – チュートリアル
これは、KotlinとCのマッピングチュートリアルシリーズの最終パートです。先に進む前に、前の手順を完了していることを確認してください。
Cからのプリミティブデータ型のマッピング
Cからの構造体と共用体のマッピング
関数ポインタのマッピング
Cからの文字列のマッピング
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ライブラリを既に作成しています。このステップでは:
-
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()
関数を使用していることを確認してください。 -
---
セパレーターの後に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にどのようにマッピングされるかを見てみましょう。
-
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*/)
} -
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の宣言では、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エンコードされた文字列を変換します
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との相互運用性ドキュメントで詳細をご覧ください。