Картирование строк из C
| Последнее обновление | 15 апреля 2019 г. |
Это последний учебник в серии. Первый учебник серии — Картирование примитивных типов данных из 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. Самый быстрый способ попробовать сопоставление API C — разместить все объявления 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 и запустить его.
Просмотр сгенерированных API Kotlin для библиотеки C
Хотя можно использовать командную строку, как непосредственно, так и комбинируя ее со скриптовыми файлами (например, sh или bat), следует отметить, что это не масштабируется для больших проектов, имеющих сотни файлов и библиотек. В таком случае лучше использовать компилятор Kotlin/Native с системой сборки, так как это поможет загружать и кешировать двоичные файлы и библиотеки компилятора Kotlin/Native с транзитивными зависимостями, а также запускать компилятор и тесты. Kotlin/Native может использовать систему сборки Gradle через плагин kotlin-multiplatform.
Основы настройки совместимого с IDE проекта с Gradle мы рассмотрели в учебнике Базовое Kotlin/Native приложение. Пожалуйста, ознакомьтесь с ним, если ищете подробные первые шаги и инструкции по началу нового проекта Kotlin/Native и его открытию в IntelliJ IDEA. В этом учебнике мы рассмотрим расширенные методы взаимодействия с C, связанные с Kotlin/Native и многоплатформенными сборками с Gradle.
Сначала создадим папку проекта. Все пути в этом учебнике будут относительными к этой папке. Иногда недостающие каталоги необходимо создать, прежде чем добавлять новые файлы.
Мы будем использовать следующий файл сборки Gradle build.gradle build.gradle.kts с указанным ниже содержимым:
plugins {
id 'org.jetbrains.kotlin.multiplatform' version '1.3.21'
}
repositories {
mavenCentral()
}
kotlin {
macosX64("native") {
compilations.main.cinterops {
interop
}
binaries {
executable()
}
}
}
wrapper {
gradleVersion = "5.3.1"
distributionType = "ALL"
}
plugins {
id 'org.jetbrains.kotlin.multiplatform' version '1.3.21'
}
repositories {
mavenCentral()
}
kotlin {
linuxX64("native") {
compilations.main.cinterops {
interop
}
binaries {
executable()
}
}
}
wrapper {
gradleVersion = "5.3.1"
distributionType = "ALL"
}
plugins {
id 'org.jetbrains.kotlin.multiplatform' version '1.3.21'
}
repositories {
mavenCentral()
}
kotlin {
mingwX64("native") {
compilations.main.cinterops {
interop
}
binaries {
executable()
}
}
}
wrapper {
gradleVersion = "5.3.1"
distributionType = "ALL"
}
plugins {
kotlin("multiplatform") version "1.3.21"
}
repositories {
mavenCentral()
}
kotlin {
macosX64("native") {
val main by compilations.getting
val interop by main.cinterops.creating
binaries {
executable()
}
}
}
tasks.withType<Wrapper> {
gradleVersion = "5.3.1"
distributionType = Wrapper.DistributionType.ALL
}
plugins {
kotlin("multiplatform") version "1.3.21"
}
repositories {
mavenCentral()
}
kotlin {
linuxX64("native") {
val main by compilations.getting
val interop by main.cinterops.creating
binaries {
executable()
}
}
}
tasks.withType<Wrapper> {
gradleVersion = "5.3.1"
distributionType = Wrapper.DistributionType.ALL
}
plugins {
kotlin("multiplatform") version "1.3.21"
}
repositories {
mavenCentral()
}
kotlin {
mingwX64("native") {
val main by compilations.getting
val interop by main.cinterops.creating
binaries {
executable()
}
}
}
tasks.withType<Wrapper> {
gradleVersion = "5.3.1"
distributionType = Wrapper.DistributionType.ALL
}
Подготовленные исходные файлы проекта можно загрузить напрямую с GitHub. GitHub. GitHub. GitHub. GitHub. GitHub.
Файл проекта настраивает взаимодействие с 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 для случаев, когда нам нужны 16-битные символы 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.toKString()
println("Message from C: $copiedStringFromC")
}
Прежде всего, нам нужен родной указатель, который нужно передать функции C. Мы используем функцию расширения usePinned, чтобы временно закрепить родной адрес памяти массива байтов. Функция C заполняет массив байтов данными. Мы используем другую функцию расширения ByteArray.toKString(), чтобы преобразовать массив байтов в Kotlin String, предполагая кодировку UTF-8.
Исправление кода
Мы рассмотрели все определения, и пришло время исправить код. Запустите задачу runDebugExecutableNative Gradle в IDE или используйте следующую команду для запуска кода:
./gradlew runDebugExecutableNative
./gradlew runDebugExecutableNative
gradlew.bat 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().toKString()
}
println(copyFromC)
}
Следующие шаги
Мы продолжаем изучать другие типы языка C и их представление в Kotlin/Native в наших других учебниках:
- Преобразование примитивных типов данных из C
- Преобразование структур и объединений из C
- Преобразование указателей на функции из C
Документация C Interop охватывает более сложные сценарии взаимодействия.
© 2010–2020 JetBrains s.r.o. and Kotlin Programming Language contributors
Licensed under the Apache License, Version 2.0.
https://kotlinlang.org/docs/tutorials/native/mapping-strings-from-c.html