Full-stack Fortran Part 1
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.
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:
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:
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.
#!/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.
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:
#!/bin/sh
DNAME=$(realpath $(dirname $0))
PORT=8000
TDIR="$DNAME/../tools"
#!/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
#!/bin/sh
. "$(realpath $(dirname $0))"/common
cd $DNAME/bin ; python -m http.server $PORT
So now test.sh
becomes:
#!/bin/sh
. ./common
$DNAME/build.sh
$DNAME/serve.sh
That's it for now, stay tuned for Part 2!