Kotlin Native: WatchYour Files

Kotlin Native: WatchYour FilesAlex MaisonBlockedUnblockFollowFollowingFeb 5When you write a command line utility, the last thing you want to rely on is that 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 a 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.

FieldThe 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.

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 settingsAnd so, finally, we have an empty Kotlin Native project.

Interestingly, it is based on Gradle, 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.

kexeCodeIt’s time to show the code already: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() ?: 0require(file.

exists()) { "No such file: $file" }require(interval > 0) { "Interval must be positive" }while (true) { // We should do something here }}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 intervalFor 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("?.File copied: ${file.

copyAside()}") modified = file.

modified() } print(".

") // Opps.

Thread.

sleep(interval * 1000)}A thread is a class from Java.

We cannot use Java classes in Kotlin Native.

By the way, that’s why we didn’t use java.

io.

File in main.

Well, then what can you use!.Functions from C!var modified = file.

modified()while (true) { if (file.

modified() > modified) { println("?.File copied: ${file.

copyAside()}") modified = file.

modified() } print(".

") sleep(interval)}Welcome to the C worldNow 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 } // More functions.

}A 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: copyAside ()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 — usingcopyfile_state_free (state)The last thing left to show is the generation of names.

There is just a bit of Kotlin:private var count = 0private 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.

StartNow 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 1First, we compile our project using Gradle.

If everything went well, the following message appears:BUILD SUCCESSFUL in 16sSixteen 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.

mdStart time is difficult to measure.

But we can check how much memory our process takes, using, for example, the Activity Monitor: 852KB.

Not bad!ConclusionsAnd 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 cross-platform 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.

.

. More details

Leave a Reply