Lorsque vous écrivez un utilitaire de ligne de commande, la dernière chose sur laquelle vous souhaitez compter est que JVM, Ruby ou Python est installé sur l'ordinateur sur lequel il s'exécutera. J'aimerais également avoir un fichier binaire qui sera facile à lancer. Et ne vous embêtez pas trop avec la gestion de la mémoire.
Pour les raisons ci-dessus, ces dernières années, chaque fois que j'avais besoin d'écrire de tels utilitaires, j'utilisais Go.
Go a une syntaxe relativement simple, une bonne bibliothèque standard, il y a un ramasse-miettes et à la sortie nous obtenons un binaire. Il semblerait que quoi d'autre soit nécessaire?
Il n'y a pas si longtemps, Kotlin a également commencé à s'essayer dans un domaine similaire sous la forme de Kotlin Native. La proposition semblait prometteuse - GC, une syntaxe binaire unique, familière et pratique. Mais tout est-il aussi bon que nous le souhaiterions?
Le problème que nous devons résoudre: écrire un simple observateur de fichiers sur Kotlin Native. Comme arguments, l'utilitaire doit obtenir le chemin du fichier et la fréquence de l'analyse. Si le fichier a changé, l'utilitaire doit en créer une copie dans le même dossier avec le nouveau nom.
En d'autres termes, l'algorithme devrait ressembler à ceci:
fileToWatch = getFileToWatch() howOftenToCheck = getHowOftenToCheck() while (!stopped) { if (hasChanged(fileToWatch)) { copyAside(fileToWatch) } sleep(howOftenToCheck) }
D'accord, donc ce que nous voulons réaliser semble être réglé. Il est temps d'écrire du code.
Mercredi
La première chose dont nous avons besoin est un IDE. Les amoureux de Vim ne vous inquiétez pas.
Nous lançons l'IntelliJ IDEA familier et constatons que dans Kotlin Native, il ne peut pas du tout du mot. Vous devez utiliser
CLion .
Les mésaventures de la dernière personne ayant rencontré C en 2004 ne sont pas encore terminées. Besoin d'une chaîne d'outils. Si vous utilisez OSX, CLion trouvera très probablement la chaîne d'outils appropriée elle-même. Mais si vous décidez d'utiliser Windows et ne programmez pas en C, vous devrez bricoler le tutoriel pour installer du
Cygwin .
IDE installé, trié la chaîne d'outils. Puis-je déjà commencer à écrire du code? Presque.
Étant donné que Kotlin Native est encore quelque peu expérimental, le plug-in correspondant dans CLion n'est pas installé par défaut. Donc, avant de voir l'inscription chérie "New Kotlin / Native Application" devra l'
installer manuellement .
Quelques réglages
Et donc, finalement, nous avons un projet Kotlin Native vide. Fait intéressant, il est basé sur Gradle (et non sur Makefiles), et même sur la version Kotlin Script.
build.gradle.kts
œil à
build.gradle.kts
:
plugins { id("org.jetbrains.kotlin.konan") version("0.8.2") } konanArtifacts { program("file_watcher") }
Le seul plugin que nous utiliserons s'appelle Konan. Il produira notre fichier binaire.
Dans
konanArtifacts
nous
konanArtifacts
le nom du fichier exécutable. Dans cet exemple, nous obtenons
file_watcher.kexe
Code
Il est déjà temps de montrer le code. Le voici, au fait:
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) {
En règle générale, les utilitaires de ligne de commande ont également des arguments facultatifs et leurs valeurs par défaut. Mais par exemple, nous supposerons qu'il y a toujours deux arguments:
path
et
interval
Pour ceux qui ont déjà travaillé avec Kotlin, il peut sembler étrange que le
path
termine dans sa propre classe
File
, sans utiliser
java.io.File
. L'explication est dans une minute ou deux.
Si vous n'êtes pas familier avec la fonction require () dans Kotlin, c'est juste un moyen plus pratique de valider les arguments. Kotlin - c'est une question de commodité. On pourrait écrire comme ceci:
if (interval <= 0) { println("Interval must be positive") return }
En général, voici le code Kotlin habituel, rien d'intéressant. Mais à partir de maintenant, ce sera amusant.
Essayons d'écrire du code Kotlin ordinaire, mais chaque fois que nous devons utiliser quelque chose de Java, nous disons "oups!". Êtes-vous prêt?
Revenons à notre époque et laissons imprimer à chaque
interval
un symbole, par exemple une période.
var modified = file.modified() while (true) { if (file.modified() > modified) { println("\nFile copied: ${file.copyAside()}") modified = file.modified() } print(".")
Thread
est une classe de Java. Nous ne pouvons pas utiliser les classes Java dans Kotlin Native. Uniquement les cours de Kotlin. Pas de java.
Soit dit en passant, parce que, dans
main
nous n'avons pas utilisé
java.io.File
Eh bien, qu'est-ce qui peut alors être utilisé? Fonctions de C!
var modified = file.modified() while (true) { if (file.modified() > modified) { println("\nFile copied: ${file.copyAside()}") modified = file.modified() } print(".") sleep(interval) }
Bienvenue dans le monde C
Maintenant que nous savons ce qui nous attend, voyons à quoi ressemble la fonction exist
exists()
de notre
File
:
data class File(private val filename: String) { fun exists(): Boolean { return access(filename, F_OK) != -1 }
File
est une simple
data class
, qui nous donne l'implémentation de
toString()
partir de la boîte, que nous utiliserons plus tard.
«Sous le capot», nous appelons la fonction
access()
C, qui renvoie
-1
si un tel fichier n'existe pas.
Plus bas dans la liste, nous avons la fonction
modified()
:
fun modified(): Long = memScoped { val result = alloc<stat>() stat(filename, result.ptr) result.st_mtimespec.tv_sec }
La fonction pourrait être un peu simplifiée en utilisant l'inférence de type, mais ici j'ai décidé de ne pas le faire, de sorte qu'il était clair que la fonction ne retourne pas, par exemple,
Boolean
.
Il y a deux détails intéressants dans cette fonction. Tout d'abord, nous utilisons
alloc()
. Puisque nous utilisons C, nous devons parfois allouer des structures, et cela se fait manuellement en C, en utilisant malloc ().
Ces structures doivent également être libérées manuellement. La fonction
memScoped()
de Kotlin Native vient à la
memScoped()
, ce qui le fera pour nous.
Il nous reste à considérer la fonction la plus lourde:
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 }
Ici, nous utilisons la fonction C
copyfile_state_alloc()
, qui sélectionne la structure nécessaire pour
copyfile()
.
Mais nous devons libérer cette structure nous-mêmes - en utilisant
copyfile_state_free(state)
La dernière chose à montrer est la génération de noms. Il y a juste un petit Kotlin:
private var count = 0 private val extension = filename.substringAfterLast(".") private fun generateFilename() = filename.replace(extension, "${++count}.$extension")
C'est un code assez naïf qui ignore de nombreux cas, mais il fera comme exemple.
Commencer
Maintenant, comment tout gérer?
Une option est bien sûr d'utiliser CLion. Il fera tout pour nous.
Mais utilisons plutôt la ligne de commande pour mieux comprendre le processus. Oui, et certains CI n'exécuteront pas notre code depuis CLion.
./gradlew build && ./build/konan/bin/macos_x64/file_watcher.kexe ./README.md 1
Tout d'abord, nous compilons notre projet en utilisant Gradle. Si tout s'est bien passé, le message suivant apparaîtra:
BUILD SUCCESSFUL in 16s
Seize secondes?! Oui, par rapport à certains Go ou même Kotlin pour la JVM, le résultat est décevant. Et c'est encore un tout petit projet.
Vous devriez maintenant voir des points courir sur l'écran. Et si vous modifiez le contenu du fichier, un message apparaîtra. Quelque chose comme cette image:
................................ File copied: ./README.1.md ................... File copied: ./README.2.md
Le temps de lancement est difficile à mesurer. Mais nous pouvons vérifier la quantité de mémoire nécessaire à notre processus, en utilisant, par exemple, Activity Monitor: 852 Ko. Pas mal!
Quelques conclusions
Et donc, nous avons découvert qu'avec l'aide de Kotlin Native, nous pouvons obtenir un seul fichier exécutable avec une empreinte mémoire inférieure à celle du même Go. Victoire Pas vraiment.
Comment tout tester? Ceux qui ont travaillé avec Go ou Kotlin'om savent que dans les deux langues, il existe de bonnes solutions pour cette tâche importante. Kotlin Native a une mauvaise affaire avec ça.
Il semble que dans le
2017e JetBrains a essayé de résoudre ce problème . Mais étant donné que même les
exemples officiels de Kotlin Native n'ont pas de tests, apparemment pas encore avec succès.
Un autre problème est le développement multiplateforme. Ceux qui ont travaillé avec C plus gros que le mien ont probablement déjà remarqué que mon exemple fonctionnerait sur OSX, mais pas sur Windows, car je me fie à plusieurs fonctions disponibles uniquement avec
platform.darwin
. J'espère qu'à l'avenir Kotlin Native aura plus de wrappers qui lui permettront d'abstraire de la plate-forme, par exemple, lors de l'utilisation du système de fichiers.
Vous pouvez
trouver tous les exemples de code
ici.Et un
lien vers mon article d'origine , si vous préférez lire en anglais