Cross Compilation Adventures with Nim lang

Cross Compilation Adventures with Nim lang

December 8, 2023Β·
Nishant Srivastava

Banner

ℹ️

This post is part of a series.

TLDR; I want to build cross-platform CLI utility tools that can be compiled on my laptop and run seamlessly on other platforms.

I like working inside the Terminal app. So obviously I am using a lot of Terminal CLI (Command Line Interface) tools 🧰. To work with these CLI tools, for every usecase there is a small set of shell (bash/zsh) aliases/functions that I have created to work with for specific use cases and to simplify my life. These do differ based on the operating system I am working with (I mostly work with *nix systems i.e macOS and Linux). Remembering these aliases is, let’s just say, not a good developer user experience or DevUX.

For that matter, I have wanted to build my own CLI tools that I could use instead of my shell dependent aliases. Many CLI tools themselves are cross-platform, so my idea is to build cross platform CLI tools for myself.

In order to build such CLI tools, I have been in search of suitable languages to help me build them. For my use case, an ideal language of choice must-have:

  • Ability to generate binaries that are
    • cross-platform: Works on Linux, macOS, Windows (and more if possible).
    • small footprint in size
    • performant (No one wants slow tools πŸ‘€. Terminal is all about speed πŸš€)
  • Can cross-compile
    • Binaries for multiple target platform can be built from one platform itself (i.e from macOS compile binaries that run on macOS as well as Linux and Windows platform).
  • Easy to maintain
    • The language is easy and simple enough to pick up. Thus enabling maintaining my project in the long run.

The first langauage that I stumbled upon is Nim Lang.

From the official website:

<ul> <li>Nim is a statically typed compiled systems programming language. It combines successful concepts from mature languages like Python, Ada and Modula.</li> <li>Nim generates native dependency-free executables, not dependent on a virtual machine, which are small and allow easy redistribution.</li> <li>The Nim compiler and the generated executables support all major platforms like Windows, Linux, BSD and macOS.</li> </ul>

Sounds good! Let’s dive into building a very basic CLI tool.

A good example to showcase would be to build a CLI tool that can convert from Β°C to F and vice versa. Our tool will take an input for value and the unit to be converted to, then output would be converted temprature value.

<p><strong>NOTE</strong>: I am using macOS (M2 Pro, Apple Silicon), so the instructions follow through using that only. However the steps should work on all platform with little tweaks.</p>

First we need to install nim-lang. Open your Terminal app and execute the command

curl https://nim-lang.org/choosenim/init.sh -sSf | sh
<p><a href="https://nim-lang.org/install.html" target="_blank" rel="noopener">Read here</a> to learn how to install on other platforms</p>

Once installed, you should have access to nim compiler in your Terminal. If not restart your session or open a new Terminal window so it is loaded in the PATH. Follow through next steps

  • Create a file named run.nim.

    touch run.nim
  • Add the below code to the run.nim file and save the file.

    import strutils, std/os
    
    proc celsiusToFahrenheit(c: float): float =
      result = c * 9 / 5 + 32
    
    proc fahrenheitToCelsius(f: float): float =
      result = (f - 32) * 5 / 9
    
    when isMainModule:
      var value: string
      var unit: string
      if paramCount() == 2:
        value = $(paramStr(1).parseFloat())
        unit = paramStr(2).toUpper()
      else:
        echo "Usage: ./run <value> <unit_to_convert_to>"
        quit(1)
    
      var convertedTemperature: float
      if unit == "C":
        convertedTemperature = celsiusToFahrenheit(value.parseFloat())
      elif unit == "F":
        convertedTemperature = fahrenheitToCelsius(value.parseFloat())
      else:
        echo "Invalid unit. Please use C or F."
        quit(1)
    
      echo "Converted temperature: ", convertedTemperature, "Β°", unit
    <p>I am not going to explain this code as it is simple and self explanatory.</p> <p>To understand and learn the language I used <a href="https://learnxinyminutes.com/docs/nim/" target="_blank" rel="noopener">Learn X in Y minutes: Nim Lang</a> πŸš€</p>
  • Now to compile, execute the nim compiler with compileargument and the run.nim file:

    nim compile run.nim
    <p><strong>NOTE</strong>: You can also replace <code>compile</code> with its shorthand version <code>c</code> in above command. i.e <code>nim c run.nim</code>.</p>

    You should now have a binary generated in the same directory with the same name as the nim file i.e run.

    run

    <p><strong>NOTE</strong>: I use <a href="https://github.com/bootandy/dust" target="_blank" rel="noopener"><code>dust</code></a> CLI tool to list files in directory with their sizes. <strong>TIP</strong>: You can generate an optimized binary by passing <code>-d:release --opt:size</code> flags at the time of compilation. i.e <code>nim -d:release --opt:size c run.nim</code>. Result is just a smaller binary.</p>

    run optimized

  • Time to execute our generated run binary file:

    ❯ ./run 
    Usage: ./run <value> <unit_to_convert_to>

    Didn’t work πŸ™„, but we have a helpful message stating how to use the CLI tool 😊

    ❯ ./run 49 C                         
    Converted temperature: 120.2Β°C

Done! That was a super quick intro to working with Nim Language in less than 5 mins πŸ˜…

<p><strong>TIP</strong>: You can also compile and run the <code>.nim</code> file in one command by using the <code>-r</code> flag along with <code>c</code> flag. i.e <code>nim c -r run.nim</code></p>

But we aren’t done yet. This generated binary would work on *nix systems. I mentioned earlier that I would like to have cross-(platform + compilation).

nim lang allows to do that easily about which you can read here. Since we already have *nix compatible binary i.e Linux and macOS are sorted for us. We need to cross compile to a format that Windows understands i.e exe/executable. Let’s do that next.

You can read about Windows cross compilation using nim lang here.

  • First install the mingw-w64 toolchain using homebrew for macOS:

    brew install mingw-w64
  • Install MacPorts

  • Start a new Terminal session and install x86_64-w64-mingw32-gcc

    sudo port install x86_64-w64-mingw32-gcc
    <p><strong>NOTE</strong>: Due to a <a href="https://github.com/nim-lang/Nim/issues/10717#issuecomment-480867457" target="_blank" rel="noopener">path issue</a>, you will have to tell nim where this <code>x86_64-w64-mingw32-gcc</code> is, by editing the <code>nim.cfg</code> file.</p>

    To know the path where x86_64-w64-mingw32-gcc was installed by Macports, run port content x86_64-w64-mingw32-gcc | grep bin/x86_64-w64-mingw32-gcc

    ❯ port content x86_64-w64-mingw32-gcc | grep bin/x86_64-w64-mingw32-gcc
    /opt/local/bin/x86_64-w64-mingw32-gcc

    from above x86_64-w64-mingw32-gcc is installed at path /opt/local/bin. You need to let nim know about this path.

    Open nim.cfg at path ~/.choosenim/toolchains/nim-2.0.0/config/nim.cfg using a code editor i.e VSCode.

    code ~/.choosenim/toolchains/nim-2.0.0/config/nim.cfg

    Find and edit the path value for key amd64.windows.gcc.path under macosx, from /user/bin

    @if macosx:
      amd64.windows.gcc.path = "/user/bin"
      ..

    to /opt/local/bin

    @if macosx:
      amd64.windows.gcc.path = "/opt/local/bin"
      ...

    Save the file and open a new Terminal session.

  • Compile the run.nim file with -d:mingw flag:

    nim c -d:mingw run.nim

    You should now have a .exe binary generated in the same directory with the same name as the nim file i.e run.exe

    run.exe

    <p><strong>TIP</strong>: You can generate an optimized binary by passing <code>-d:release --opt:size</code> flags at the time of compilation. i.e <code>nim -d:release --opt:size c -d:mingw run.nim</code>. Result is just a smaller binary.</p>

    run.exe optimized

    <p><strong>NOTE</strong>: In order to run this .exe file, you need to either execute this on Windows directly or if on a *nix system then make use of <a href="https://www.winehq.org/" target="_blank" rel="noopener">Wine</a>.</p>
<p><strong>TIP</strong>: Further reading about reduceing the binary size, read <a href="https://hookrace.net/blog/nim-binary-size/" target="_blank" rel="noopener">here</a></p>

Thats it. I think Nim Language pretty much does what I wanted to get out of it:

βœ… Generate cross-platform binaries βœ… Can cross-compile to platforms βœ… Easy syntax, so maintainable code

Generate cross-platform binariesCan cross-compile to platformsEasy syntax, so maintainable code
βœ…βœ…βœ…

All check boxes ticked is good 😊

<p><strong>BONUS</strong>: While my requirement isn&rsquo;t about compiling to other platforms, but Nim is quite capable such as compiling for <a href="https://nim-lang.github.io/Nim/nimc.html#crossminuscompilation-for-android" target="_blank" rel="noopener">Android</a>, <a href="https://nim-lang.github.io/Nim/nimc.html#crossminuscompilation-for-ios" target="_blank" rel="noopener">iOS</a> and <a href="https://nim-lang.github.io/Nim/nimc.html#crossminuscompilation-for-nintendo-switch" target="_blank" rel="noopener">Nintendo-Switch</a></p>

I’ll be trying this approach of evaluating more languages in the future. You can find the code for this post here.

Last updated on