Ir al contenido principal

Ralsina.Me — El sitio web de Roberto Alsina

Publicaciones sobre 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 abd saw a few nice things:

To build a svg tag, you can use <>svg(at­tr=val­ue) 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. usingit as when is­Main­Mod­ule means that code is built and executed when svg­write.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 Draw­ing as first argument, you can just call d.Add() if d is a Draw­ing. Is 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 im­port 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. nim­ble in­­stall do­­copt

  2. im­­port do­­copt 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 striaght­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­har­f­buz­z.­­so".}

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

  • To de­­clare a func­­tion just do some gy­­manstic­s:

    proc cre­ate*(): Buf­fer {.­­head­­er: "har­f­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­a­bil­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­pec­t­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