Spec-Zone.ru › Kotlin 1.6

Картирование типов struct и union из C – учебник

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

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

  • Как отображаются типы struct и union

  • Как использовать типы struct и union из Kotlin

Картирование типов struct и union языка C

Лучший способ понять сопоставление между Kotlin и C — это попробовать небольшой пример. Мы объявим struct и union на языке 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. В этом учебнике мы рассмотрим расширенные варианты использования Kotlin/Native, связанные с взаимодействием с C, и многоплатформенные сборки с Gradle.

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

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

plugins {
    kotlin("multiplatform") version "1.6.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.6.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!")
  
  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

END_OF_DOCUMENT_MARKER

Использование структур и объединений типов из Kotlin

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

Давайте рассмотрим сгенерированные функции, которые принимают наши 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)
}

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

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

Последнее изменение: 07 апреля 2022
Преобразование примитивных типов данных из C — учебник Преобразование указателей на функции из 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-struct-union-types-from-c.html

Spec-Zone.ru

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