When you write a command line utility, the last thing you want to rely on is that the JVM, Ruby or Python is installed on the computer where it will be run. I would also like to have one binary file at the output, which will be easy to run. And don't bother too much with memory management.
For the above reasons, in recent years, whenever I needed to write such utilities, I used Go.
Go has relatively simple syntax, a good standard library, there is a garbage collection, and at the output we get one binary. It would seem that still need?
')
Not so long ago, Kotlin also began to try himself in a similar field in the form of Kotlin Native. The proposal sounded promising - GC, a single binary, a familiar and convenient syntax. But is everything as good as we would like?
The task we have to solve is: write a simple file watcher on Kotlin Native. As arguments, the utility should receive the file path and frequency of the scan. If the file is changed, the utility must create a copy of it in the same folder with the new name.
In other words, the algorithm should look like this:
fileToWatch = getFileToWatch() howOftenToCheck = getHowOftenToCheck() while (!stopped) { if (hasChanged(fileToWatch)) { copyAside(fileToWatch) } sleep(howOftenToCheck) }
Okay, so we want to achieve what seems to be sorted out. Time to write code.
Wednesday
The first thing we need is an IDE. Vim lovers will ask not to worry.
We run the familiar IntelliJ IDEA and find out that in Kotlin Native it cannot be from the word at all. Need to use
CLion .
The misadventures of a person who last encountered C in 2004 are not over yet. Need a toolchain. If you are using OSX, CLion will most likely find the appropriate toolchain itself. But if you decide to use Windows and do not program in C, you will have to tinker with the tutorial to install some
Cygwin .
The IDE was installed, the toolchain was sorted out. Can I start writing code already? Nearly.
Since Kotlin Native is somewhat experimental, the plugin for it in CLion is not installed by default. So before we see the cherished inscription "New Kotlin / Native Application" will have to
install it manually .
Some settings
And so, finally we have an empty Kotlin Native project. Interestingly, it is based on Gradle (and not on Makefiles), and even on Kotlin Script version.
Let's look at
build.gradle.kts
:
plugins { id("org.jetbrains.kotlin.konan") version("0.8.2") } konanArtifacts { program("file_watcher") }
The only plugin we will use is called Konan. This will produce our binary file.
In
konanArtifacts
we specify the name of the executable file. In this example, we get
file_watcher.kexe
Code
It’s time to show the code already. Here it is, by the way:
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) {
Usually, command line utilities also have optional arguments and their default values. But for example, we will assume that there are always two arguments:
path
and
interval
For those who have already worked with Kotlin, it may seem strange that the
path
turns into its own
File
class, without using
java.io.File
. The explanation for this is in a minute or two.
If you are not familiar with the require () function in Kotlin, this is simply a more convenient way to validate arguments. Kotlin - it's all about convenience. One could write like this:
if (interval <= 0) { println("Interval must be positive") return }
In general, here is the usual Kotlin code, nothing interesting. But from now on it will be more fun.
Let's try to write regular Kotlin code, but every time we need to use something from Java, we say “Oops!”. Ready?
Let's return to our
while
, and let it stamps each
interval
some character, for example a point.
var modified = file.modified() while (true) { if (file.modified() > modified) { println("\nFile copied: ${file.copyAside()}") modified = file.modified() } print(".")
Thread
is a class from Java. We cannot use Java classes in Kotlin Native. Only Kotlin'ovskie classes. No java.
By the way, that's why we didn't use
java.io.File
in
main
java.io.File
Well, then what can you use? Functions from C!
var modified = file.modified() while (true) { if (file.modified() > modified) { println("\nFile copied: ${file.copyAside()}") modified = file.modified() } print(".") sleep(interval) }
Welcome to world C
Now that we know what is waiting for us, let's see what the
exists()
function looks like from our
File
:
data class File(private val filename: String) { fun exists(): Boolean { return access(filename, F_OK) != -1 }
File
is a simple
data class
, which gives us the implementation of
toString()
out of the box, which we will use later.
“Under the hood” we call the C
access()
function, which returns
-1
if no such file exists.
Next on the list we have the function
modified()
:
fun modified(): Long = memScoped { val result = alloc<stat>() stat(filename, result.ptr) result.st_mtimespec.tv_sec }
The function could be slightly simplified using type inference, but then I decided not to do this so that it was clear that the function does not return, for example,
Boolean
.
There are two interesting details in this feature. First, we use
alloc()
. Since we use C, sometimes it is necessary to select structures, and this is done in C manually, using malloc ().
These structures also need to be released manually. Here comes the
memScoped()
function from Kotlin Native, which will do it for us.
It remains for us to consider the most weighty function:
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 }
Here we use the C function
copyfile_state_alloc()
, which selects the structure necessary for
copyfile()
.
But we also have to release this structure ourselves — using
copyfile_state_free(state)
The last thing left to show is the generation of names. There is just a bit of Kotlin:
private var count = 0 private val extension = filename.substringAfterLast(".") private fun generateFilename() = filename.replace(extension, "${++count}.$extension")
This is a rather naive code that ignores many cases, but for example it will do.
Start
Now how to start all this?
One option is to use CLion, of course. He will do everything for us.
But let's use the command line instead to better understand the process. And any CI will not launch our code from CLion.
./gradlew build && ./build/konan/bin/macos_x64/file_watcher.kexe ./README.md 1
First we compile our project using Gradle. If everything went well, the following message appears:
BUILD SUCCESSFUL in 16s
Sixteen seconds ?! Yes, in comparison with some Go or even Kotlin for JVM, the result is disappointing. And this is a tiny project.
You should now see the points running across the screen. And if you change the contents of the file, a message will appear about it. Something like this picture:
................................ File copied: ./README.1.md ................... File copied: ./README.2.md
Start time is difficult to measure. But we can check how much memory our process takes, using for example the Activity Monitor: 852KB. Not bad!
Few conclusions
And so, we found out that with the help of Kotlin Native we can get a single executable file with a memory footprint smaller than that of Go. Victory? Not really.
How to test all this? Those who worked with Go or Kotlin know that in both languages ​​there are good solutions for this important task. With Kotlin Native, this is bad for now.
It seems that in
2017 JetBrains tried to solve this . But considering that even the
official examples of Kotlin Native do not have tests, apparently not too successfully.
Another problem is crossplatform development. Those who have worked with C more than mine have probably already noticed that my example will work on OSX, but not on Windows, since I rely on several functions available only from
platform.darwin
. I hope that in the future, Kotlin Native will have more wrappers that will abstract from the platform, for example when working with the file system.
All code examples can
be found here.And a
link to my original article , if you prefer to read English