Spec-Zone.ru › Kotlin 1.8

Картирование структур и объединений типов из C – учебник

Это вторая запись в серии. Первый учебник в серии – Картирование примитивных типов данных из C. Также есть учебники Картирование указателей на функции из C и Картирование строк из C.

В учебнике вы узнаете:

  • Как отображаются структуры и объединения типов

  • Как использовать структуры и объединения типов из Kotlin

Картирование структур и объединений типов 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 и запустить его.

Просмотр сгенерированных 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 и многоплатформенных сборок с Gradle.

Сначала создайте папку проекта. Все пути в этом руководстве будут относительными к этой папке. Иногда для добавления новых файлов необходимо создать отсутствующие каталоги.

Используйте следующий файл сборки Gradle:

plugins {
    kotlin("multiplatform") version "1.8.0"
}

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 = "7.3"
  distributionType = Wrapper.DistributionType.BIN
}
plugins {
    id 'org.jetbrains.kotlin.multiplatform' version '1.8.0'
}

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 = '7.3'
  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!")
  
  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-взаимодействию

Использование типов struct и union из Kotlin

Легко использовать сгенерированные оболочки для типов C struct и union из Kotlin. Благодаря сгенерированным свойствам, их использование в коде Kotlin кажется естественным. Единственный вопрос пока заключается в том, как создать новый экземпляр этих классов. Как видно из объявлений MyStruct и MyUnion, их конструкторы требуют NativePtr. Конечно, вы не хотите вручную работать с указателями. Вместо этого можно использовать Kotlin API для создания этих объектов за нас.

Давайте рассмотрим сгенерированные функции, которые принимают наши MyStruct и MyUnion в качестве параметров. Вы видите, что параметры по значению представлены как kotlinx.cinterop.CValue<T>. А для параметра указателя с типом вы видите kotlinx.cinterop.CValuesRef<T>. Kotlin предоставляет API для лёгкой работы с обоими типами, давайте попробуем.

Создание CValue

Тип 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)
}

Создание struct и union как CValuesRef

Тип 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 и CValuesRef

Конечно, есть случаи, когда вам нужно передать структуру как значение одному вызову, а затем – как ссылку другому. Это тоже возможно в 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 в своём коде, вы готовы попробовать это на реальном примере. Исправим код и посмотрим, как он работает, вызвав задачу runDebugExecutableNative Gradle в IDE или используя следующую команду консоли:

./gradlew 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 охватывает более сложные сценарии взаимодействия.

Последнее изменение: 10 января 2023
Преобразование примитивных типов данных из C – руководство Преобразование указателей на функции из C – руководство

© 2010–2023 JetBrains s.r.o. and Kotlin Programming Language contributors
Licensed under the Apache License, Version 2.0.
https://kotlinlang.org/docs/mapping-struct-union-types-from-c.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API