Serverless QBasic
My first exposure to programming was on an HP-85. This must have been around 1992 or 1993; the machine was then already largely obsolete, having been first produced in 1980.
Like many personal computers of its time, the HP-85 came with an interpreter for a dialect of BASIC. Running PRINT "Hello"
would literally print the message on the built-in thermal printer. You could either use a dedicated DISP
statement to print to the screen, or preface your program with the almighty PRINTER IS 1
which would then redirect all output, textual and graphical, to the screen.
Our father taught us some, well, basic BASIC then. Printing things, printing things many times, tracing lines. My own creations were fairly limited: the way I remember it, I mostly made pretty pictures with very long series of DRAW
statements.1 Everything was in RAM, and turning off the computer meant letting go of all the code — pure programming zen.
Some time over the next few years, our family acquired two IBM PC compatible computers. The first was a Compaq Contura Aero 4/33c. The Contura was a netbook before there really was a net, and, in an early display of corporate courage, could only read floppy disks with an external PCMCIA reader. Somewhat interestingly, it was running Windows 3.1 with an alternative shell called TabWorks. The other computer was an unremarkable desktop running, I think, a 486DX2 at 33MHz.
I naturally spent most of my computer time playing games.
Through circumstances that I don’t fully recall, we eventually learned that our computers hosted a DOS program called QBASIC.EXE
: a development environment for BASIC.
QBasic turned out to be a great entertaining complement to Crystal Caves speed runs and Wacky Wheels battles over null modem. We could draw lines in color, produce sounds, etc., all at the mesmerizing speed of the 486. I still remember the first time we played with DRAW
: because we were used to the HP-85 drawing lines slowly across the screen, pixel by pixel, we could not understand why we were only seeing flashes before the control returned to the development environment. Somehow I vaguely recall that we eventually figured out we could slow down the programs by issuing inaudible SOUND
statements.
My largest QBasic project was a Snake clone. I don’t remember the exact time frame, but it was around the time the Nokia 3210 was taking over the world, so around 2000. This was also around the time I started moving to programming websites in PHP, and SNAKE.BAS
must have been my last QBasic program.
In the rest of this post, I get back to programming in QBasic; in particular, I’ll show how to build a serverless function that runs QBasic programs in the cloud, using the original environment from Microsoft,2 Docker and OpenWhisk.
Back to basics
The first step is to find a copy of the interpreter. Through a chain of searches and links I cannot retrace, I stumbled across an Internet Archive copy of a Windows 95 download for a file called OLDDOS.EXE
. That .EXE
is an archive of several DOS-era utilities, apparently bundled and redistributed for post-3.11 versions of Windows (yes, they were calling it “old” in 1995). Crucially for us it includes QBASIC.EXE
.
After a quick installation of DOSBox, we can run OLDDOS.EXE
in an MS-DOS shell; the executable is an old-fashioned self-extracting archive, which helpfully warns us to make sure we are decompressing into an empty folder or on a blank formatted diskette
.
Once the decompression is successful, we are ready to launch the QBasic IDE. The environment in itself is quite interesting; it features integrated documentation for every function and procedure, some basic code navigation options, and a debugger. I can imagine that those features were fairly standard in other, more “serious”, tools from the era (e.g. Turbo Pascal), and they certainly were invaluable in a pre-Internet world.
A sample program
As a nod to our interests around the time we learned QBasic as kids, I picked a Morse encoder for my return to the language. The program simply prompts for an input string, loops over its characters, and builds a Morse code equivalent with a lookup table:
DECLARE FUNCTION MORSEC$ (CHAR$)
DIM MESSAGE AS STRING
CLS
INPUT "Text to encode: "; MESSAGE$
LET RESULT$ = ""
FOR I = 1 TO LEN(MESSAGE$)
LET C$ = UCASE$(MID$(MESSAGE$, I, 1))
IF I <> 1 THEN RESULT$ = RESULT$ + " "
RESULT$ = RESULT$ + MORSEC$(C$)
NEXT I
PRINT RESULT$
END
FUNCTION MORSEC$ (CHAR$)
SELECT CASE CHAR$
CASE "A"
MORSEC$ = ".-"
' another 50 lines
' ...
CASE " "
MORSEC$ = "/"
CASE ELSE
MORSEC$ = ""
END SELECT
END FUNCTION
(As an aside, I had to look up almost every keyword to produce this program. This resulted in probably the highest quality code sample I ever wrote in this language. I’m almost certain for instance that I had never heard of FUNCTION
before, being content to use only the venerable GOSUB
.)
We can run our program from the IDE by pressing F5, and we get the expected result:
Text to encode: ace of base
.- -.-. . / --- ..-. / -... .- ... .
Containerizing QBasic
We got to the point where we can run a program. The next step is to make it available in the cloud. Probably the most portable approach is to package the runtime in a Docker container, ensuring it can run on commodity Linux servers.
Headless QBasic
The first step is to find a way to run the program without the need to interact with the IDE. While Microsoft did have a compiler for the language, I couldn’t easily find a version of it online.
What I did find online however is a post from 2002 explaining how to run QBasic programs from batch files, which is essentially what we are after. The steps are:
- use
QBASIC.EXE /run FILE.BAS
, which instructs the IDE to immediately run the program, and - use
SYSTEM
instead ofEND
, to instruct the interpreter to exit the IDE instead of returning control to it at the end of execution.
On my setup, the execution briefly flashes the IDE but otherwise the technique works as advertised.
We also need to alter the program slightly so that it doesn’t expect any input from the user. We will instead read input from a file with a predetermined name. We achieve this by replacing the line
INPUT "Text to encode: "; MESSAGE$
with the following two lines:
OPEN "INPUT.STR" FOR INPUT AS 1
LINE INPUT #1, MESSAGE$
Headless DOSBox
We have removed the need to interact with the IDE, and now need a way to automate the interaction with DOSBox. Once more, Internet comes to our help. The steps are:
- use an
[autoexec]
section indosbox.conf
to setup the mount(s) whereQBASIC.EXE
, the program, and the input file will be, - have QBasic output to a file instead of the standard output, and
- instruct DOSBox to exit after running a predefined (dummy) command.
The complete command that ended up working for me is as follows:
dosbox "./PRINT.EXE" -c "C:\\QBASIC.EXE /run C:\\MORSE.BAS > C:\\LOG.TXT" -exit
When running it, a DOSBox window will show up, then flash through the QBasic IDE, and finally close itself, having written the result to LOG.TXT
.
The final step is to disable the graphical interface entirely, which we can do by setting the environment variable SDL_VIDEODRIVER
to dummy
.
We can now run a single command from a Linux environment (or Mac in my case) to encode the contents of a text file into Morse, and write the result to another file.
QBasic DevOps
We are almost there. To expose the Morse encoding functionality as a web API, I wrote a simple Python proxy that conforms to the OpenWhisk action interface:
- it listens to HTTP requests on port
8080
- it responds to requests to
/init
- it responds to
POST
requests to/run
, expecting JSON input and producing JSON output
Since we run DOSBox on every request and keep nothing in memory in between invocations, the /init
endpoint does nothing. (Actions that need to, e.g., compile code common to all invocations would do it during this phase.)
Requests to /run
expect the payload to have an input
field and store its contents to INPUT.STR
, which the QBasic program knows to expect. At the end of the execution of DOSBox, control returns to the Python proxy which reads out the output of LOG.TXT
and sends it back as a JSON response.
This is all that is required to expose our QBasic program as an OpenWhisk function.3 The final step is to replicate all the steps outlined above as a Dockerfile
to build a Docker image of the action.
Try it out
The complete source for the OpenWhisk action is available on GitHub. I have built and deployed the action using IBM Cloud Functions, a hosted deployment of OpenWhisk. You can try out the QBasic Morse encoder yourself using the box below.
There we have our final product; a webpage with a JavaScript function that calls a web API, which triggers the execution of a Python script in a Docker container, which starts a DOS emulator, itself running QBasic to encode our message into Morse. Pretty straightforward.
-
Only much later did I learn about the existence of arrays and the
DATA
statement. I retroactively attribute my programming style to an early insight into the benefits of loop unrolling. ↩ -
I should note that there are simpler alternatives: there are modern tools to write and run QBasic on current operating systems. The two most mature suitable projects are QB64 and FreeBasic. But then again, who wants madeleines to taste almost like they used to. ↩
-
…with the understanding that we built a proof-of-concept. There are almost certainly code paths that can be exploited to crash or subvert the action container. ↩