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

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{.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 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{.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 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{.nimrod} 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".}{.n­im­rod}

  • To de­clare types which are equiv­a­lent to void *{.n­im­rod} just use dis­tinct point­er{.n­im­rod}

  • 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" .}{.n­im­rod}

  • Cre­ates a nim func­tion called cre­ate{.n­im­rod} (the * means it's "ex­port­ed")

  • It is a wrap­per around hb_buffer­_cre­ate{.n­im­rod} (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{.n­im­rod} 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-2024 Roberto Alsina