Картирование типов struct и union из C – учебник
Это вторая публикация в серии. Самый первый учебник серии — Картирование примитивных типов данных из C. Также есть учебники Картирование указателей на функции из C и Картирование строк из C.
В этом учебнике вы узнаете:
Картирование типов struct и union 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. В этом учебнике мы рассмотрим расширенное использование Kotlin/Native, связанное с взаимодействием с C, и сборки с использованием multiplatform с Gradle.
Сначала создайте папку проекта. Все пути в этом учебнике будут относительными к этой папке. Иногда необходимо предварительно создать отсутствующие каталоги, прежде чем можно будет добавить новые файлы.
Используйте следующий build.gradle(.kts) файл Gradle:
plugins {
kotlin("multiplatform") version "1.7.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.7.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 struct и union видны из 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 struct и union отображаются в Kotlin/Native.
Типы struct и union в 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-взаимодействию
Использование структур и объединений типов из 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. Здесь понадобится 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
Конечный код в файле 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–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