Skip to main content

Ralsina.Me — Roberto Alsina's website

Posts about nim

Playing with Nim

A few days ago I saw a men­tion in twit­ter about a lan­guage called Nim

And... why not. I am a bit stale in my pro­gram­ming lan­guage va­ri­ety. I used to be flu­ent in a dozen, now I do 80% python 10% go, some JS and al­most noth­ing else. Be­cause I learn by do­ing, I de­cid­ed to do some­thing. Be­cause I did not want a prob­lem I did not know how to solve to get in the way of the lan­guage, I de­cid­ed to reim­ple­ment the ex­am­ple for the python book I am writ­ing: a text lay­out en­gine that out­puts SVG, based on harf­buz­z, freetype2 and oth­er things.

This is a good learn­ing project for me, be­cause a lot of my cod­ing is glue­ing things to­geth­er, I hard­ly ev­er do things from scratch.

So, I de­cid­ed to start in some­what ran­dom or­der.

Preparation

I read the Nim Tu­to­ri­al quick­ly. I end­ed re­fer­ring to it and to Nim by ex­am­ple a lot. While try­ing out a new lan­guage one is bound to for­get syn­tax. It hap­pen­s.

Wrote a few "hel­lo world" 5 line pro­grams to see that the ecosys­tem was in­stalled cor­rect­ly. Im­pres­sion: builds are fast-ish. They can get ac­tu­al­ly fast if you start us­ing tcc in­stead of gc­c.

SVG Output

I looked for li­braries that were the equiv­a­lent of svg­write, which I am us­ing on the python side. Sad­ly, such a thing does­n't seem to ex­ist for nim. So, I wrote my own. It's very rudi­men­ta­ry, and sure­ly the nim code is garbage for ex­pe­ri­enced nim coder­s, but I end­ed us­ing the xml­tree mod­ule of nim's stan­dard li­brary and ev­ery­thing!

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 writ­ing this I ran in­to a few is­sues and saw a few nice things:

To build a svg tag, you can use <>svg(attr=value) which is delightful syntax. But what happens if the attr is "xmlns:ev"? That is not a valid identifier, so it doesn't work. So I worked around it by creating a StringTable filling it and setting all attributes at once.

A good thing is the when keyword. using it as when isMainModule means that code is built and executed when svgwrite.nim is built standalone, and not when used as a module.

An­oth­er good thing is the syn­tax sug­ar for what in python we would call "ob­jec­t's meth­od­s".

Because Add takes a Drawing as first argument, you can just call d.Add() if d is a Drawing. It's simple, it's clear and it's useful and I like it.

One bad thing is that some­times im­port­ing a mod­ule will cause weird er­rors that are hard to guess. For ex­am­ple, this sim­pli­fied ver­sion 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 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 work­ing, use­ful nim code!

Doing a script with options / parameters

In my python ver­sion I was us­ing do­copt and this was smooth: there is a nim ver­sion of do­copt and us­ing it was as easy as:

  1. nimble install docopt
  2. import docopt in the script

The us­age is re­mark­ably sim­i­lar 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 skele­ton of the pro­gram done. The miss­ing pieces are call­ing harf­buzz and freetype2 to fig­ure out text sizes and so on.

Interfacing with C libs

One of the main sell­ing points of Nim is that it in­ter­faces with C and C++ in a straight­for­ward man­ner. So, since no­body had wrapped harf­buzz un­til now, I could try to do it my­self!

First I tried to get c2n­im work­ing, since it's the rec­om­mend­ed way to do it. Sad­ly, the ver­sion of nim that ships in Arch is not able to build c2n­im via nim­ble, and I end­ed hav­ing to man­u­al­ly build nim-git and c2n­im-git ... which took quite a while to get right.

And then c2n­im just failed.

So then I tried to do it man­u­al­ly. It start­ed well!

  • To link li­braries you just use prag­mas: {.link: "/us­r/lib/lib­harf­buz­z.­so".}

  • To de­clare types which are equiv­a­lent to void * just use dis­tinct point­er

  • To de­­clare a func­­tion just do some gym­­nas­tic­s:

    proc cre­ate*(): Buf­fer {.­head­er: "harf­buz­z/h­b.h", im­portc: "h­b_buffer­_$1" .}

  • Cre­ates a nim func­tion called cre­ate (the * means it's "ex­port­ed")

  • It is a wrap­per around hb_buffer­_cre­ate (see the syn­tax there? That is nice!)

  • Says it's de­­clared in C in "har­f­buz­z/h­b.h"

  • It re­turns a Buf­fer which is de­clared thus:

type
    Buffer* = distinct pointer

Here is all I could do try­ing to wrap what I need­ed:

{.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)

Sad­ly, this seg­faults and I have no idea how to de­bug it. It's prob­a­bly close to right? Maybe some nim coder can fig­ure it out and help me?

In any case, con­clu­sion time!

Conclusions

  • I like the lan­guage
  • I like the syn­tax
  • nim­ble, the pack­age man­ag­er is cool
  • Is there an equiv­a­lent of vir­tualen­vs? Is it nec­es­sary?
  • The C wrap­ping is, in­deed, easy. When it work­s.
  • The avail­abil­i­ty of 3rd par­ty code is of course not as large as with oth­er lan­guages
  • The com­pil­ing / build­ing is cool
  • There are some strange bugs, which is to be ex­pect­ed
  • Tool­ing is ok. VS­Code has a work­ing ex­ten­sion for it. I miss an opin­ion­at­ed for­mat­ter.
  • It pro­duces fast code.
  • It builds fast.

I will keep it in mind if I need to write fast code with lim­it­ed de­pen­den­cies on ex­ter­nal li­braries.


Contents © 2000-2023 Roberto Alsina