Kotlin Native: garder une trace des fichiers

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) { // We should do something here } } 

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.sleep(interval * 1000) } 

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 } // More functions... } 

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

Source: https://habr.com/ru/post/fr435220/


All Articles