Преобразование строк из C – учебник
Это последний учебник в серии. Первый учебник в серии — Преобразование примитивных типов данных из C. Также есть учебники Преобразование структур и объединений из C и Преобразование указателей на функции из C.
В этом учебнике вы узнаете, как работать со строками C в Kotlin/Native. Вы узнаете, как:
Работа со строками C
В языке C нет специального типа для строк. Разработчик узнает из сигнатуры метода или документации, означает ли данное char * C-строку в контексте. Строки на языке C имеют нулевую терминацию; в конце последовательности байтов добавляется конечный нулевой символ \0, чтобы обозначить конец строки. Обычно используются строки, закодированные в кодировке UTF-8. Кодировка UTF-8 использует символы переменной длины и обратной совместима с ASCII. Kotlin/Native по умолчанию использует кодировку символов UTF-8.
Лучший способ понять сопоставление между C и Kotlin — попробовать это на небольшом примере. Создайте небольшой заголовочный файл библиотеки для этого. Сначала создайте lib.h файл со следующим объявлением функций, которые работают со строками 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(..).
Kotlin/Native поставляется с инструментом cinterop; инструмент генерирует связи между языком C и Kotlin. Он использует файл .def для указания C-библиотеки для импорта. Более подробные сведения об этом см. в учебнике Взаимодействие с C-библиотеками. Самый быстрый способ попробовать сопоставление C-API — поместить все объявления C в файл interop.def, не создавая каких-либо файлов .h или .c вообще. Затем поместите объявления C в файл interop.def после специальной строки-разделителя ---.
headers = lib.h
---
void pass_string(char* str) {
}
char* return_string() {
return "C stirng";
}
int copy_string(char* str, int size) {
*str++ = 'C';
*str++ = ' ';
*str++ = 'K';
*str++ = '/';
*str++ = 'N';
*str++ = 0;
return 0;
}
Файла interop.def достаточно для компиляции и запуска приложения или открытия его в IDE. Теперь пришло время создать файлы проекта, открыть проект в IntelliJ IDEA и запустить его.
Проверка сгенерированных Kotlin-API для C-библиотеки
Хотя можно использовать командную строку, как напрямую, так и в сочетании с файлом сценария (например, файлом .sh или .bat), такой подход не масштабируется для крупных проектов, имеющих сотни файлов и библиотек. Тогда лучше использовать компилятор Kotlin/Native с системой сборки, так как он помогает загружать и кэшировать двоичные файлы и библиотеки компилятора Kotlin/Native с транзитивными зависимостями и запускать компилятор и тесты. Kotlin/Native может использовать систему сборки Gradle через плагин kotlin-multiplatform.
Мы рассмотрели основы настройки IDE-совместимого проекта с Gradle в учебнике Базовое Kotlin/Native-приложение. Пожалуйста, ознакомьтесь с ним, если вам нужны подробные начальные шаги и инструкции по запуску нового Kotlin/Native-проекта и его открытию в IntelliJ IDEA. В этом учебнике мы рассмотрим расширенные C-взаимодействующие возможности Kotlin/Native и сборки multiplatform с Gradle.
Сначала создайте папку проекта. Все пути в этом учебнике будут относительными к этой папке. Иногда необходимо создать отсутствующие каталоги перед добавлением новых файлов.
Используйте следующий файл сборки Gradle:
plugins {
kotlin("multiplatform") version "1.7.20"
}
repositories {
mavenCentral()
}
kotlin {
linuxX64("native") { // on Linux
// macosX64("native") { // on x86_64 macOS
// macosArm64("native") { // on Apple Silicon macOS
// mingwX64("native") { // on Windows
val main by compilations.getting
val interop by main.cinterops.creating
binaries {
executable()
}
}
}
tasks.wrapper {
gradleVersion = "6.7.1"
distributionType = Wrapper.DistributionType.BIN
}
plugins {
id 'org.jetbrains.kotlin.multiplatform' version '1.7.20'
}
repositories {
mavenCentral()
}
kotlin {
linuxX64('native') { // on Linux
// macosX64("native") { // on x86_64 macOS
// macosArm64("native") { // on Apple Silicon macOS
// mingwX64('native') { // on Windows
compilations.main.cinterops {
interop
}
binaries {
executable()
}
}
}
wrapper {
gradleVersion = '6.7.1'
distributionType = 'BIN'
}
Файл проекта настраивает C-взаимодействие как дополнительный шаг сборки. Давайте переместим файл interop.def в каталог src/nativeInterop/cinterop. Gradle рекомендует использовать соглашения вместо конфигураций, например, ожидается, что исходные файлы будут находиться в папке src/nativeMain/kotlin. По умолчанию все символы из C импортируются в пакет interop, возможно, вы захотите импортировать весь пакет в наши файлы .kt. Ознакомьтесь с документацией плагина kotlin-multiplatform, чтобы узнать обо всех возможных способах его настройки.
Давайте создадим файл-заглушку src/nativeMain/kotlin/hello.kt со следующим содержимым, чтобы увидеть, как объявления C-строк видны из Kotlin:
import interop.*
fun main() {
println("Hello Kotlin/Native!")
pass_string(/*fix me*/)
val useMe = return_string()
val useMe2 = copy_string(/*fix me*/)
}
Теперь вы готовы открыть проект в IntelliJ IDEA и увидеть, как исправить пример проекта. Делая это, посмотрите, как C-строки отображаются в Kotlin/Native.
Строки в Kotlin
С помощью IntelliJ IDEA, функции Перейти к | Объявлению или ошибок компилятора, вы видите следующую сгенерированную API для функций C:
fun pass_string(str: CValuesRef<ByteVar /* = ByteVarOf<Byte> */>?) fun return_string(): CPointer<ByteVar /* = ByteVarOf<Byte> */>? fun copy_string(str: CValuesRef<ByteVar /* = ByteVarOf<Byte> */>?, size: Int): Int
Эти объявления выглядят понятно. Все указатели char * преобразуются в str: CValuesRef<ByteVar>? для параметров и в CPointer<ByteVar>? в типах возвращаемых значений. Kotlin преобразует тип char в тип kotlin.Byte, так как это обычно 8-битовое знаковое значение.
В сгенерированных Kotlin-объявлениях вы видите, что str представлено как CValuesRef<ByteVar/>?. Тип является необязательным, и вы можете просто передать Kotlin null в качестве значения параметра.
Передача Kotlin-строки в C
Давайте попробуем использовать API из Kotlin. Сначала вызовите pass_string.
fun passStringToC() {
val str = "this is a Kotlin String"
pass_string(str.cstr)
}
Передача Kotlin-строки в C проста, благодаря наличию String.cstr расширяемого свойства в Kotlin для нее. Также есть String.wcstr для случаев, когда вам нужны широкие UTF-16 символы.
Чтение C-строк в Kotlin
На этот раз вы получите возвращаемое char * из функции return_string и преобразуете его в Kotlin-строку. Для этого сделайте следующее в Kotlin:
fun passStringToC() {
val stringFromC = return_string()?.toKString()
println("Returned from C: $stringFromC")
}
Этот код использует расширяющую функцию toKString() выше. Пожалуйста, не пропустите функцию toString(). Функция toKString() имеет два перегруженных расширяемых метода в Kotlin:
fun CPointer<ByteVar>.toKString(): String fun CPointer<ShortVar>.toKString(): String
Первый расширяемый метод принимает char * как строку UTF-8 и преобразует её в строку. Второй метод делает то же самое, но для широких строк UTF-16.
Получение байтов C-строки из Kotlin
На этот раз мы попросим функцию C записать C-строку в заданный буфер. Функция называется copy_string. Она принимает указатель на место записи символов и допустимый размер буфера. Функция возвращает значение, указывающее на успешность или неудачу. Предположим, 0 означает успех, и предоставленный буфер был достаточно большим:
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() для преобразования массива байтов в Kotlin String, предполагая кодировку UTF-8.
Исправить код
Вы теперь ознакомились со всеми определениями, и пришло время исправить код. Запустите задачу Gradle runDebugExecutableNative в IDE или используйте следующую команду для запуска кода:
./gradlew runDebugExecutableNative
Код в окончательном файле hello.kt может выглядеть следующим образом:
import interop.*
import kotlinx.cinterop.*
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 string from C")
pinned.get().decodeToString()
}
println(copyFromC)
}
Следующие шаги
Продолжайте изучать другие типы языка C и их представление в Kotlin/Native в наших других учебниках:
Документация по C-взаимодействию охватывает более сложные сценарии взаимодействия.
© 2010–2022 JetBrains s.r.o. and Kotlin Programming Language contributors
Licensed under the Apache License, Version 2.0.
https://kotlinlang.org/docs/mapping-strings-from-c.html