Kotlin Native: realiza un seguimiento de los archivos

Cuando escribe una utilidad de línea de comandos, lo último en lo que quiere confiar es que JVM, Ruby o Python están instalados en la computadora donde se ejecutará. También me gustaría tener un archivo binario que sea fácil de iniciar. Y no se preocupe demasiado con la gestión de la memoria.

Por las razones anteriores, en los últimos años, cada vez que necesitaba escribir tales utilidades, usaba Go.

Go tiene una sintaxis relativamente simple, una buena biblioteca estándar, hay una recolección de basura y en la salida obtenemos un binario. Parece que ¿qué más se necesita?

No hace mucho tiempo, Kotlin también comenzó a probarse en un campo similar en forma de Kotlin Native. La propuesta sonaba prometedora: GC, una única sintaxis binaria, familiar y conveniente. ¿Pero es todo tan bueno como nos gustaría?

El problema que tenemos que resolver es escribir un simple observador de archivos en Kotlin Native. Como argumentos, la utilidad debería obtener la ruta del archivo y la frecuencia del escaneo. Si el archivo ha cambiado, la utilidad debe crear una copia del mismo en la misma carpeta con el nuevo nombre.

En otras palabras, el algoritmo debería verse así:

fileToWatch = getFileToWatch() howOftenToCheck = getHowOftenToCheck() while (!stopped) { if (hasChanged(fileToWatch)) { copyAside(fileToWatch) } sleep(howOftenToCheck) } 

Bien, entonces lo que queremos lograr parece estar resuelto. Hora de escribir el código.

Miercoles


Lo primero que necesitamos es un IDE. Vim amantes por favor no te preocupes.

Lanzamos el conocido IntelliJ IDEA y descubrimos que en Kotlin Native no puede de la palabra en absoluto. Necesitas usar CLion .

Las desventuras de la persona que se encontró con C por última vez en 2004 aún no han terminado. Necesito una cadena de herramientas. Si usa OSX, lo más probable es que CLion encuentre la cadena de herramientas adecuada. Pero si decide utilizar Windows y no programar en C, tendrá que jugar con el tutorial para instalar Cygwin .

IDE instalado, resuelto la cadena de herramientas. ¿Ya puedo comenzar a escribir código? Casi.
Dado que Kotlin Native todavía es algo experimental, el complemento para CLion no está instalado de manera predeterminada. Entonces, antes de que veamos la preciada inscripción "Nueva aplicación Kotlin / Native" tendrá que instalarla manualmente .

Algunas configuraciones


Y así, finalmente tenemos un proyecto Kotlin Native vacío. Curiosamente, se basa en Gradle (y no en Makefiles), e incluso en la versión Kotlin Script.

build.gradle.kts vistazo a build.gradle.kts :

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

El único complemento que usaremos se llama Konan. Producirá nuestro archivo binario.

En konanArtifacts especificamos el nombre del archivo ejecutable. En este ejemplo, obtenemos file_watcher.kexe

Código


Es hora de mostrar el código ya. Aquí está, por cierto:

 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 } } 

Por lo general, las utilidades de línea de comandos también tienen argumentos opcionales y sus valores predeterminados. Pero, por ejemplo, asumiremos que siempre hay dos argumentos: path e interval

Para aquellos que ya han trabajado con Kotlin, puede parecer extraño que la path ajuste en su propia clase File , sin usar java.io.File . La explicación de esto es en un minuto o dos.

Si no está familiarizado con la función require () en Kotlin, esta es solo una forma más conveniente de validar los argumentos. Kotlin: se trata de conveniencia. Se podría escribir así:

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

En general, aquí está el código Kotlin habitual, nada interesante. Pero a partir de ahora será divertido.

Intentemos escribir código Kotlin normal, pero cada vez que necesitamos usar algo de Java, decimos "¡Uy!". Estas listo

Volvamos a nuestro while y dejemos que imprima en cada interval algún símbolo, por ejemplo, un punto.

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

Thread es una clase de Java. No podemos usar clases Java en Kotlin Native. Solo las clases de Kotlin. No java

Por cierto, porque en main no utilizamos java.io.File

Bueno, entonces, ¿qué se puede usar? Funciones de C!

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

Bienvenido al mundo C


Ahora que sabemos lo que nos espera, veamos cómo se ve la función exists() desde nuestro File :

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

File es una data class simple, que nos da la implementación de toString() desde el cuadro, que usaremos más adelante.

"Bajo el capó", llamamos a la función de access() C, que devuelve -1 si dicho archivo no existe.

Más abajo en la lista tenemos la función modified() :

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

La función podría simplificarse un poco usando la inferencia de tipos, pero aquí decidí no hacer esto, de modo que estaba claro que la función no retorna, por ejemplo, Boolean .

Hay dos detalles interesantes en esta función. Primero, usamos alloc() . Como usamos C, a veces necesitamos asignar estructuras, y esto se hace en C manualmente, usando malloc ().

Estas estructuras también deben liberarse manualmente. La función memScoped() de Kotlin Native viene al memScoped() , que lo hará por nosotros.

Nos queda por considerar la función más importante: 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 } 

Aquí usamos la función C copyfile_state_alloc() , que selecciona la estructura necesaria para copyfile() .

Pero tenemos que liberar esta estructura nosotros mismos, utilizando
copyfile_state_free(state)

Lo último que queda por mostrar es la generación de nombres. Solo hay un pequeño Kotlin:

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

Este es un código bastante ingenuo que ignora muchos casos, pero servirá como ejemplo.

Inicio


¿Ahora cómo ejecutarlo todo?

Una opción es, por supuesto, usar CLion. Él hará todo por nosotros.

Pero usemos la línea de comando para comprender mejor el proceso. Sí, y algunos CI no ejecutarán nuestro código desde CLion.

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

En primer lugar, compilamos nuestro proyecto usando Gradle. Si todo salió bien, aparecerá el siguiente mensaje:

 BUILD SUCCESSFUL in 16s 

¿Dieciséis segundos? Sí, en comparación con algunos Go o incluso Kotlin para la JVM, el resultado es decepcionante. Y este sigue siendo un pequeño proyecto.

Ahora debería ver puntos corriendo por la pantalla. Y si cambia el contenido del archivo, aparecerá un mensaje. Algo así como esta imagen:

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

El tiempo de lanzamiento es difícil de medir. Pero podemos verificar cuánta memoria ocupa nuestro proceso, utilizando, por ejemplo, Activity Monitor: 852KB. No esta mal!

Algunas conclusiones


Y así, descubrimos que con la ayuda de Kotlin Native podemos obtener un solo archivo ejecutable con una huella de memoria menor que la del mismo Go. Victoria En realidad no

¿Cómo probarlo todo? Quienes trabajaron con Go o Kotlin'om saben que en ambos idiomas hay buenas soluciones para esta importante tarea. Kotlin Native tiene un mal trato hasta ahora.

Parece que en el 2017th JetBrains intentó resolver esto . Pero teniendo en cuenta que incluso los ejemplos nativos de Kotlin oficiales no tienen pruebas, aparentemente todavía no tienen demasiado éxito.

Otro problema es el desarrollo de plataforma cruzada. Aquellos que trabajaron con C más grande que el mío probablemente ya notaron que mi ejemplo funcionaría en OSX, pero no en Windows, ya que confío en varias funciones disponibles solo con platform.darwin . Espero que en el futuro Kotlin Native tenga más envoltorios que le permitan abstraerse de la plataforma, por ejemplo, cuando trabaje con el sistema de archivos.

Puede encontrar todos los ejemplos de código aquí.

Y un enlace a mi artículo original , si prefieres leer en inglés

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


All Articles