Картирование типов struct и union из C – учебник
Это вторая публикация в серии. Первый учебник серии – Картирование примитивных типов данных из C. Также есть учебники Картирование указателей на функции из C и Картирование строк из C.
В этом учебнике вы узнаете:
Картирование типов 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-взаимодействию
Использование структур и объединений типов из 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 Interop охватывает более сложные сценарии взаимодействия.
© 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