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) {
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
é 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 }
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