Full-stack Fortran Part 1

2020-12-13fortranwasmemscripten

I was recently asked to do something weird: compile very old Fortran code to WebAssembly, so it can easily interface with a frontend Elm app.

O'Rly

Fortran is not exactly a current language, and this particular piece of code counts as legacy by even Fortran standards, you have to specify -std=legacy for gfortran to correctly compile it, as it is written in Fortran 77 dialect. It’s not the oldest of fortrans, but it still comes with weird format and line length constraints. Weird, until you consider how they used to do this back in the day:

Punchcard

As to why would you use Fortran in 2020 AD? Because it’s Lindy. It’s old. It’s proven to work. It’s battle-tested. Also, it’s very ugly by today’s standards, and you wouldn’t want to rewrite it anyway, even if that wasn’t very error-prone to do. :)

My “client” was kind enough to do some research into this topic already, and basically provided me with the blueprint: turns out someone already built a toolchain for this exact task in April. (That’s also where the mock O’Reilly book is from)

I won’t go into the toolchain here, I tried to build my own, beacuse there have been advancements in the meantime - LLVM 11 was released, with flang included - but haven’t been successful. Turns out flang’s still not a first class citizen actually producing LLVM IR which could be used with a wasm target out of the box.

Standing on the shoulders of giants, I cloned the repo and started adapting. It’s using some really old versions of tools to make this process possible.

Dragonegg is a plugin for the GNU compilers. It uses GCC as the frontend
and compiles the GCCs IR (GIMPLE) via LLVM. Thus it is able to emit
LLVM IR or even compile to wasm.

Dragonegg is pretty old. the last supported versions of GCC and LLVM
are ggc-4.6 (maybe 4.8) and llvm-3.3. This is bad because LLVM 3.3 does
not support wasm and the IR format of LLVM 3.3 is not compatible with
the newer versions of LLVM that do support wasm.

[...]

After some time, I found out that while the textual representation of
the LLVM IR was incompatible between LLVM 3.3 and >3.7, the binary
bitcode was still compatible.

Okay, so that’s interesting, here’s the test example:

test.f90
module test

        implicit none

        contains

        function add(a, b) result(res)
            integer, value, intent(in)      :: a, b
            integer                         :: res

            write(*,*) "Hello from Fortran!"

            res = a * b
        end function

end module

How do we build this? There’s a test.sh included, let’s try that.

test.sh
#!/bin/bash

cd "${0%/*}/../tools"
DNAME=`readlink -f ../test`
docker-compose run --rm -v $DNAME:/project f90wasm bash -c 'cd /project && VERBOSE=1 make build'
docker kill testserver
docker rm testserver
docker run --name testserver -p 8080:3000 -v $DNAME/bin:/app/public:ro -d tobilg/mini-webserver

Nice, the author kindly provided a convenient docker container for building. The only problem is: this wouldn’t build for me. I fixed two or three problems, before getting impatient, so I opted to pull the image from the docker registry instead.

# docker pull stargate01/f90wasm

Now, docker-compose works (which is a separate package from docker for some reason), and we can try it out.

First test

It works!

Now, spinning up a container to serve some static HTML and js looks a bit overindulgent, can we do this with a simple python HTTP server? We absolutely can, in fact, that’s what you are seeing on the screenshot, because I’m cheating here :)

Let’s clean this thing up then, here’s what I did:

common
#!/bin/bash

DNAME=$(realpath $(dirname $0))
PORT=8000

TDIR="$DNAME/../tools"
build.sh
#!/bin/bash

source ./common

pushd $TDIR
docker-compose run --rm -v $DNAME:/project f90wasm bash -c 'cd /project && VERBOSE=1 make build'
popd

#SRCPATH=$DNAME/src
#gfortran -std=legacy -c ${SRCPATH}/data.for ${SRCPATH}/matutils.for ${SRCPATH}/calcprint.for ${SRCPATH}/jacobi.for ${SRCPATH}/main.for
serve.sh
#!/bin/bash

. "$(realpath $(dirname $0))"/common

cd $DNAME/bin ; python -m http.server $PORT

So now test.sh becomes:

test.sh
#!/bin/bash

. ./common

$DNAME/build.sh

$DNAME/serve.sh

I’ll have to finish this later, stay tuned for part 2.