Spec-Zone.ru › Kotlin 1.9

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

Просмотр сгенерированных Kotlin API для C-библиотеки

Хотя использование командной строки, как напрямую, так и в сочетании со скриптовым файлом (например, .sh или .bat файл), возможно, этот подход не масштабируется для больших проектов с сотнями файлов и библиотек. Лучше использовать компилятор Kotlin/Native с системой сборки, поскольку это помогает загружать и кэшировать двоичные файлы и библиотеки компилятора Kotlin/Native с транзитивными зависимостями и запускать компилятор и тесты. Kotlin/Native может использовать систему сборки Gradle через плагин kotlin-multiplatform.

Основы настройки проекта, совместимого с IDE, с Gradle, мы рассмотрели в учебнике Простой Kotlin/Native-приложение. Пожалуйста, ознакомьтесь с ним, если вам нужны подробные первые шаги и инструкции по запуску нового проекта Kotlin/Native и его открытию в IntelliJ IDEA. В этом учебнике мы рассмотрим расширенные использования Kotlin/Native, связанные с взаимодействием с C, и сборку с использованием Gradle и плагина multiplatform.

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

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

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

Файл проекта настраивает взаимодействие с C как дополнительный шаг сборки. Переместите файл interop.def в каталог src/nativeInterop/cinterop. Gradle рекомендует использовать соглашения вместо конфигураций, например, файлы исходного кода должны находиться в папке src/nativeMain/kotlin. По умолчанию все символы из C импортируются в пакет interop, вы можете импортировать весь пакет в файлы .kt. Ознакомьтесь с документацией плагина kotlin-multiplatform, чтобы узнать о различных способах его настройки.

Создайте заглушку src/nativeMain/kotlin/hello.kt с приведенным ниже содержимым, чтобы увидеть, как объявления struct и union 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 и посмотреть, как исправить примерный проект. Во время этого посмотрите, как типы struct и union C отображаются в Kotlin/Native.

Типы struct и union в Kotlin

С помощью IntelliJ IDEA's Перейти к | Объявление или ошибок компилятора вы видите следующий сгенерированный 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<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, которое происходит от типа приемника лямбда-функции, для преобразования экземпляров 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, которое происходит от типа приемника лямбда-функции, для преобразования экземпляров 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 охватывает более сложные сценарии взаимодействия.

Последнее изменение: 04 июля 2022
Сопоставление примитивных типов данных из 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