Картирование структур и объединений из C
| Последнее обновление | 15 апреля 2019 г. |
Это второй пост в серии. Первый учебник в серии — Картирование примитивных типов данных из C. Также есть учебники Картирование указателей на функции из C и Картирование строк из C.
В этом учебнике мы узнаем:
Картирование структур и объединений C
Лучший способ понять отображение между Kotlin и C — это пример. Мы объявим структуру и объединение на языке C, чтобы увидеть, как они отображаются в Kotlin.
Kotlin/Native поставляется с инструментом cinterop, который генерирует связи между языком C и Kotlin. Он использует файл .def, чтобы указать библиотеку C для импорта. Более подробная информация приведена в учебнике Взаимодействие с библиотеками C.
В предыдущем учебнике мы создали файл lib.h. На этот раз мы добавим эти объявления непосредственно в файл interop.def, после разделительной строки ---.
---
typedef struct {
int a;
double b;
} MyStruct;
void struct_by_value(MyStruct s) {}
void struct_by_pointer(MyStruct* s) {}
typedef union {
int a;
MyStruct b;
float c;
} MyUnion;
void union_by_value(MyUnion u) {}
void union_by_pointer(MyUnion* u) {}
Файла 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!")
struct_by_value(/* fix me*/)
struct_by_pointer(/* fix me*/)
union_by_value(/* fix me*/)
union_by_pointer(/* fix me*/)
}
Теперь мы готовы открыть проект в IntelliJ IDEA и посмотреть, как исправить пример проекта. Делая это, мы рассмотрим, как примитивные типы C отображаются в Kotlin/Native.
Примитивные типы в Kotlin
С помощью IntelliJ IDEA (Перейти к определению) или ошибок компилятора мы видим сгенерированный API для наших функций C, struct, и union.
fun struct_by_value(s: CValue<MyStruct>)
fun struct_by_pointer(s: CValuesRef<MyStruct>?)
fun union_by_value(u: CValue<MyUnion>)
fun union_by_pointer(u: CValuesRef<MyUnion>?)
class MyStruct constructor(rawPtr: NativePtr /* = NativePtr */) : CStructVar {
var a: Int
var b: Double
companion object : CStructVar.Type
}
class MyUnion constructor(rawPtr: NativePtr /* = NativePtr */) : CStructVar {
var a: Int
val b: MyStruct
var c: Float
companion object : CStructVar.Type
}
Мы видим, что cinterop сгенерировал обёртки типов для наших типов struct и union. Для объявлений типов MyStruct и MyUnion в C у нас есть сгенерированные классы Kotlin MyStruct и MyUnion соответственно. Обёртки наследуют от базового класса CStructVar и объявляют все поля в виде свойств Kotlin. Используется CValue<T> для представления структуры параметра по значению и CValuesRef<T>? для представления передачи указателя на структуру или объединение.
Технически, нет разницы между типами struct и union в Kotlin. Следует отметить, что свойства a, b, и c класса MyUnion в Kotlin используют ту же область памяти для чтения/записи значения, как и union в языке C.
Более подробная информация и расширенные примеры использования представлены в
документации по C Interop
Использование структур и объединений из Kotlin
Легко использовать сгенерированные классы-обёртки для типов C struct и union из Kotlin. Благодаря сгенерированным свойствам их использование в коде Kotlin интуитивно понятно. Единственный вопрос на данный момент — как создать новый экземпляр этих классов. Как видно из объявлений MyStruct и MyUnion, их конструкторы требуют NativePtr. Конечно, мы не хотим вручную работать с указателями. Вместо этого, мы можем использовать API Kotlin для автоматического создания этих объектов.
Давайте посмотрим на сгенерированные функции, которые принимают наши типы MyStruct и MyUnion в качестве параметров. Мы видим, что параметры по значению представлены как kotlinx.cinterop.CValue<T>. А для параметров-указателей-типов мы видим kotlinx.cinterop.CValuesRef<T>. Kotlin предоставляет нам API для удобной работы с обоими типами, давайте попробуем.
Создание объекта CValue<T>
Тип CValue<T> используется для передачи параметров по значению в вызов C-функции. Мы используем функцию cValue для создания экземпляра объекта CValue<T>. Функция требует лямбда-функцию с получателем, чтобы инициализировать базовый тип C на месте. Функция объявлена следующим образом:
fun <reified T : CStructVar> cValue(initialize: T.() -> Unit): CValue<T>
Теперь давайте посмотрим, как использовать cValue и передавать параметры по значению:
fun callValue() {
val cStruct = cValue<MyStruct> {
a = 42
b = 3.14
}
struct_by_value(cStruct)
val cUnion = cValue<MyUnion> {
b.a = 5
b.b = 2.7182
}
union_by_value(cUnion)
}
Создание структуры и объединения как CValuesRef<T>
Тип CValuesRef<T> используется в Kotlin для передачи параметра-указателя-типа C-функции. Сначала нам нужен экземпляр классов MyStruct и MyUnion. В этот раз мы создаём их напрямую в родной памяти. Давайте воспользуемся
fun <reified T : kotlinx.cinterop.CVariable> alloc(): T
расширяющей функцией для типа kotlinx.cinterop.NativePlacement для этого.
NativePlacement представляет собой родную память с функциями, аналогичными malloc и free. Существует несколько реализаций NativePlacement. Глобальная реализация вызывается с помощью kotlinx.cinterop.nativeHeap и не забудьте вызвать функцию nativeHeap.free(..) для освобождения памяти после использования.
Другой вариант — использовать
fun <R> memScoped(block: kotlinx.cinterop.MemScope.() -> R): R
функцию. Она создаёт область кратковременного выделения памяти, и все выделения будут автоматически очищены в конце block.
Наш код для вызова функций с указателями будет выглядеть следующим образом:
fun callRef() {
memScoped {
val cStruct = alloc<MyStruct>()
cStruct.a = 42
cStruct.b = 3.14
struct_by_pointer(cStruct.ptr)
val cUnion = alloc<MyUnion>()
cUnion.b.a = 5
cUnion.b.b = 2.7182
union_by_pointer(cUnion.ptr)
}
}
Обратите внимание, мы используем свойство расширения ptr, которое происходит от типа получателя лямбда-выражения memScoped, чтобы преобразовать экземпляры MyStruct и MyUnion в родные указатели.
Классы MyStruct и MyUnion содержат указатель на родную память внизу. Память будет освобождена, когда завершится функция memScoped, что равносильно окончанию её block. Будьте внимательны, чтобы убедиться, что указатель не используется за пределами вызова memScoped. Мы можем использовать Arena() или nativeHeap для указателей, которые должны быть доступны дольше или кэшируются внутри библиотеки C.
Преобразование между CValue<T> и CValuesRef<T>
Конечно, есть случаи, когда нам нужно передать структуру как значение одному вызову, а затем передать ту же структуру как ссылку другому вызову. Это также возможно в Kotlin/Native. Здесь потребуется NativePlacement.
Давайте посмотрим, как CValue<T> сначала преобразуется в указатель:
fun callMix_ref() {
val cStruct = cValue<MyStruct> {
a = 42
b = 3.14
}
memScoped {
struct_by_pointer(cStruct.ptr)
}
}
Мы используем свойство расширения ptr, которое происходит от типа получателя лямбда-выражения memScoped, чтобы преобразовать экземпляры MyStruct и MyUnion в родные указатели. Эти указатели действительны только внутри блока memScoped.
Для обратного преобразования, чтобы преобразовать указатель в переменную по значению, мы вызываем функцию расширения readValue():
fun callMix_value() {
memScoped {
val cStruct = alloc<MyStruct>()
cStruct.a = 42
cStruct.b = 3.14
struct_by_value(cStruct.readValue())
}
}
Запуск кода
Теперь, когда мы узнали, как использовать объявления C в нашем коде, мы готовы попробовать это на реальном примере. Давайте исправим наш код и посмотрим, как он выполняется, вызвав задачу Gradle runDebugExecutableNative в IDE или используя следующую команду консоли:
./gradlew runDebugExecutableNative
./gradlew runDebugExecutableNative
gradlew.bat runDebugExecutableNative
Конечный код в файле hello.kt может выглядеть так:
import interop.*
import kotlinx.cinterop.alloc
import kotlinx.cinterop.cValue
import kotlinx.cinterop.memScoped
import kotlinx.cinterop.ptr
import kotlinx.cinterop.readValue
fun main() {
println("Hello Kotlin/Native!")
val cUnion = cValue<MyUnion> {
b.a = 5
b.b = 2.7182
}
memScoped {
union_by_value(cUnion)
union_by_pointer(cUnion.ptr)
}
memScoped {
val cStruct = alloc<MyStruct> {
a = 42
b = 3.14
}
struct_by_value(cStruct.readValue())
struct_by_pointer(cStruct.ptr)
}
}
Следующие шаги
Присоединяйтесь к нам, чтобы продолжить изучение типов языка 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-struct-union-types-from-c.html