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. That is, 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" (a friend) 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/sh
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.

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. ;)

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

common
#!/bin/sh

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

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

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/sh

. "$(realpath $(dirname $0))"/common
cd $DNAME/bin ; python -m http.server $PORT

So now test.sh becomes:

test.sh
#!/bin/sh

. ./common
$DNAME/build.sh
$DNAME/serve.sh

That's it for now, stay tuned for Part 2!