psuter.net

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.

HP-85
HP-85: personal computing in 1980 (source).

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.

QBasic IDE welcome screen
The QBasic IDE welcome screen, alive and kicking in 2018, running in DOSBox.

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:

  1. use QBASIC.EXE /run FILE.BAS, which instructs the IDE to immediately run the program, and
  2. use SYSTEM instead of END, 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:

  1. use an [autoexec] section in dosbox.conf to setup the mount(s) where QBASIC.EXE, the program, and the input file will be,
  2. have QBasic output to a file instead of the standard output, and
  3. 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:

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.

  1. 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.

  2. 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.

  3. …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.