Kotlin Native: Watch Files

    When 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 path to the file and the 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 launch 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 the 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.

    Look in 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. It will produce our binary file.

    In konanArtifacts we specify the name of the executable file. In this example, we getfile_watcher.kexe

    Code


    It’s time to show the code already. Here it is, by the way:

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

    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 it path turns into its own class File, without use 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 us return to ours while, and let it imprints every single interval character, for example a dot.

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

    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 main we didn’t use java.io.File

    Ok, but what then can I 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 awaits us, let's see what the function exists()from ours looks like File:

    dataclassFile(privateval filename: String) {
        funexists(): Boolean {
            return access(filename, F_OK) != -1
        }
        // More functions...
    }

    File it's simple data class, which gives us the implementation toString()of the box, which we then use.

    "Under the hood," we call the C function access(), which returns -1if no such file exists.

    Next on the list we have a function modified():

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

    Function could be a bit easier using type inference, but then I decided not to do it, to make it 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, with the help of malloc ().

    These structures also need to be released manually. Here comes to the rescue function memScoped()from Kotlin Native, which will do it for us.

    It remains for us to consider the most weighty function:сopyAside()

    funcopyAside(): 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 necessary copyfile()structure.

    But we have to release this structure ourselves — using the
    copyfile_state_free(state)

    last thing that remains to be shown — this is the generation of names. There is just a bit of Kotlin:

    privatevar count = 0privateval extension = filename.substringAfterLast(".")
    privatefungenerateFilename() = 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 the JVM, the result is disappointing. And this is a tiny project.

    Now you should 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 have no tests, it seems that it’s not very successful yet.

    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 withplatform.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 you can find here.

    And the link to my original article , if you prefer to read English

    Also popular now: