Когда вы пишите command line утилиту, последнее, на что вам хочется полагаться, так это на то, что на компьютере где она будет запущена установлен JVM, Ruby или Python. Так же хотелось бы на выходе иметь один бинарный файл, который будет легко запустить. И не возиться слишком много с memory management'ом.
По вышеозначенным причинам, в последние годы всегда, когда мне нужно было писать подобные утилиты, я использовал Go.
У Go относительно простой синтаксис, неплохая стандартная библиотека, есть garbage collection, и на выходе мы получаем один бинарник. Казалось бы, что еще нужно?
Не так давно Kotlin так же стал пробовать себя на схожем поприще в форме Kotlin Native. Предложение звучало многообещающе — GC, единый бинарник, знакомый и удобный синтаксис. Но все ли так хорошо, как хотелось бы?
Задача, которую нам предстоит решить: написать на Kotlin Native простой file watcher. Как аргументы утилита должна получать путь к файлу и частоту проверки. Если файл изменился, утилита дожна создать его копию в той же папке с новым именем.
Иначе говоря, алгоритм должен выглядеть следующим образом:
Ладно, с тем чего хотим добиться вроде бы разобрались. Время писать код.
Первое, что нам потребуется — это IDE. Любителей vim попрошу не беспокоиться.
Запускаем привычный IntelliJ IDEA и обнаруживаем, что в Kotlin Native он не может от слова совсем. Нужно использовать CLion.
На этом злоключения человека, который в последний раз сталкивался с C в 2004 еще не окончены. Нужен toolchain. Если вы используете OSX, скорее всего CLion обнаружит подходящий toolchain сам. Но если вы решили использовать Windows и на C не программируете, придется повозиться с tutorial'ом по установке какого-нибудь Cygwin.
IDE установили, с toolchain'ом разобрались. Можно уже начать код писать? Почти.
Поскольку Kotlin Native еще несколько экспериментален, плагин для него в CLion не установлен по умолчанию. Так что прежде, чем мы увидим заветную надпись «New Kotlin/Native Application» придется его установить вручную.
И так, наконец-то у нас есть пустой Kotlin Native проект. Что интересно, он основан на Gradle (а не на Makefile'ах), да еще на Kotlin Script версии.
Заглянем в
Единственный плагин, который мы будем использовать называется Konan. Он то и будет производить наш бинарный файл.
В
Пора бы уже и код показать. Вот он, кстати:
Обычно у command line утилиты бывают так же опциональные аргументы и их значения по умолчанию. Но для примера будем предполагать, что аргумента всегда два:
Для тех, кто с Kotlin уже работал можем показаться странным, что
Если вы вдруг не знакомы с функцией require() в Kotlin — это просто более удобный способ для валидации аргументов. Kotlin — он вообще про удобство. Можно было бы написать и так:
В целом, тут пока обычный Kotlin код, ничего интересного. А вот с этого момента станет повеселей.
Давайте будем пытаться писать обычный Kotlin-код, но каждый раз, когда нам нужно использовать что-нибудь из Java, мы говорим «упс!». Готовы?
Вернемся к нашему
Кстати, потому в
Хорошо, а что тогда можно использовать? Функции из C!
Теперь, когда мы знаем что нас ждет, давайте посмотрим как выглядит функция
«Под капотом» мы вызываем C функцию
Дальше по списку у нас функция
Функцию можно было бы немного упростить используя type inference, но тут я решил этого не делать, чтобы было понятно, что функция не возвращает, к примеру,
В этой фукнции есть две интересные детали. Во-первых, мы используем
Высвобождать эти структуры тоже нужно вручную. Тут на помощь приходит функция
Нам осталось рассмотреть наиболее увесистую функцию:
Тут мы используем С функцию
Но и высвобождать нам эту структуру приходится самим — используя
Последнее, что осталось показать — это генерация имен. Тут просто немного Kotlin:
Это довольно наивный код, который игнорирует многие кейсы, но для примера сойдет.
Теперь как все это запускать?
Один вариант — это конечно использовать CLion. Он все сделает за нас.
Но давайте вместо этого используем command line, чтобы лучше понять процесс. Да и какой-нибудь CI не будет запускать наш код из CLion.
Первым делом мы компилируем наш проект используя Gradle. Если все прошло успешно, появится следующее сообщение:
Шестнадцать секунд?! Да, в сравнение с каким-нибудь Go или даже Kotlin для JVM, результат неутешителен. И это еще крошечный проект.
Теперь вы должны увидеть бегущие по экрану точки. И если вы измените содержимое файла, об этом появится сообщение. Что-то вроде такой картины:
Время запуска замерить сложно. Зато мы можем проверить, сколько памяти занимает наш процесс, используя к примеру Activity Monitor: 852KB. Неплохо!
И так, мы выяснили что при помощи Kotlin Native мы можем получить единый исполняемый файл с memory footprint'ом меньше, чем у того же Go. Победа? Не совсем.
Как это все тестировать? Те кто работал с Go или Kotlin'ом знаю, что в обоих языках есть хорошие решения для этой важной задачи. У Kotlin Native с этим пока что все плохо.
Вроде бы в 2017ом JetBrains пытались это решить. Но учитывая, что даже у официальных примеров Kotlin Native нет тестов, видимо пока не слишком успешно.
Другая проблема — crossplatform разработка. Те, кто работали с C побольше моего уже наверняка заметили, что мой пример будет работать на OSX, но не на Windows, поскольку я полагаюсь на несколько функций доступных только с
Все примеры кода вы можете найти тут
И ссылка на мою оригинальную статью, если вы предпочитаете читать на английском
По вышеозначенным причинам, в последние годы всегда, когда мне нужно было писать подобные утилиты, я использовал Go.
У Go относительно простой синтаксис, неплохая стандартная библиотека, есть garbage collection, и на выходе мы получаем один бинарник. Казалось бы, что еще нужно?
Не так давно Kotlin так же стал пробовать себя на схожем поприще в форме Kotlin Native. Предложение звучало многообещающе — GC, единый бинарник, знакомый и удобный синтаксис. Но все ли так хорошо, как хотелось бы?
Задача, которую нам предстоит решить: написать на Kotlin Native простой file watcher. Как аргументы утилита должна получать путь к файлу и частоту проверки. Если файл изменился, утилита дожна создать его копию в той же папке с новым именем.
Иначе говоря, алгоритм должен выглядеть следующим образом:
fileToWatch = getFileToWatch()
howOftenToCheck = getHowOftenToCheck()
while (!stopped) {
if (hasChanged(fileToWatch)) {
copyAside(fileToWatch)
}
sleep(howOftenToCheck)
}
Ладно, с тем чего хотим добиться вроде бы разобрались. Время писать код.
Среда
Первое, что нам потребуется — это IDE. Любителей vim попрошу не беспокоиться.
Запускаем привычный IntelliJ IDEA и обнаруживаем, что в Kotlin Native он не может от слова совсем. Нужно использовать CLion.
На этом злоключения человека, который в последний раз сталкивался с C в 2004 еще не окончены. Нужен toolchain. Если вы используете OSX, скорее всего CLion обнаружит подходящий toolchain сам. Но если вы решили использовать Windows и на C не программируете, придется повозиться с tutorial'ом по установке какого-нибудь Cygwin.
IDE установили, с toolchain'ом разобрались. Можно уже начать код писать? Почти.
Поскольку Kotlin Native еще несколько экспериментален, плагин для него в CLion не установлен по умолчанию. Так что прежде, чем мы увидим заветную надпись «New Kotlin/Native Application» придется его установить вручную.
Немного настроек
И так, наконец-то у нас есть пустой Kotlin Native проект. Что интересно, он основан на Gradle (а не на Makefile'ах), да еще на Kotlin Script версии.
Заглянем в
build.gradle.kts
:plugins {
id("org.jetbrains.kotlin.konan") version("0.8.2")
}
konanArtifacts {
program("file_watcher")
}
Единственный плагин, который мы будем использовать называется Konan. Он то и будет производить наш бинарный файл.
В
konanArtifacts
мы указываем имя исполняемого файла. В данном примере получится file_watcher.kexe
Код
Пора бы уже и код показать. Вот он, кстати:
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
}
}
Обычно у command line утилиты бывают так же опциональные аргументы и их значения по умолчанию. Но для примера будем предполагать, что аргумента всегда два:
path
и interval
Для тех, кто с Kotlin уже работал можем показаться странным, что
path
оборачивается в свой собственный класс File
, без использования java.io.File
. Объяснение этому — черезе минуту-другую.Если вы вдруг не знакомы с функцией require() в Kotlin — это просто более удобный способ для валидации аргументов. 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)
}
Thread
— это класс из Java. Мы не можем использовать Java классы в Kotlin Native. Только Kotlin'овские классы. Никакой Java.Кстати, потому в
main
мы и не использовали java.io.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()
. Поскольку мы используем C, иногда нужно выделять структуры, а делается это в C вручную, при помощи malloc(). Высвобождать эти структуры тоже нужно вручную. Тут на помощь приходит функция
memScoped()
из Kotlin Native, которая это сделает за нас.Нам осталось рассмотреть наиболее увесистую функцию:
с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.substringAfterLast(".")
private fun generateFilename() = filename.replace(extension, "${++count}.$extension")
Это довольно наивный код, который игнорирует многие кейсы, но для примера сойдет.
Пуск
Теперь как все это запускать?
Один вариант — это конечно использовать CLion. Он все сделает за нас.
Но давайте вместо этого используем command line, чтобы лучше понять процесс. Да и какой-нибудь CI не будет запускать наш код из CLion.
./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 с этим пока что все плохо.
Вроде бы в 2017ом JetBrains пытались это решить. Но учитывая, что даже у официальных примеров Kotlin Native нет тестов, видимо пока не слишком успешно.
Другая проблема — crossplatform разработка. Те, кто работали с C побольше моего уже наверняка заметили, что мой пример будет работать на OSX, но не на Windows, поскольку я полагаюсь на несколько функций доступных только с
platform.darwin
. Надеюсь что в будущем у Kotlin Native появится больше оберток, которые позволят абстрагироваться от платформы, к примеру при работе с файловой системой.Все примеры кода вы можете найти тут
И ссылка на мою оригинальную статью, если вы предпочитаете читать на английском
VanquisherWinbringer
В плане написания вот таких банальных утилит Go однозначный чемпион и вообще мастхев для DevOps инженера. Хоть я и ненавижу Go, эту его сильную сторону приходиться признать. Поэтому мне кажется вообще идея на этом поприще Котлин с ним сравнивать не очень то удачной. Вот если бы его с С сравнивали я бы ещё понял а так это выглядит странным. Да и вообще, слышал от Джавистов с которыми работаю что Котлин клевый и удобный под JVM. Пытаться его сделать натив при этом во всю используя СLang это ИМХО натягивание совы на глобус. Как тот же JS для микроконтроллеров. Да, если кто не знал, на JS можно под микроконтроллеры писать. Как страшно жить.
AlexeySoshin Автор
Для JVM у Kotlin действительно все прекрасно. Но вот JetBrains уже пару релизов экспериментируют с кроссплатформенностью. Скорее всего видят там какой-то потенциал.
SirEdvin
Подскажите, пожалуйста, а почему не python? На серверах он у вас скорее всего есть, на рабочей станции тоже. Для простых утилит даже нет необходимости что-то доставлять — все есть из коробки.
VanquisherWinbringer
В общем да, питон крут НО его надо ещё поставить. Хотя да, обычно он и так есть. Ну Го таки вывозит единым бинарем, тем что больше ничего не надо ставить и тем что почти все что нужно есть в стандартной библиотеке. Я как то писал такую вещь на C# и ради такой фигни подключать какие то либы, проверять есть ли дотнет вообще не охота. И на питон тоже писал такие утилиты. Также история. А с Go — взял и пишешь не о чем не парясь, все есть в стандартной либо и все запускается на месте. С другой стороны, для больших и серьезных проектов все преимущества Go сходят на нет. Для такого проекта поставить JVM/CLR(.net) не проблема. В них при любом раскладе с десяток библиотек подключённой, они обычно вообще весь сервер и все его ресурсы себе забирают и т. д.
SirEdvin
У меня сервера настраиваются при помощи ansible, так что питон там уже стоит по умолчанию. Как-то так.
Edison
А нужные библиотеки могут там не стоять
SirEdvin
Edison
Отсутствие зависимостей всегда лучше их наличия и ansible тут не панацея.
А
нужна 2.2 версия, а тулзеБ
3.1. Блин, надо virtualenv настраивать?pip
илиyum
— чем мы там python зависимости ставим? А уже везде дефолтная версия python3? Ахх, тут на 100 серверах yum'ом установили, а на другой сотне pip'ом.state: latest
игнорит версию в названии пакет).И так далее.
Преимущество есть и оно большое (может быть конечно не "невообразимое").
SirEdvin
В случае с python вам нужно настраивать virtualenv вообще всегда, если вы не выпускаете тулзу для дистрибутивов и встраиваете ее в правильный pipe ее менеджера пакетов. И вместе с этим уходят вопросы 2 и 3.
Дает доступ на сервер, но не дает рут доступ? Я смотрю у вас ОС команда еще те выдумщики, учитывая, сколько есть возможностей и эскалировать доступ в случае чего, и понаставить магии. Ну и да, если у вас у разработчиков нет возможности лазить на сервер и делать то, что им надо — фиксите процессы. Либо оно на самом деле им не надо, либо если таки надо, то надо возможность им давать.
Опять же, С зависимости у вас могут быть и в go, например для imagemagick. От них вообще ничего не спасает.
Я побью это картой "интерпретируемые языки для cli тулзовин лучше, чем комбилируемые".
Нет, правда, у вас есть 1000 и 1 возможность собрать python в бинарник, даже собрать просто все в один файл, необходимый инструментарий для того, что бы вообще не было проблем с управлением зависимостями на серверах (контейнеризация, например), но если для вас это все какое-то либо преимущество перед другими языками, то это ваше право.
Мое мнение в том что преимуществом должно быть что-то, что еще не реализовали в других языках, или же это подключается жуткими костымями. Например, тот же шедулинг. Но сборка в один файл, серьезно? Я как-то уже года три минимум работаю с питоном и никогда не было такой проблемы.
Edison
То есть вы мне предлагаете сделать что-то (как virtualenv или собрать python в бинарник) вместо того, чтобы ничего этого не делать и называете это преимуществом?
SirEdvin
Простите, вы же собираете го в бинарник, вместо того что бы деплоить прямо кодом. Что мешает так сделать с питоном?
Ну и нет, лично мое мнение по поводу того, почему cli тулзы лучше писать на интерпретируемых языках я написал выше. Вкратце, их проще писать, проще дебажить куда быстрее поправить, если они сбоят в каких-то местах. Не говоря уже о том, что значительное количество тех же веб фреймворков позволяют просто запустить условную консоль внутри, что значительно снижает необходимость в таких мелких инструментах.
Необходимость собирать в бинарник или использовать virtualenv (я предпочитаю этот вариант) это больше неудобство, чем проблема.
Edison
В Go я делаю вот так `GOOS=linux go build` и запускаю бинарник на сервере. А как это сделать с python?
> Необходимость собирать в бинарник или использовать virtualenv (я предпочитаю этот вариант) это больше неудобство, чем проблема.
Так разве удобство это не преимущество?
0xd34df00d
А что именно в Go делает его чемпионом для таких утилит?
deril
Думаю что C-подобный синтаксис, простота синтаксиса и единый бинарник на выходе. А кто чемпион по Вашемму мнению?
VanquisherWinbringer
Питон ещё претендует но там одна проблема — его ещё поставить надо хотя да он обычно и так уже стоит.
0xd34df00d
По моему мнению чемпион тот, с чем удобно и привычно.
Вот лично я наркоман и поэтому писал бы на хаскеле, получилось бы
Restorer
Если это планируется использовать где-то кроме как в качестве обучения, то почти все ОС предоставляют специальные механизмы для отслеживания изменений файлов, чтобы не делать
while (true) { ... }
. Гугл подсказывает, что есть кросс-платформенные библиотеки, например — github.com/emcrisostomo/fswatchAlexeySoshin Автор
Тысячи разных filewatcher'ов, на любой вкус и цвет. Цель была все же разобраться что предоставляет (и не предоставляет) Kotlin Native, а не написать еще один.
SirEdvin
На практике получается, наоборот. Последнее, что мне бы хотелось, это что бы она была написана на комбилируемом языке и падала с паникой где-то внутри.
MikailBag
А почему паника хуже чем непойманный эксепшн? И там и там аварийное завершение, и там и там стектрейс.
SirEdvin
Очевидно, потому что в случае интерпритируемого языка есть надежда это поправить так сказать, на месте.
MikailBag
Скачиваете исходник утилиты и патчите, в чём проблема?
SirEdvin
В основном проблема в том, что для исправление интерпретируемого языка нужно просто патчить файл и в самом простом случае выводить принтами, а в случае комбилируемого языка нужно разбиратся с его окружением, подходами к дебагу и прочее.
Я пытался провернуть такое с traefik, но схема собери и запусти не работает без настроенного рабочего окружения с этими всеми GOPATH и прочим.
Тот же gitlab мне было гораздно проще пачтить (хотя руби я не знаю еще хуже, чем golang), потому что это сводилось к изменению файликов :)
Опять же, если вы используете golang в своей работе — этой проблемы не будет.
Edison
А говорят в Go очень низкой порог вхождения, оказывается не такой уже и низкий.
SirEdvin
Это специфика наркотика известного как «интерпретируемые языки». После них очень сложно привыкнуть к тому, как сделан дебаг в компилируемых языках.
Ну и кстати, про GOPATH я зря сказал, оказывается, наконец-то можно запустить hello-world без переменных окружения.
AlexeySoshin Автор
Debug везде примерно одинаковый, не? Или ты про REPL?
А GOPATH вот недавно только профиксили. Не прошло и 8и лет, ага.
SirEdvin
REPL + возможность встроить дебаг поинт прямо в код (не знаю, если ли такое в компилируемых языках, не попадалось) + тот факт, что исходники почти всегда с программой поставляются :)
AlexeySoshin Автор
REPL в том же Go сильно не хватает, да.
MikailBag
В C/C++ есть трюк с
AlexeySoshin Автор
Тут все относительно: с каким языком сравнивать и куда именно нужно входить.
Если сравнивать с каким-нибудь C/C++, действительно начать писать на нем проще.
Если же писать на нем какой-нибудь concurrent код, то не все так однозначно. А с написанием business logic там все вообще очень плохо.
NishchebrodKolya
На практике, если бы не было пхп и джавы (и прочих скриптовых/интерпретируемых уродов), датацентры были бы в два раза меньше, экология чище, и тигры в Сибири не вымирали.
SirEdvin
Боюсь, если бы у нас не было интерпретируемых языков, датацентры были бы в 20 раз больше, все было бы в угле, никто бы в здравом уме не доверял бы компьютерным системам, потому что каждая вторая была бы решетом, а куча людей, которые топят за комбилируемые языки и прямое управление памятью оказались бы на улицах, потому что говорить они все мастаки, а про утечки и двойную очистку памяти, переполнения и прочее прелести в таких языках они почему-то забывают.
Ну и ентерпрайз рестарты раз в час в каждой второй системе, куда же без них.
AlexeySoshin Автор
Даже не знаю, что меня смущает больше: разделение на «скриптовые» и «интерпретируемые», или то, что Java причисляется к «интерпретируемым».
But I'll bite. Какую альтернативу предложишь?
AlexeySoshin Автор
По началу не понял связи, в интерпретируемых языках ведь так же можно получить exception. Но видимо ты о ситуации, когда утилита есть, а исходников нет?
SirEdvin
В целом — да. Или когда разработчик утилиты не моя компания, например.
AlexeySoshin Автор
Так большинство утилит будет сделано какой-нибудь другой компанией.
Есть же PR, есть fork в конце-то концов.
rfq
было бы интереснее почитать об опыте использования java Ahead-Of-Time компиляторов. Я знаю о двух: Excelsior JET и jaotc из JDK9+.
AlexeySoshin Автор
Мне как раз на днях попалась хорошая статья по теме:
medium.com/graalvm/instant-netty-startup-using-graalvm-native-image-generation-ed6f14ff7692
MechanicZelenyy
Куча людей пишет мультиплатформенные проекты на koltin в IDEA, они спокойно компилируются и в Native и в JS. Так что читать статью о том что kotlin native не торт, от человека, который даже IDE не настроил, это ну как-то, ну сами понимаете.
AlexeySoshin Автор
Проверил, ты прав, теперь и IntelliJ позволяет создавать Kotlin/Native проекты. Еще пару месяцев назад такой возможности не было.
SerVB
filename.split(".").last()
->filename.substringAfterLast(".")
:)AlexeySoshin Автор
Спасибо, действительно элегантней :)