Kotlin Native: acompanhe os arquivos

Quando você escreve um utilitário de linha de comando, a última coisa em que deseja confiar é que a JVM, Ruby ou Python está instalada no computador em que estará sendo executado. Eu também gostaria de ter um arquivo binário que será fácil de iniciar. E não se preocupe muito com o gerenciamento de memória.

Pelas razões acima, nos últimos anos, sempre que eu precisava escrever esses utilitários, usei o Go.

Go tem uma sintaxe relativamente simples, uma boa biblioteca padrão, há uma coleta de lixo e, na saída, obtemos um binário. Parece que o que mais é necessário?

Há não muito tempo atrás, Kotlin também começou a se experimentar em um campo semelhante na forma de Kotlin Native. A proposta parecia promissora - GC, uma única sintaxe binária, familiar e conveniente. Mas tudo é tão bom quanto gostaríamos?

O problema que temos que resolver: escreva um simples observador de arquivos no Kotlin Native. Como argumentos, o utilitário deve obter o caminho do arquivo e a frequência da verificação. Se o arquivo foi alterado, o utilitário deve criar uma cópia dele na mesma pasta com o novo nome.

Em outras palavras, o algoritmo deve ficar assim:

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

Ok, então o que queremos alcançar parece estar resolvido. Hora de escrever o código.

Quarta-feira


A primeira coisa que precisamos é de um IDE. Amantes Vim, por favor, não se preocupe.

Lançamos a familiar IntelliJ IDEA e descobrimos que, no Kotlin Native, ela não pode da palavra. Você precisa usar o CLion .

As desventuras da pessoa que encontrou C pela última vez em 2004 ainda não terminaram. Precisa de uma cadeia de ferramentas. Se você usa OSX, provavelmente o CLion encontrará a própria cadeia de ferramentas apropriada. Mas se você decidir usar o Windows e não programar em C, terá que mexer no tutorial para instalar o Cygwin .

IDE instalado, resolvido a cadeia de ferramentas. Já posso começar a escrever código? Quase.
Como o Kotlin Native ainda é um pouco experimental, o plug-in para ele no CLion não é instalado por padrão. Portanto, antes de vermos a inscrição "New Kotlin / Native Application", será necessário instalá-la manualmente .

Algumas configurações


E assim, finalmente, temos um projeto nativo do Kotlin vazio. Curiosamente, é baseado em Gradle (e não em Makefiles), e até na versão Kotlin Script.

build.gradle.kts olhada no build.gradle.kts :

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

O único plugin que usaremos é chamado Konan. Ele produzirá nosso arquivo binário.

Em konanArtifacts , especificamos o nome do arquivo executável. Neste exemplo, obtemos file_watcher.kexe

Código


É hora de mostrar o código já. Aqui está, a propósito:

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

Normalmente, os utilitários de linha de comando também possuem argumentos opcionais e seus valores padrão. Mas, por exemplo, assumiremos que sempre existem dois argumentos: path e interval

Para aqueles que já trabalharam com o Kotlin, pode parecer estranho que o path envolva sua própria classe File , sem usar o java.io.File . A explicação para isso está em um ou dois minutos.

Se você não estiver familiarizado com a função require () no Kotlin, esta é apenas uma maneira mais conveniente de validar os argumentos. Kotlin - é tudo uma questão de conveniência. Alguém poderia escrever assim:

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

Em geral, aqui está o código Kotlin usual, nada de interessante. Mas a partir de agora será divertido.

Vamos tentar escrever código Kotlin comum, mas toda vez que precisamos usar algo do Java, dizemos "oops!". Você está pronta?

Vamos voltar para o nosso while e deixar a cada interval algum símbolo, por exemplo, um período.

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

Thread é uma classe de Java. Não podemos usar classes Java no Kotlin Native. Apenas as aulas de Kotlin. Sem java.

A propósito, porque em main não usamos java.io.File

Bem, o que então pode ser usado? Funções de C!

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

Bem-vindo ao mundo C


Agora que sabemos o que nos espera, vamos ver como é a função exists() no nosso File :

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

File é uma data class simples, que nos fornece a implementação de toString() da caixa, que usaremos posteriormente.

"Sob o capô", chamamos a função access() C, que retorna -1 se esse arquivo não existir.

Mais abaixo na lista, temos a função modified() :

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

A função pode ser simplificada um pouco usando a inferência de tipo, mas aqui decidi não fazer isso, para que fique claro que a função não retorna, por exemplo, Boolean .

Existem dois detalhes interessantes nessa função. Primeiro, usamos alloc() . Como usamos C, às vezes precisamos alocar estruturas, e isso é feito manualmente em C, usando malloc ().

Essas estruturas também devem ser liberadas manualmente. A função memScoped() do Kotlin Native vem em memScoped() , o que fará isso por nós.

Resta considerar a função mais 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 } 

Aqui usamos a função C copyfile_state_alloc() , que seleciona a estrutura necessária para copyfile() .

Mas temos que liberar essa estrutura - usando
copyfile_state_free(state)

A última coisa que resta a mostrar é a geração de nomes. Há apenas um pouco de Kotlin:

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

Este é um código bastante ingênuo que ignora muitos casos, mas serve como exemplo.

Iniciar


Agora, como executar tudo isso?

Uma opção é, obviamente, usar o CLion. Ele fará tudo por nós.

Mas vamos usar a linha de comando para entender melhor o processo. Sim, e alguns ICs não executarão nosso código no CLion.

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

Primeiro de tudo, compilamos nosso projeto usando Gradle. Se tudo correu bem, a seguinte mensagem será exibida:

 BUILD SUCCESSFUL in 16s 

Dezesseis segundos ?! Sim, em comparação com alguns Go ou mesmo Kotlin para a JVM, o resultado é decepcionante. E este ainda é um projeto minúsculo.

Agora você deve ver pontos correndo pela tela. E se você alterar o conteúdo do arquivo, uma mensagem será exibida. Algo parecido com esta imagem:

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

É difícil medir o tempo de lançamento. Mas podemos verificar quanta memória nosso processo leva, usando, por exemplo, o Activity Monitor: 852KB. Nada mal!

Algumas conclusões


E assim, descobrimos que, com a ajuda do Kotlin Native, podemos obter um único arquivo executável com um espaço de memória menor que o do mesmo Go. Vitória Na verdade não.

Como testar tudo? Quem trabalhou com Go ou Kotlin'om sabe que em ambos os idiomas existem boas soluções para esta importante tarefa. O Kotlin Native tem um mau acordo até agora.

Parece que no 2017th JetBrains tentou resolver isso . Mas considerando que mesmo os exemplos oficiais nativos do Kotlin não têm testes, aparentemente ainda não com muito sucesso.

Outro problema é o desenvolvimento de plataformas cruzadas. Aqueles que trabalharam com C maior que o meu provavelmente já notaram que meu exemplo funcionaria no OSX, mas não no Windows, pois confio em várias funções disponíveis apenas no platform.darwin . Espero que, no futuro, o Kotlin Native tenha mais wrappers que permitam abstrair da plataforma, por exemplo, ao trabalhar com o sistema de arquivos.

Você pode encontrar todos os exemplos de código aqui.

E um link para o meu artigo original , se você preferir ler em inglês

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


All Articles