Картирование типов 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 и запустить его.
Просмотр сгенерированных 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
Использование структур и объединений типов из 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 охватывает более сложные сценарии взаимодействия.
© 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