Over the New Year's break, I got a bit interested in the Nim programming language. In the words of its creators, "Nim is a statically typed compiled systems programming language. It combines successful concepts from mature languages like Python, Ada and Modula."
What made it interesting to me was that it can use a C compiler as backend and -- with the proper configuration -- C code generated by the Nim compiler only depends on the standard C library. Also, it allows easy interfacing with C functions, e.g., to call operating system functions. In fact, the section Nim for embedded systems describes almost everything that was needed to compile Nim programs for the Atari using Vincent's gcc and MiNTLib.
Thus, I created a nim.cfg configuration file in my project directory containing...
Code: Select all
m68k.any.gcc.exe = "m68k-atari-mint-gcc"
m68k.any.gcc.linkerexe = "m68k-atari-mint-gcc"
gc = arc
passL="-s"
define = "useMalloc"
nim c --cpu:m68k --os:Any -o:myprog.tos myprog.nim
... for the debug build or:
nim c --cpu:m68k --os:Any -d:release -o:myprog.tos myprog.nim
... for the (faster, smaller) release build.
(Note: the "passL" line in the configuration file is not strictly required, but instructs gcc's linker to strip symbols for smaller executables.)
Building with libcmini (instead of MiNTLib) is a bit more involved, in particular because libcmini requires its startup code (crt0.o) to be linked first, which normally the Nim compiler would not support.
However, this can be solved by an updated nim.cfg:
Code: Select all
m68k.any.gcc.exe = "m68k-atari-mint-gcc"
m68k.any.gcc.linkerexe = "m68k-atari-mint-gcc"
m68k.any.gcc.linkTmpl = "-o $exefile /home/czietz/libcmini-experimental/build/crt0.o $objfiles $options"
gc = arc
passL="-nostdlib"
passL="-L/home/czietz/libcmini-experimental/build"
passL="-lcmini"
passL="-lgcc"
passL="-s"
define = "useMalloc"
define = "noSignalHandler"
* The path to libcmini must obviously be adapted to your system.
* "noSignalHandler" needs to be added because libcmini does not implement the signal() function.
* If you want to run the resulting executable under TOS (as opposed to MiNT), you should compiler libcmini with -DSTDIO_MAP_NEWLINE, so that it maps LF (as used by Nim's runtime as line ending) to CR/LF (as expected by TOS).
This allows you to build working, reasonably small executables with Nim.
During my first experiments, I found two small issues:
- A few of Nim's standard libraries are "impure" (their words), i.e., they depend on external libraries. For example, "re" (regular expressions) depends on the PCRE library. The Nim compiler wants to link to these dynamically which does not work on TOS (because of no shared library support). Maybe the compiler could be told to statically link to these, but I haven't explored that yet.
- Unfortunately, compiling with --os:Any disables printing of an explanatory error message in case of an uncaught exception. Instead the program just terminates, which makes debugging harder. The clean solution would of course be to add support for something like --os:AtariTOS (*) to the compiler/library, but for the time being, I fixed it by simply editing the standard library, specifically the system/excpt.nim file. (The library is provided as source code and modified files are automatically(!) recompiled when building your application. Nice!)
(I just commented out hostOS != "any" bit.)
Code: Select all
proc reportUnhandledError(e: ref Exception) {.nodestroy.} = if unhandledExceptionHook != nil: unhandledExceptionHook(e) when true: #hostOS != "any": reportUnhandledErrorAux(e) else: discard ()
Maybe I got someone interested in Nim, too.
Code: Select all
import tables
import sequtils
import strformat
import strutils
import volatile
type RawCookie = object
id: clong
val: clong
# UncheckedArray is an array without bounds checking, equivalent to a C '[]' array
type RawCookies = UncheckedArray[RawCookie]
type Cookie = object
id: string
val: int
# Easy syntax to call C functions
proc Super(val: clong): clong {.header: "<osbind.h>".}
proc SuperToUser(val: clong) {.header: "<osbind.h>".}
# Converts an array of chars to a string
# openArray is an array with 'flexible' bounds, defined by the caller
proc chartoString(chars: openArray[char]): string =
result = foldl(@chars, a & b, "")
# Returns the string representation of a cookie ID
proc cookieName(id: clong): string =
result = chartoString(cast[array[4, char]](id)) # only works with a big-endian C long
# Retrieves the pointer to the cookie jar.
# Note that this pointer is stored at an address only accessible in supervisor mode.
proc getCookieJar: ptr RawCookies =
let old_ssp = Super(0)
# Use a volatileLoad (instead of a '[]' derefence) to make sure that value
# is really being read between the Super/SuperToUser calls.
result = volatileLoad(cast[ptr ptr RawCookies](0x5A0))
SuperToUser(old_ssp)
# An iterator over all cookies in the jar
iterator cookieIterator(): Cookie =
let COOKIE_JAR = getCookieJar()
if COOKIE_JAR != nil:
for k in 0 .. high(int):
if COOKIE_JAR[k].id == 0:
# found end of jar
break
yield Cookie(id: cookieName(COOKIE_JAR[k].id), val: COOKIE_JAR[k].val)
# Returns all cookies in a table indexed by cookie name (similar to a Python dictionary)
proc getCookiesTable(): auto =
result = initTable[string, int]()
for c in cookieIterator():
result[c.id] = c.val
# Show 'strformat' formatting
for c in cookieIterator():
echo fmt"Cookie '{c.id}' = {c.val:08X}"
# Show 'tables' module
let allCookies = getCookiesTable()
echo "_CPU cookie is: " & toHex(allCookies["_CPU"])
# Show exception handling
try:
echo "FROB cookie is: " & toHex(allCookies["FROB"])
except:
echo "FROB cookie not found!"