Картирование указателей на функции из C – учебник
Это третья статья в серии. Первый учебник — Картирование примитивных типов данных из C. Также есть учебники Картирование структур и объединений из C и Картирование строк из C.
В этом учебнике мы узнаем, как:
Картирование типов указателей на функции из C
Лучший способ понять сопоставление между Kotlin и C — попробовать небольшой пример. Объявите функцию, принимающую указатель на функцию в качестве параметра, и другую функцию, возвращающую указатель на функцию.
Kotlin/Native поставляется с инструментом cinterop; этот инструмент генерирует связи между языком C и Kotlin. Он использует файл .def для указания библиотеки C, которую нужно импортировать. Более подробные сведения об этом содержатся в Взаимодействие с библиотеками C.
Самый быстрый способ попробовать сопоставление API C — разместить все объявления C в файле interop.def, не создавая никаких файлов .h или .c. Затем поместите объявления C в файл .def после специальной разделительной строки ---.
---
int myFun(int i) {
return i+1;
}
typedef int (*MyFun)(int);
void accept_fun(MyFun f) {
f(42);
}
MyFun supply_fun() {
return myFun;
}
Файл 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.
Сначала создайте папку проекта. Все пути в этом учебнике будут относительными к этой папке. Иногда перед добавлением новых файлов необходимо создать недостающие директории.
Используйте следующий файл сборки Gradle build.gradle(.kts):
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 со следующим содержимым, чтобы увидеть, как объявления указателей на функции C видны из Kotlin:
import interop.*
fun main() {
println("Hello Kotlin/Native!")
accept_fun(https://kotlinlang.org/*fix me */)
val useMe = supply_fun()
}
Теперь вы готовы открыть проект в IntelliJ IDEA и увидеть, как исправить примерный проект. Делая это, обратите внимание на то, как функции C отображаются в объявлениях Kotlin/Native.
Указатели на функции C в Kotlin
С помощью IntelliJ IDEA, Перейти | Объявление или использование или ошибок компилятора, посмотрите на следующие объявления функций C:
fun accept_fun(f: MyFun? /* = CPointer<CFunction<(Int) -> Int>>? */) fun supply_fun(): MyFun? /* = CPointer<CFunction<(Int) -> Int>>? */ fun myFun(i: kotlin.Int): kotlin.Int typealias MyFun = kotlinx.cinterop.CPointer<kotlinx.cinterop.CFunction<(kotlin.Int) -> kotlin.Int>> typealias MyFunVar = kotlinx.cinterop.CPointerVarOf<lib.MyFun>
Вы видите, что функция typedef из C была преобразована в Kotlin typealias. Она использует тип CPointer<..> для представления параметров-указателей и CFunction<(Int)->Int> для представления сигнатуры функции. Для всех типов CPointer<CFunction<..> доступна функция-расширение оператора invoke, поэтому ее можно вызывать так же, как и любую другую функцию в Kotlin.
Передача функции Kotlin в качестве указателя на функцию C
Сейчас можно попробовать использовать функции C из программы Kotlin. Вызовите функцию accept_fun и передайте указатель на функцию C в lambda Kotlin:
fun myFun() {
accept_fun(staticCFunction<Int, Int> { it + 1 })
}
Этот вызов использует вспомогательную функцию staticCFunction{..} из Kotlin/Native для обертывания функции Kotlin lambda в указатель на функцию C. Она допускает только функции lambda без привязки и без захвата. Например, она не может использовать локальную переменную из функции. Можно использовать только глобальные объявления. Генерация исключений из staticCFunction{..} приведет к непредсказуемым побочным эффектам. Очень важно убедиться, что ваш код не генерирует неожиданные исключения.
Использование указателя на функцию C из Kotlin
Следующий шаг — вызов указателя на функцию C из указателя на C, полученного из вызова supply_fun():
fun myFun2() {
val functionFromC = supply_fun() ?: error("No function is returned")
functionFromC(42)
}
Kotlin преобразует возвращаемый тип указателя на функцию в объект CPointer<CFunction<..>, имеющий значение null. Нужно явно проверить значение null сначала. Для этого используется оператор Elvis в приведенном выше коде. Инструмент cinterop помогает преобразовать указатель на функцию C в легко вызываемый объект в Kotlin. Это то, что мы сделали в последней строке.
Исправление кода
Вы видели все определения, и теперь пришло время исправить и запустить код. Запустите задачу Gradle runDebugExecutableNative в IDE или используйте следующую команду для запуска кода:
./gradlew runDebugExecutableNative
Код в файле hello.kt может выглядеть так:
import interop.*
import kotlinx.cinterop.*
fun main() {
println("Hello Kotlin/Native!")
val cFunctionPointer = staticCFunction<Int, Int> { it + 1 }
accept_fun(cFunctionPointer)
val funFromC = supply_fun() ?: error("No function is returned")
funFromC(42)
}
Дальнейшие шаги
Продолжайте изучать больше типов языка C и их представление в Kotlin/Native в следующих учебниках:
Документация по взаимодействию с 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-function-pointers-from-c.html