Хабрахабр

Kotlin Native: следите за файлами

Когда вы пишите command line утилиту, последнее, на что вам хочется полагаться, так это на то, что на компьютере где она будет запущена установлен JVM, Ruby или Python. Так же хотелось бы на выходе иметь один бинарный файл, который будет легко запустить. И не возиться слишком много с memory management'ом.

По вышеозначенным причинам, в последние годы всегда, когда мне нужно было писать подобные утилиты, я использовал Go.

Казалось бы, что еще нужно? У Go относительно простой синтаксис, неплохая стандартная библиотека, есть garbage collection, и на выходе мы получаем один бинарник.

Предложение звучало многообещающе — GC, единый бинарник, знакомый и удобный синтаксис. Не так давно Kotlin так же стал пробовать себя на схожем поприще в форме Kotlin Native. Как аргументы утилита должна получать путь к файлу и частоту проверки. Но все ли так хорошо, как хотелось бы?
Задача, которую нам предстоит решить: написать на Kotlin Native простой file watcher. Если файл изменился, утилита дожна создать его копию в той же папке с новым именем.

Иначе говоря, алгоритм должен выглядеть следующим образом:

fileToWatch = getFileToWatch()
howOftenToCheck = getHowOftenToCheck()
while (!stopped) sleep(howOftenToCheck)
}

Время писать код. Ладно, с тем чего хотим добиться вроде бы разобрались.

Среда

Любителей vim попрошу не беспокоиться.
Запускаем привычный IntelliJ IDEA и обнаруживаем, что в Kotlin Native он не может от слова совсем. Первое, что нам потребуется — это IDE. Нужно использовать CLion.

Нужен toolchain. На этом злоключения человека, который в последний раз сталкивался с C в 2004 еще не окончены. Но если вы решили использовать Windows и на C не программируете, придется повозиться с tutorial'ом по установке какого-нибудь Cygwin. Если вы используете OSX, скорее всего CLion обнаружит подходящий toolchain сам.

Можно уже начать код писать? IDE установили, с toolchain'ом разобрались. Так что прежде, чем мы увидим заветную надпись «New Kotlin/Native Application» придется его установить вручную. Почти.
Поскольку Kotlin Native еще несколько экспериментален, плагин для него в CLion не установлен по умолчанию.

Немного настроек

Что интересно, он основан на Gradle (а не на Makefile'ах), да еще на Kotlin Script версии.
Заглянем в build.gradle.kts: И так, наконец-то у нас есть пустой Kotlin Native проект.

plugins { id("org.jetbrains.kotlin.konan") version("0.8.2")
} konanArtifacts { program("file_watcher")
}

Он то и будет производить наш бинарный файл. Единственный плагин, который мы будем использовать называется Konan.

В данном примере получится file_watcher.kexe В konanArtifacts мы указываем имя исполняемого файла.

Код

Пора бы уже и код показать. Вот он, кстати:

fun main(args: Array<String>) { if (args.size != 2) { return println("Usage: file_watcher.kexe <path> <interval>") } val file = File(args[0]) val interval = args[1].toIntOrNull() ?: 0 require(file.exists()) { "No such file: $file" } require(interval > 0) { "Interval must be positive" } while (true) { // We should do something here }
}

Но для примера будем предполагать, что аргумента всегда два: path и interval Обычно у command line утилиты бывают так же опциональные аргументы и их значения по умолчанию.

File. Для тех, кто с Kotlin уже работал можем показаться странным, что path оборачивается в свой собственный класс File, без использования java.io. Объяснение этому — черезе минуту-другую.

Kotlin — он вообще про удобство. Если вы вдруг не знакомы с функцией require() в Kotlin — это просто более удобный способ для валидации аргументов. Можно было бы написать и так:

if (interval <= 0) { println("Interval must be positive") return
}

А вот с этого момента станет повеселей. В целом, тут пока обычный Kotlin код, ничего интересного.

Готовы? Давайте будем пытаться писать обычный Kotlin-код, но каждый раз, когда нам нужно использовать что-нибудь из Java, мы говорим «упс!».

Вернемся к нашему while, и пусть он отпечатывает каждый interval какой-нибудь символ, к примеру точку.


var modified = file.modified()
while (true) { if (file.modified() > modified) { println("\nFile copied: ${file.copyAside()}") modified = file.modified() } print(".") // Упс... Thread.sleep(interval * 1000)
}

Мы не можем использовать Java классы в Kotlin Native. Thread — это класс из Java. Никакой Java.
Кстати, потому в main мы и не использовали java.io. Только Kotlin'овские классы. File

Функции из C! Хорошо, а что тогда можно использовать?

var modified = file.modified()
while (true) { if (file.modified() > modified) { println("\nFile copied: ${file.copyAside()}") modified = file.modified() } print(".") sleep(interval)
}

Добро пожаловать в мир C

Теперь, когда мы знаем что нас ждет, давайте посмотрим как выглядит функция exists() из нашего File:

data class File(private val filename: String) { fun exists(): Boolean { return access(filename, F_OK) != -1 } // More functions...
}

File это простой data class, что дает нам имплементацию toString() из коробки, которой мы потом воспользуемся.
«Под капотом» мы вызываем C функцию access(), которая возвращает -1, если такого файла не существует.

Дальше по списку у нас функция modified():

fun modified(): Long = memScoped { val result = alloc<stat>() stat(filename, result.ptr) result.st_mtimespec.tv_sec
}

Функцию можно было бы немного упростить используя type inference, но тут я решил этого не делать, чтобы было понятно, что функция не возвращает, к примеру, Boolean.

Во-первых, мы используем alloc(). В этой фукнции есть две интересные детали. Тут на помощь приходит функция memScoped() из Kotlin Native, которая это сделает за нас. Поскольку мы используем C, иногда нужно выделять структуры, а делается это в C вручную, при помощи malloc().
Высвобождать эти структуры тоже нужно вручную.

Нам осталось рассмотреть наиболее увесистую функцию: сopyAside()

fun copyAside(): String { val state = copyfile_state_alloc() val copied = generateFilename() if (copyfile(filename, copied, state, COPYFILE_DATA) < 0) { println("Unable to copy file $filename -> $copied") } copyfile_state_free(state) return copied
}

Тут мы используем С функцию copyfile_state_alloc(), которая выделяет нужную для copyfile() структуру.
Но и высвобождать нам эту структуру приходится самим — используя copyfile_state_free(state)

Тут просто немного Kotlin: Последнее, что осталось показать — это генерация имен.

private var count = 0
private val extension = filename.split(".").last() private fun generateFilename() = filename.replace(extension, "${++count}.$extension")

Это довольно наивный код, который игнорирует многие кейсы, но для примера сойдет.

Пуск

Теперь как все это запускать?

Он все сделает за нас. Один вариант — это конечно использовать CLion.

Да и какой-нибудь CI не будет запускать наш код из CLion. Но давайте вместо этого используем command line, чтобы лучше понять процесс.

./gradlew build && ./build/konan/bin/macos_x64/file_watcher.kexe ./README.md 1

Если все прошло успешно, появится следующее сообщение: Первым делом мы компилируем наш проект используя Gradle.

BUILD SUCCESSFUL in 16s

Да, в сравнение с каким-нибудь Go или даже Kotlin для JVM, результат неутешителен. Шестнадцать секунд?! И это еще крошечный проект.

И если вы измените содержимое файла, об этом появится сообщение. Теперь вы должны увидеть бегущие по экрану точки. Что-то вроде такой картины:

................................
File copied: ./README.1.md
...................
File copied: ./README.2.md

Зато мы можем проверить, сколько памяти занимает наш процесс, используя к примеру Activity Monitor: 852KB. Время запуска замерить сложно. Неплохо!

Немного выводов

И так, мы выяснили что при помощи Kotlin Native мы можем получить единый исполняемый файл с memory footprint'ом меньше, чем у того же Go. Победа? Не совсем.

Те кто работал с Go или Kotlin'ом знаю, что в обоих языках есть хорошие решения для этой важной задачи. Как это все тестировать? У Kotlin Native с этим пока что все плохо.

Но учитывая, что даже у официальных примеров Kotlin Native нет тестов, видимо пока не слишком успешно. Вроде бы в 2017ом JetBrains пытались это решить.

Те, кто работали с C побольше моего уже наверняка заметили, что мой пример будет работать на OSX, но не на Windows, поскольку я полагаюсь на несколько функций доступных только с platform.darwin. Другая проблема — crossplatform разработка. Надеюсь что в будущем у Kotlin Native появится больше оберток, которые позволят абстрагироваться от платформы, к примеру при работе с файловой системой.

Все примеры кода вы можете найти тут: github.com/AlexeySoshin/KotlinNativeFileWatcher

И ссылка на мою оригинальную статью, если вы предпочитаете читать на английском: proandroiddev.com/kotlin-native-watch-your-files-6fff7266c490

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть