Wenn Sie ein Befehlszeilenprogramm schreiben, möchten Sie sich als letztes darauf verlassen, dass JVM, Ruby oder Python auf dem Computer installiert ist, auf dem es ausgeführt wird. Ich hätte auch gerne eine Binärdatei, die einfach zu starten ist. Und kümmern Sie sich nicht zu sehr um die Speicherverwaltung.
Aus den oben genannten Gründen habe ich in den letzten Jahren immer dann, wenn ich solche Dienstprogramme schreiben musste, Go verwendet.
Go hat eine relativ einfache Syntax, eine gute Standardbibliothek, es gibt eine Garbage Collection und am Ausgang erhalten wir eine Binärdatei. Es scheint, dass was noch benötigt wird?
Vor nicht allzu langer Zeit begann Kotlin auch, sich in Form von Kotlin Native auf einem ähnlichen Gebiet zu versuchen. Der Vorschlag klang vielversprechend - GC, eine einzige binäre, vertraute und bequeme Syntax. Aber ist alles so gut wie wir möchten?
Das Problem, das wir lösen müssen: Schreiben Sie einen einfachen Datei-Watcher auf Kotlin Native. Als Argumente sollte das Dienstprogramm den Dateipfad und die Häufigkeit des Scans abrufen. Wenn sich die Datei geändert hat, muss das Dienstprogramm eine Kopie davon im selben Ordner mit dem neuen Namen erstellen.
Mit anderen Worten, der Algorithmus sollte folgendermaßen aussehen:
fileToWatch = getFileToWatch() howOftenToCheck = getHowOftenToCheck() while (!stopped) { if (hasChanged(fileToWatch)) { copyAside(fileToWatch) } sleep(howOftenToCheck) }
Okay, was wir erreichen wollen, scheint geklärt zu sein. Zeit, Code zu schreiben.
Mittwoch
Das erste, was wir brauchen, ist eine IDE. Vim Liebhaber bitte keine Sorge.
Wir starten die vertraute IntelliJ IDEA und stellen fest, dass es in Kotlin Native überhaupt nicht vom Wort kommt. Sie müssen
CLion verwenden .
Die Missgeschicke der Person, die C zuletzt im Jahr 2004 begegnet ist, sind noch nicht vorbei. Benötigen Sie eine Toolchain. Wenn Sie OSX verwenden, findet CLion höchstwahrscheinlich die entsprechende Toolchain. Wenn Sie sich jedoch für Windows entscheiden und nicht in C programmieren, müssen Sie am Tutorial basteln, um
Cygwin zu installieren.
IDE installiert, sortiert die Toolchain. Kann ich bereits mit dem Schreiben von Code beginnen? Fast.
Da Kotlin Native noch etwas experimentell ist, ist das Plugin dafür in CLion nicht standardmäßig installiert. Bevor wir also die geschätzte Inschrift "New Kotlin / Native Application" sehen, muss
sie manuell installiert werden .
Einige Einstellungen
Und so haben wir endlich ein leeres Kotlin Native-Projekt. Interessanterweise basiert es auf Gradle (und nicht auf Makefiles) und sogar auf der Kotlin Script-Version.
Schauen
build.gradle.kts
sich
build.gradle.kts
:
plugins { id("org.jetbrains.kotlin.konan") version("0.8.2") } konanArtifacts { program("file_watcher") }
Das einzige Plugin, das wir verwenden werden, heißt Konan. Er wird unsere Binärdatei erstellen.
In
konanArtifacts
wir den Namen der ausführbaren Datei an. In diesem Beispiel erhalten wir
file_watcher.kexe
Code
Es ist Zeit, den Code bereits anzuzeigen. Hier ist es übrigens:
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) {
In der Regel verfügen Befehlszeilenprogramme auch über optionale Argumente und deren Standardwerte. Wir gehen jedoch beispielsweise davon aus, dass es immer zwei Argumente gibt:
path
und
interval
Für diejenigen, die bereits mit Kotlin gearbeitet haben, mag es seltsam erscheinen, dass der
path
in eine eigene
File
Klasse eingeschlossen wird, ohne
java.io.File
. Die Erklärung dafür ist in ein oder zwei Minuten.
Wenn Sie mit der Funktion require () in Kotlin nicht vertraut sind, ist dies nur eine bequemere Möglichkeit, die Argumente zu überprüfen. Kotlin - alles dreht sich um Bequemlichkeit. Man könnte so schreiben:
if (interval <= 0) { println("Interval must be positive") return }
Im Allgemeinen ist hier der übliche Kotlin-Code, nichts Interessantes. Aber von jetzt an wird es Spaß machen.
Versuchen wir, normalen Kotlin-Code zu schreiben, aber jedes Mal, wenn wir etwas aus Java verwenden müssen, sagen wir "Ups!". Bist du bereit
Kehren wir zu unserer
while
und lassen Sie jedes
interval
ein Symbol einprägen, zum Beispiel einen Punkt.
var modified = file.modified() while (true) { if (file.modified() > modified) { println("\nFile copied: ${file.copyAside()}") modified = file.modified() } print(".")
Thread
ist eine Klasse aus Java. Wir können keine Java-Klassen in Kotlin Native verwenden. Nur Kotlins Klassen. Kein Java.
Übrigens, weil wir
main
java.io.File
nicht verwendet
java.io.File
Nun, was kann dann verwendet werden? Funktionen von C!
var modified = file.modified() while (true) { if (file.modified() > modified) { println("\nFile copied: ${file.copyAside()}") modified = file.modified() } print(".") sleep(interval) }
Willkommen in der Welt C.
Nachdem wir nun wissen, was uns erwartet, wollen wir in unserer
File
sehen, wie die Funktion exist
exists()
aussieht:
data class File(private val filename: String) { fun exists(): Boolean { return access(filename, F_OK) != -1 }
File
ist eine einfache
data class
, die uns die Implementierung von
toString()
aus der Box gibt, die wir später verwenden werden.
"Unter der Haube" rufen wir die
access()
Funktion C auf, die
-1
zurückgibt, wenn eine solche Datei nicht existiert.
Weiter unten in der Liste haben wir die Funktion
modified()
:
fun modified(): Long = memScoped { val result = alloc<stat>() stat(filename, result.ptr) result.st_mtimespec.tv_sec }
Die Funktion könnte durch Typinferenz etwas vereinfacht werden, aber hier habe ich beschlossen, dies nicht zu tun, damit klar ist, dass die Funktion beispielsweise nicht
Boolean
.
Diese Funktion enthält zwei interessante Details. Zuerst verwenden wir
alloc()
. Da wir C verwenden, müssen wir manchmal Strukturen zuweisen, und dies erfolgt in C manuell mit malloc ().
Diese Strukturen müssen auch manuell freigegeben werden. Die
memScoped()
-Funktion von Kotlin Native
memScoped()
, was für uns
memScoped()
wird.
Es bleibt uns
opyAside()
die gewichtigste Funktion zu betrachten:
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 }
Hier verwenden wir die C-Funktion
copyfile_state_alloc()
, die die für
copyfile()
benötigte Struktur auswählt.
Aber wir müssen diese Struktur selbst freigeben - mit
copyfile_state_free(state)
Das Letzte, was noch zu zeigen ist, ist die Generierung von Namen. Es gibt nur einen kleinen Kotlin:
private var count = 0 private val extension = filename.substringAfterLast(".") private fun generateFilename() = filename.replace(extension, "${++count}.$extension")
Dies ist ein ziemlich naiver Code, der viele Fälle ignoriert, aber für ein Beispiel ausreicht.
Starten Sie
Wie läuft das alles?
Eine Möglichkeit ist natürlich die Verwendung von CLion. Er wird alles für uns tun.
Verwenden wir stattdessen die Befehlszeile, um den Prozess besser zu verstehen. Ja, und einige CI führen unseren Code nicht über CLion aus.
./gradlew build && ./build/konan/bin/macos_x64/file_watcher.kexe ./README.md 1
Zunächst kompilieren wir unser Projekt mit Gradle. Wenn alles gut gegangen ist, wird die folgende Meldung angezeigt:
BUILD SUCCESSFUL in 16s
Sechzehn Sekunden ?! Ja, im Vergleich zu einigen Go oder sogar Kotlin für die JVM ist das Ergebnis enttäuschend. Und das ist immer noch ein winziges Projekt.
Sie sollten jetzt Punkte auf dem Bildschirm sehen. Wenn Sie den Inhalt der Datei ändern, wird eine Meldung angezeigt. So etwas wie dieses Bild:
................................ File copied: ./README.1.md ................... File copied: ./README.2.md
Die Startzeit ist schwer zu messen. Wir können jedoch überprüfen, wie viel Speicher unser Prozess benötigt, indem wir beispielsweise Activity Monitor: 852 KB verwenden. Nicht schlecht!
Einige Schlussfolgerungen
Und so haben wir herausgefunden, dass wir mit Hilfe von Kotlin Native eine einzelne ausführbare Datei mit einem Speicherbedarf erhalten können, der geringer ist als der des gleichen Go. Sieg Nicht wirklich.
Wie teste ich alles? Diejenigen, die mit Go oder Kotlin'om gearbeitet haben, wissen, dass es in beiden Sprachen gute Lösungen für diese wichtige Aufgabe gibt. Kotlin Native hat bisher ein schlechtes Geschäft damit.
Es scheint, dass
JetBrains im Jahr
2017 versucht hat, dies zu lösen . Aber wenn man bedenkt, dass selbst die
offiziellen Beispiele der
Kotlin Native keine Tests haben, anscheinend noch nicht zu erfolgreich.
Ein weiteres Problem ist die plattformübergreifende Entwicklung. Diejenigen, die mit C gearbeitet haben, das größer als meins ist, haben wahrscheinlich bereits bemerkt, dass mein Beispiel unter OSX funktioniert, aber nicht unter Windows, da ich auf mehrere Funktionen angewiesen bin, die nur mit
platform.darwin
verfügbar
platform.darwin
. Ich hoffe, dass Kotlin Native in Zukunft mehr Wrapper haben wird, die es ihm ermöglichen, von der Plattform zu abstrahieren, beispielsweise wenn er mit dem Dateisystem arbeitet.
Alle Codebeispiele finden Sie
hier.Und ein
Link zu meinem Originalartikel , wenn Sie lieber auf Englisch lesen möchten