Playing with Nim
A few days ago I saw a mention in twitter about a language called Nim
Se ve interesante ~90% Python-like, compila a Binario o JS, no dependencias ni runtime.https://t.co/JF3vIScWqG
— Juan_Carlos (@juancarlospaco) 27 de abril de 2018
And... why not. I am a bit stale in my programming language variety. I used to be fluent in a dozen, now I do 80% python 10% go, some JS and almost nothing else. Because I learn by doing, I decided to do something. Because I did not want a problem I did not know how to solve to get in the way of the language, I decided to reimplement the example for the python book I am writing: a text layout engine that outputs SVG, based on harfbuzz, freetype2 and other things.
This is a good learning project for me, because a lot of my coding is glueing things together, I hardly ever do things from scratch.
So, I decided to start in somewhat random order.
Preparation
I read the Nim Tutorial quickly. I ended referring to it and to Nim by example a lot. While trying out a new language one is bound to forget syntax. It happens.
Wrote a few "hello world" 5 line programs to see that the ecosystem was installed correctly. Impression: builds are fast-ish. They can get actually fast if you start using tcc instead of gcc.
SVG Output
I looked for libraries that were the equivalent of svgwrite, which I am using on the python side. Sadly, such a thing doesn't seem to exist for nim. So, I wrote my own. It's very rudimentary, and surely the nim code is garbage for experienced nim coders, but I ended using the xmltree module of nim's standard library and everything!
import xmltree
import strtabs
import strformat
type
Drawing* = tuple[fname: string, document: XmlNode]
proc NewDrawing*(fname: string, height:string="100", width:string="100"): Drawing =
result = (
fname: fname,
document: <>svg()
)
var attrs = newStringTable()
attrs["baseProfile"] = "full"
attrs["version"] = "1.1"
attrs["xmlns"] = "http://www.w3.org/2000/svg"
attrs["xmlns:ev"] = "http://www.w3.org/2001/xml-events"
attrs["xmlns:xlink"] = "http://www.w3.org/1999/xlink"
attrs["height"] = height
attrs["width"] = width
result.document.attrs = attrs
proc Add*(d: Drawing, node: XmlNode): void =
d.document.add(node)
proc Rect*(x: string, y: string, width: string, height: string, fill: string="blue"): XmlNode =
result = <>rect(
x=x,
y=y,
width=width,
height=height,
fill=fill
)
proc Text*(text: string, x: string, y: string, font_size: string, font_family: string="Arial"): XmlNode =
result = <>text(newText(text))
var attrs = newStringTable()
attrs["x"] = x
attrs["y"] = y
attrs["font-size"] = font_size
attrs["font-family"] = font_family
result.attrs = attrs
proc Save*(d:Drawing): void =
writeFile(d.fname,xmlHeader & $(d.document))
when isMainModule:
var d = NewDrawing("foo.svg", width=fmt"{width}cm", height=fmt"{height}cm")
d.Add(Rect("10cm","10cm","15cm","15cm","white"))
d.Add(Text("HOLA","12cm","12cm","2cm"))
d.Save()
While writing this I ran into a few issues and saw a few nice things:
To build a svg
{.nimrod} tag, you can use <>svg(attr=value)
{.nimrod}
which is delightful syntax. But what happens if the attr is
"xmlns:ev"
{.nimrod}? That is not a valid identifier, so it doesn't
work. So I worked around it by creating a StringTable
{.nimrod} filling
it and setting all attributes at once.
A good thing is the when
{.nimrod} keyword. using it as
when isMainModule
{.nimrod} means that code is built and executed when
svgwrite.nim
{.nimrod} is built standalone, and not when used as a
module.
Another good thing is the syntax sugar for what in python we would call "object's methods".
Because Add
{.nimrod} takes a Drawing
{.nimrod} as first argument, you
can just call d.Add()
{.nimrod} if d
{.nimrod} is a
Drawing
{.nimrod}. It's simple, it's clear and it's useful and I like
it.
One bad thing is that sometimes importing a module will cause weird errors that are hard to guess. For example, this simplified version fails to build:
import xmltree
type
Drawing* = tuple[fname: string, document: XmlNode]
proc NewDrawing*(fname: string, height:string="100", width:string="100"): Drawing =
result = (
fname: fname,
document: <>svg(width=width, height=height)
)
when isMainModule:
var d = NewDrawing("foo.svg")
$ nim c svg1.nim
Hint: used config file '/etc/nim.cfg' [Conf]
Hint: system [Processing]
Hint: svg1 [Processing]
Hint: xmltree [Processing]
Hint: macros [Processing]
Hint: strtabs [Processing]
Hint: hashes [Processing]
Hint: strutils [Processing]
Hint: parseutils [Processing]
Hint: math [Processing]
Hint: algorithm [Processing]
Hint: os [Processing]
Hint: times [Processing]
Hint: posix [Processing]
Hint: ospaths [Processing]
svg1.nim(9, 19) template/generic instantiation from here
lib/nim/core/macros.nim(556, 26) Error: undeclared identifier: 'newStringTable'
WAT? I am not using newStringTable
anywhere! The solution
is to add import strtabs
{.nimrod} which defines it, but there is
really no way to guess which imports will trigger this sort of issue. If
it's possible that importing a random module will trigger some weird
failure with something that is not part of the stdlib and I need to
figure it out... it can hurt.
In any case: it worked! My first working, useful nim code!
Doing a script with options / parameters
In my python version I was using docopt and this was smooth: there is a nim version of docopt and using it was as easy as:
-
nimble install docopt
-
import docopt
{.nimrod} in the script
The usage is remarkably similar to python:
import docopt
when isMainModule:
let doc = """Usage:
boxes <input> <output> [--page-size=<WxH>] [--separation=<sep>]
boxes --version"""
let arguments = docopt(doc, version="Boxes 0.13")
var (w,h) = (30f, 50f)
if arguments["--page-size"]:
let sizes = ($arguments["--page-size"]).split("x")
w = parse_float(sizes[0])
h = parse_float(sizes[1])
var separation = 0.05
if arguments["--separation"]:
separation = parse_float($arguments["--separation"])
var input = $arguments["<input>"]
var output = $arguments["<output>"]
Not much to say, other that the code for parsing --page-size
is
slightly less graceful than I would like because I can't figure out how
to split the string and convert to float at once.
So, at that point I sort of have the skeleton of the program done. The missing pieces are calling harfbuzz and freetype2 to figure out text sizes and so on.
Interfacing with C libs
One of the main selling points of Nim is that it interfaces with C and C++ in a straightforward manner. So, since nobody had wrapped harfbuzz until now, I could try to do it myself!
First I tried to get c2nim working, since it's the recommended way to do it. Sadly, the version of nim that ships in Arch is not able to build c2nim via nimble, and I ended having to manually build nim-git and c2nim-git ... which took quite a while to get right.
And then c2nim just failed.
So then I tried to do it manually. It started well!
To link libraries you just use pragmas:
{.link: "/usr/lib/libharfbuzz.so".}
{.nimrod}To declare types which are equivalent to
void *
{.nimrod} just usedistinct pointer
{.nimrod}-
To declare a function just do some gymnastics:
proc create*(): Buffer {.header: "harfbuzz/hb.h", importc: "hb_buffer_$1" .}
{.nimrod} Creates a nim function called
create
{.nimrod} (the * means it's "exported")It is a wrapper around
hb_buffer_create
{.nimrod} (see the syntax there? That is nice!)Says it's declared in C in "harfbuzz/hb.h"
It returns a
Buffer
{.nimrod} which is declared thus:
type
Buffer* = distinct pointer
Here is all I could do trying to wrap what I needed:
{.link: "/usr/lib/libharfbuzz.so".}
{.pragma: ftimport, cdecl, importc, dynlib: "/usr/lib/libfreetype.so.6".}
type
Buffer* = distinct pointer
Face* = distinct pointer
Font* = distinct pointer
FT_Library* = distinct pointer
FT_Face* = distinct pointer
FT_Error* = cint
proc create*(): Buffer {.header: "harfbuzz/hb.h", importc: "hb_buffer_$1" .}
proc add_utf8*(buffer: Buffer, text: cstring, textLength:int, item_offset:int, itemLength:int) {.importc: "hb_buffer_$1", nodecl.}
proc guess_segment_properties*( buffer: Buffer): void {.header: "harfbuzz/hb.h", importc: "hb_buffer_$1" .}
proc create_referenced(face: FT_Face): Font {.header: "harfbuzz/hb.h", importc: "hb_ft_font_$1" .}
proc shape(font: Font, buf: Buffer, features: pointer, num_features: int): void {.header: "harfbuzz/hb.h", importc: "hb_$1" .}
proc FT_Init_FreeType*(library: var FT_Library): FT_Error {.ft_import.}
proc FT_Done_FreeType*(library: FT_Library): FT_Error {.ft_import.}
proc FT_New_Face*(library: FT_Library, path: cstring, face_index: clong, face: var FT_Face): FT_Error {.ft_import.}
proc FT_Set_Char_Size(face: FT_Face, width: float, height: float, h_res: int, v_res: int): FT_Error {.ft_import.}
var buf: Buffer = create()
buf.add_utf8("Hello", -1, 0, -1)
buf.guess_segment_properties()
var library: FT_Library
assert(0 == FT_Init_FreeType(library))
var face: FT_Face
assert(0 == FT_New_Face(library,"/usr/share/fonts/ttf-linux-libertine/LinLibertine_R.otf", 0, face))
assert(0 == face.FT_Set_Char_Size(1, 1, 64, 64))
var font = face.create_referenced()
font.shape(buf, nil, 0)
Sadly, this segfaults and I have no idea how to debug it. It's probably close to right? Maybe some nim coder can figure it out and help me?
In any case, conclusion time!
Conclusions
- I like the language
- I like the syntax
- nimble, the package manager is cool
- Is there an equivalent of virtualenvs? Is it necessary?
- The C wrapping is, indeed, easy. When it works.
- The availability of 3rd party code is of course not as large as with other languages
- The compiling / building is cool
- There are some strange bugs, which is to be expected
- Tooling is ok. VSCode has a working extension for it. I miss an opinionated formatter.
- It produces fast code.
- It builds fast.
I will keep it in mind if I need to write fast code with limited dependencies on external libraries.