Background

I’m a software engineer at ThreeFoldTech and the author of Nim Days

One of the projects we develop at ThreeFoldTech is Zero-OS a stateless Linux operating system designed for clustered deployments to host virtual machines and containerized applications. We wanted to have a CLI (like docker) to manage the containers and communicate with zero-os instead of using Python client.

Application requirements

  • single binary
  • zos should be like docker for dockerd
  • commands to interact with zero-os (via redis)
  • subcommands to interact with containers on zero-os
  • documentation (soft documentation, hard documentation)
  • tabular output for humans (listing containers and such)
  • support json output when needed too (for further manipulation by tools like jq)

Sounds simple enough. Any language would do just fine

Choosing Nim

From Nim website

Nim is a systems and applications programming language. Statically typed and compiled, it provides unparalleled performance in an elegant package.

  • High-performance garbage-collected language
  • Compiles to C, C++ or JavaScript
  • Produces dependency-free binaries
  • Runs on Windows, macOS, Linux, and more

In the upcoming sections, I’ll talk about the good, the okay, and the hard points I faced while developing this simple CLI application with the requirements above.

The good

Static typing

Nim eliminates a whole class of errors by being statically typed

Expressiveness

Nim is like python (whitespace sensitive language) and there’s even a guide on the official repo Nim for Python programmers. Seeing some of Pascal concepts in Nim gets me very nostalgic too.

import strutils, strformat, os, ospaths, osproc, tables, parsecfg, json, marshal, logging
import net, asyncdispatch, asyncnet, streams, threadpool, uri
import logging
import algorithm
import base64

import redisclient, redisparser
import asciitables
import docopt
proc checkContainerExists*(this:App, containerid:int): bool=
  ## checks if container `containerid` exists or not
  try:
    discard this.containerInfo(containerid)
    result = true
  except:
    result = false

I find UFCS (Uniform Function Call Syntax) really great too excellent nim basics

proc plus(x, y: int): int =  # <1>
  return x + y

proc multi(x, y: int): int =
  return x * y

let
  a = 2
  b = 3
  c = 4

echo a.plus(b) == plus(a, b)
echo c.multi(a) == multi(c, a)


echo a.plus(b).multi(c)  # <2>
echo c.multi(b).plus(a)  # <3>

Also case insensitivity toUpper toupper to_upper is pretty neat

I don’t use the same identifier with different cases in the same scope

type ContainerInfo* = object of RootObj
  id*: string
  cpu*: float
  root*: string
  hostname*: string
  name*: string
  storage*: string
  pid*: int
  ports*: string

I like the way of defining types, enums and access control * means public.

Developing sync, async in the same interface

Pragmas are Nim’s method to give the compiler additional information/commands without introducing a massive number of new keywords. Pragmas are processed on the fly during semantic checking. Pragmas are enclosed in the special {. and .} curly brackets. Pragmas are also often used as a first implementation to play with a language feature before a nicer syntax to access the feature becomes available.

I’m a fan of multisync pragma because it allows you to define procs for async, sync code easily

proc readMany(this:Redis|AsyncRedis, count:int=1): Future[string] {.multisync.} =
  if count == 0:
    return ""
  let data = await this.receiveManaged(count)
  return data

Basically in sync execution multisync will remove Future, and await from the code definition and will leave them in case of async execution

The tooling

vscode-nim

vscode-nim is my daily driver, works as expected, but sometimes it consumes so much memory. there’s also LSP in the works

nimble

Everything you expect from the package manager, creating projects, custom tasks, managing dependencies and publishing (too coupled with github, but that’s fine with me)

Generating documentation

nim doc is the default tool in Nim to generate indexed and searchable documentation for the project

Here’s a nimble task to generate documentation

task genDocs, "Create code documentation for zos":
    exec "nim doc --project src/zos.nim "
nim doc src/zos.nim 
Hint: used config file '/home/xmonader/.choosenim/toolchains/nim-0.19.0/config/nim.cfg' [Conf]
Hint: used config file '/home/xmonader/.choosenim/toolchains/nim-0.19.0/config/nimdoc.cfg' [Conf]
Hint: system [Processing]
Hint: zos [Processing]
Hint: strutils [Processing]
Hint: parseutils [Processing]
Hint: math [Processing]
Hint: bitops [Processing]
Hint: algorithm [Processing]
Hint: unicode [Processing]
Hint: strformat [Processing]
Hint: macros [Processing]
Hint: os [Processing]
Hint: times [Processing]
Hint: options [Processing]
Hint: typetraits [Processing]
Hint: posix [Processing]
Hint: ospaths [Processing]
Hint: osproc [Processing]
Hint: strtabs [Processing]
Hint: hashes [Processing]
Hint: streams [Processing]
Hint: cpuinfo [Processing]
Hint: linux [Processing]
Hint: tables [Processing]
Hint: parsecfg [Processing]
Hint: lexbase [Processing]
Hint: json [Processing]
Hint: parsejson [Processing]
Hint: marshal [Processing]
Hint: typeinfo [Processing]
Hint: intsets [Processing]
Hint: logging [Processing]
Hint: net [Processing]
Hint: nativesockets [Processing]
Hint: winlean [Processing]
Hint: dynlib [Processing]
Hint: sets [Processing]
Hint: openssl [Processing]
Hint: asyncdispatch [Processing]
Hint: heapqueue [Processing]
Hint: lists [Processing]
Hint: asyncstreams [Processing]
Hint: asyncfutures [Processing]
Hint: deques [Processing]
Hint: cstrutils [Processing]
Hint: asyncnet [Processing]
Hint: threadpool [Processing]
../../../.choosenim/toolchains/nim-0.19.0/lib/pure/concurrency/threadpool.nim(13, 10) Error: Threadpool requires --threads:on option.
Hint: cpuload [Processing]
Hint: locks [Processing]
../../../.choosenim/toolchains/nim-0.19.0/lib/pure/concurrency/threadpool.nim(67, 26) Error: undeclared identifier: 'fence'
../../../.choosenim/toolchains/nim-0.19.0/lib/pure/concurrency/threadpool.nim(67, 31) Error: attempting to call undeclared routine: 'fence'
../../../.choosenim/toolchains/nim-0.19.0/lib/pure/concurrency/threadpool.nim(67, 31) Error: attempting to call undeclared routine: 'fence'
../../../.choosenim/toolchains/nim-0.19.0/lib/pure/concurrency/threadpool.nim(67, 31) Error: expression 'fence' cannot be called
../../../.choosenim/toolchains/nim-0.19.0/lib/pure/concurrency/threadpool.nim(78, 8) Error: attempting to call undeclared routine: 'fence'
../../../.choosenim/toolchains/nim-0.19.0/lib/pure/concurrency/threadpool.nim(78, 8) Error: attempting to call undeclared routine: 'fence'
../../../.choosenim/toolchains/nim-0.19.0/lib/pure/concurrency/threadpool.nim(78, 8) Error: expression 'fence' cannot be called
../../../.choosenim/toolchains/nim-0.19.0/lib/pure/concurrency/threadpool.nim(81, 10) Error: attempting to call undeclared routine: 'fence'
../../../.choosenim/toolchains/nim-0.19.0/lib/pure/concurrency/threadpool.nim(81, 10) Error: attempting to call undeclared routine: 'fence'
../../../.choosenim/toolchains/nim-0.19.0/lib/pure/concurrency/threadpool.nim(81, 10) Error: expression 'fence' cannot be called
../../../.choosenim/toolchains/nim-0.19.0/lib/pure/concurrency/threadpool.nim(83, 10) Error: attempting to call undeclared routine: 'fence'
../../../.choosenim/toolchains/nim-0.19.0/lib/pure/concurrency/threadpool.nim(83, 10) Error: attempting to call undeclared routine: 'fence'
../../../.choosenim/toolchains/nim-0.19.0/lib/pure/concurrency/threadpool.nim(83, 10) Error: expression 'fence' cannot be called
../../../.choosenim/toolchains/nim-0.19.0/lib/pure/concurrency/threadpool.nim(360, 37) Error: undeclared identifier: 'Thread'
../../../.choosenim/toolchains/nim-0.19.0/lib/pure/concurrency/threadpool.nim(360, 43) Error: no generic parameters allowed for Thread
../../../.choosenim/toolchains/nim-0.19.0/lib/pure/concurrency/threadpool.nim(363, 54) Error: no generic parameters allowed for Thread
../../../.choosenim/toolchains/nim-0.19.0/lib/pure/concurrency/threadpool.nim(391, 3) Error: undeclared identifier: 'createThread'
../../../.choosenim/toolchains/nim-0.19.0/lib/pure/concurrency/threadpool.nim(391, 15) Error: attempting to call undeclared routine: 'createThread'
../../../.choosenim/toolchains/nim-0.19.0/lib/pure/concurrency/threadpool.nim(391, 15) Error: attempting to call undeclared routine: 'createThread'
../../../.choosenim/toolchains/nim-0.19.0/lib/pure/concurrency/threadpool.nim(391, 15) Error: expression 'createThread' cannot be called
../../../.choosenim/toolchains/nim-0.19.0/lib/pure/concurrency/threadpool.nim(404, 15) Error: attempting to call undeclared routine: 'createThread'
../../../.choosenim/toolchains/nim-0.19.0/lib/pure/concurrency/threadpool.nim(404, 15) Error: attempting to call undeclared routine: 'createThread'
../../../.choosenim/toolchains/nim-0.19.0/lib/pure/concurrency/threadpool.nim(404, 15) Error: expression 'createThread' cannot be called
Hint: uri [Processing]
Hint: base64 [Processing]
Hint: redisclient [Processing]
Hint: redisparser [Processing]
Hint: sequtils [Processing]
Hint: docopt [Processing]
Hint: nre [Processing]
Hint: pcre [Processing]
Hint: util [Processing]
Hint: util [Processing]
Hint: logger [Processing]
Hint: settings [Processing]
Hint: apphelp [Processing]
Hint: errorcodes [Processing]
Hint: sshexec [Processing]
Hint: hostnamegenerator [Processing]
Hint: random [Processing]
Hint: app [Processing]
Hint: asciitables [Processing]
Hint: zosclient [Processing]
Hint: uuids [Processing]
Hint: isaac [Processing]
Hint: urandom [Processing]
Hint: vbox [Processing]

No idea why generating docs gives these errors (most likely because I’m using threadpool in my code?) so I went with my gut feeling and --threads:on

task genDocs, "Create code documentation for zos":
    exec "nim doc  --threads:on --project src/zos.nim "

and now it works just fine, and earned its place in the Good parts.

the OK

These are the OK parts that can be improved in my opinion

Documentation

There’s a great community effort to provide documentation. I hope we get more and more soft documentation and better quality on the official docs too.

Weird symbols / json

Nim chooses unreadable symbols %* and $$ over clear names like dumps or loads :(

Error Messages

Sometimes the error messages aren’t good enough. For instance, I got i is not accessible and even with using writeStackTrace I couldn’t get anything useful. So I grepped the codebase where accessible comes from and continued from there.

Another example was this

timeddoutable.nim(44, 16) template/generic instantiation from here
timeddoutable.nim(34, 6) Error: type mismatch: got <Thread[ptr Channel[system.bool]], proc (cancelChan: ptr Channel[system.bool]):bool{.gcsafe, locks: 0.}, ptr Channel[system.bool]>
but expected one of:
proc createThread[TArg](t: var Thread[TArg];
                       tp: proc (arg: TArg) {.thread, nimcall.}; param: TArg)
  first type mismatch at position: 2
  required type: proc (arg: TArg){.gcsafe.}
  but expression 'p' is of type: proc (cancelChan: ptr Channel[system.bool]): bool{.gcsafe, locks: 0.}
proc createThread(t: var Thread[void]; tp: proc () {.thread, nimcall.})
  first type mismatch at position: 1
  required type: var Thread[system.void]
  but expression 't' is of type: Thread[ptr Channel[system.bool]]

expression: createThread(t, p, addr(cancelChan))

While the error is clear I just had a hard time reading it

The Hard

I really considered switching to language with a more mature ecosystem for these points (multiple times)

Static linking

Nim promises Produces dependency-free binaries as stated on its website, but getting a static linked binary is hard, and undocumented process while it was one of the cases I hoped to use Nim for.

I managed to statically link with PCRE and SSL with lots of help from the community.

Dynamic linking

Building on Mac OSX with SSL is no fun, specially when your SSL isn’t 1.1 [I managed to do with lots of help from the community]

brew install openssl@1.1

nim c -d:ssl  --dynlibOverride:ssl --dynlibOverride:crypto --threads:on --passC:'-I/usr/local/opt/openssl\@1.1/include/' --passL:'-lssl -lcrypto -lpcre' --passL:'-L/usr/local/opt/openssl\@1.1/lib/' src/zos.nim

Developing a redisclient

We have a redis protocol keyvalue store 0-db that I needed to work against a while ago, and I found a major problem with the implementation of the parser and the client in the official nim redis library. So I had to roll my own parser/client

Developing asciitable library

To show a table listing all of the containers (id, name, open ports and image it’s running from) I needed an ascii table library in Nim (I found 0 libraries). I had to write my own nim-asciitables

Nim-JWT

In the transport layer, we send a JWT token to request extra privileges on zero-os and for that, I needed jwt support. Again, jwt libraries are far from complete in Nim and had to try to fix it ES384 support with that fix I was able to get the claims, but I couldn’t really verify it with the public key :( So I decided not to do client side validation and leave the validation to zero-os (the backend)

Concurrency and communication

In some parts of the application we want to add the ability to timeout after some period of time, and Nim supports multithreading using threadpool and async/await combo and has HTTPBeast, So that shouldn’t be a problem.

When I saw Channels and spawn I thought it’d be as easy as goroutines in Go or fibers in Crystal

So that was my first try with spawn

import os, threadpool

var cancelChan: Channel[bool]

cancelChan.open()

proc p1():bool=
    result = true
    for i in countup(0,50):
        echo "p1 Doing action"
        sleep(1000)
        let (hasData, msg) = cancelChan.tryRecv()
        if msg == true:
            echo "Cancelling p1"
            return 
    echo "Done p1..."

proc p2(): bool =
    result = true
    for i in countup(0,5):
        echo "p2 Doing action"
        sleep(1000)
        let (hasData, msg) = cancelChan.tryRecv()
        if msg == true:
            echo "Cancelling p1"
            return
    echo "Done p2"


proc timeoutable(p:proc, timeout=10)= 
    var t = (spawn p())
    for i in countup(0, timeout):
        if t.isReady():
            return
        sleep(1000)

    cancelChan.send(true)

when isMainModule:
    timeoutable(p1)
    timeoutable(p2)

However, The Nim creator Andreas Rumpf said using Spawn/Channels is a bad idea and channels are meant to be used with Threads, So I tried to move it to threads

import os, threadpool

type Args = tuple[cancelChan:ptr Channel[bool], respChan: ptr Channel[bool]]

proc p1(a: Args): void {.thread.}=
    var cancelChan = a.cancelChan[]
    var respChan = a.respChan[]
    for i in countup(0,50):
        let (hasData, msg) = cancelChan.tryRecv()
        echo "p1 HASDATA: " & $hasData
        echo "p1 MSG: " & $msg
        if hasData == true:
            echo "Cancelling p1"
            respChan.send(false)
            return 
        echo "p1 Doing action"
        sleep(1000)

    echo "Done p1..."
    respChan.send(true)

proc p2(a: Args): void {.thread.}=
    var cancelChan = a.cancelChan[]
    var respChan = a.respChan[]
    for i in countup(0,5):
        let (hasData, msg) = cancelChan.tryRecv()
        echo "p2 HASDATA: " & $hasData
        echo "p2 MSG: " & $msg
        if hasData:
            echo "proc cancelled successfully" 
            respChan.send(false)
            return 
        echo "p2 Doing action"
        sleep(1000)

    echo "Done p2..."
    respChan.send(true)


proc timeoutable(p:proc, timeout=10): bool= 

    var cancelChan: Channel[bool]
    var respChan: Channel[bool]
    var t:  Thread[Args]
    cancelChan.open()
    respChan.open()
    var args = (cancelChan.addr, respChan.addr) 
    createThread[Args](t, p, (args))

    for i in countup(0, timeout):
        let (hasData, msg) = respChan.tryRecv()
        if hasData:
            return msg 
        sleep(1000)

    echo "Cancelling proc.."
    cancelChan.send(true)
    close(cancelChan)
    close(respChan)

    return false

when isMainModule:
    echo "P1: " & $timeoutable(p1)
    echo "P2: " & $timeoutable(p2)

I’m not a fan of this passing pointers, casting, .addr

Macros

Macros allow you to apply transformations on AST on compile time which is really amazing, but It can be very challenging to follow or even work with specially if it’s not well documented and I feel they’re kinda abused in the language resulting in half-baked libraries and macros playground.

Conclusion

Overall, Nim is a language with a great potential, and its small team is doing an excellent job. Just be prepared to write lots of missing libraries if you want to use it in production. It’s a great chance to reinvent the wheel with no one blaming you :)