Conway in C89

Another year, another version of my Polyglot Conway project, this time for C89, also known as “ANSI C”, targeting quite a few compilers of the early 1990s, and some others from our more recent times.

The goal of this rabbit hole exercise was to create a set of source files that would compile and run off the box with early ANSI C compilers, from around 1991 or 1992.

I used ChatGPT to translate an existing version (in this case, the QBasic one) into ANSI C, and the result was perfect after just one iteration with the famous AI tool. I then expanded the code to add support for more compilers. And then I went overboard:

The Code

Here is a fragment of the code, with the full thing available on GitLab as usual.

/* ... */
/* Main entry point */
int main(void) {
    int generation = 0;
    world_t world; /* Global variable on the stack passed as reference */

    signal(SIGINT, signal_handler);

    world_init(&world);
    world_blinker(&world, 0, 1);
    world_beacon(&world, 10, 10);
    world_glider(&world, 4, 5);
    world_block(&world, 1, 10);
    world_block(&world, 18, 3);
    world_tub(&world, 6, 1);

    while (!signal_received) {
        clear_screen();
        world_print(&world, DISPLAY_SIZE);
        generation++;
        print_compiler();
        printf("\nGeneration %d\n", generation);
        wait(1); /* Pause for 1 second */
        world_evolve(&world);
    }
    clear_screen();
    return 0;
}
/* ... */

API

The code features a C struct called world_t, working as an abstract (as much as possible) data type, with a nice C API to encapsulates the operations on it:

#ifndef WORLD_H
#define WORLD_H

#define WORLD_SIZE 30

/* Abstract data type holding a world filled with cells */
typedef struct world_t {
    unsigned int cells[WORLD_SIZE][WORLD_SIZE];
} world_t;

/* Resets world to all zeros */
void world_init(world_t *);

/* Evolve the world to the next generation */
void world_evolve(world_t *);

/* Copy the contents of the second world passed in argument unto the first */
void world_copy(world_t *, world_t *);

/* Compares two worlds and returns 1 if they are equal */
unsigned int world_equals(world_t *, world_t *);

/* Output a visual representation of the world */
void world_print(world_t *, int);
/* ... */

Tests

You can also see this API in action in the tests.c file, implementing a very simple yet effective set of unit tests:

/* ... */
void block(void) {
    world_t original;
    world_t next;

    world_init(&original);
    world_block(&original, 0, 0);
    world_evolve(&original);

    world_init(&next);
    world_block(&next, 0, 0);
    assert(world_equals(&original, &next));
    printf("Block passed\n");
}
/* ... */

I didn’t use CUnit for these tests, because I wanted to have the most portable and simple setup as I could, and nothing beats the bare-bones, standard <assert.h> library.

WebAssembly with Emscripten

Let’s begin by the most jaw-dropping (in the opinion of this author) of all the compilers I’ve used in this project: Emscripten, which builds on top of an amazing LLVM foundation to compile C and C++ code into WebAssembly, which can then be hosted on any web page… literally like the one you’re reading now.

How do you use Emscripten? It couldn’t be easier. Install it and then run the emcc command. If your code compiles with Clang, it’ll compile with emcc without issues.

$ mkdir build
$ emcc src/conway.c src/world.c src/helper.c -o build/conway.html -sASYNCIFY
$ emcc src/tests.c src/world.c src/helper.c -o build/tests.html

I’ve adapted the HTML generated by Emscripten and embedded it right here: what you see below is the execution of the conway.wasm file.

And yes, all of this also works with the tests program.

The most interesting part of porting this code to Emscripten was the handling of the wait() function, which waits (asynchronously) for a full second before redrawing the world all over again. How to do this on a browser?

It turns out that the Emscripten developers also needed such a feature (duh!), so they bundled the emscripten_sleep() function for that. Just remember to declare it as extern before calling it, and then use the -sASYNCIFY flag when calling the emcc compiler.

/* ... */
#if defined(__EMSCRIPTEN__)
extern void emscripten_sleep(unsigned int);
#endif

/* ... */

/* Pause execution for the time specified */
void wait(unsigned int seconds) {
#if defined(__EMSCRIPTEN__)
    emscripten_sleep(seconds * 1000);
#endif
}
/* ... */

Maybe I’m late to the party (Emscripten has been around for a decade now, at least) but being able to compile a C or C++ app into something that a browser can execute is worthy of having my jaw falling on the floor.

Linux

Let’s play with this code on my daily driver platform.

GCC and Clang

On Linux Fedora 41, I compiled this code without issues using GCC and Clang. Everything just works. Nothing to phone home about.

Oh, and of course, let’s not leave those pesky warnings unattended:

CFLAGS = -Wall -Wextra -Werror -std=c89 -pedantic

Tiny C

Tiny C is a historically relevant C compiler, no longer maintained by its original author, Fabrice Bellard (of QEMU, FFmpeg, and even Bellard’s formula fame), but still active thanks to a loyal and dedicated community.

Tiny C is an astonishingly fast and very small compiler, extremely easy to use, that generates x86 and ARM code, and compiles and works off-the-box on Fedora 41. Quite a gem, really. Very easy to install: just clone the source repository, configure, and make:

$ git clone https://repo.or.cz/tinycc.git
$ cd tinycc
$ ./configure
$ make
$ sudo make install

Then compile and run the application with these commands:

$ mkdir build
$ tcc src/conway.c src/world.c src/helper.c -o build/conway
$ tcc src/tests.c src/world.c src/helper.c -o build/tests
$ build/conway
$ build/tests

Zig

Did you know that the Zig programming language bundles a C and a C++ compiler? I did not, and yes, it works:

$ mkdir build
$ zig cc -D__ZIG__ src/conway.c src/world.c src/helper.c -o build/conway
$ zig cc -D__ZIG__ src/tests.c src/world.c src/helper.c -o build/tests

The __ZIG__ symbol is only defined to have a nice identifier of the compiler when the app runs. Zig is based on LLVM, and thus defines the __clang__ symbol by default.

Mac OS 7 “Classic”

For much more interesting outcomes, let’s travel back in time 35 years, to the early 1990s, and let’s try to compile our code on a classic 68k Mac.

THINK C

I installed Symantec THINK C following the instructions in this beautiful blog post, expanding on the existing Mac OS Classic simulator I built in previous entries of my blog (specifically, the one about Mini vMac and the one about Excel 4 and Word 5.1a).

THINK C provides quite a rudimentary environment (the editor doesn’t even provide syntax highlighting), but it gets the job done. One thing to keep in mind when transferring the source code into the VM was to save it with standard Mac line endings (CR) instead of the usual Unix (LF) or DOS (CR+LF) endings; otherwise, THINK C would not be able to read the source at all. This can be easily done in Vim, opening the file and saving it with the following command:

:w ++ff=mac

As you can imagine, the Vim command above also accept the dos and unix entries.

There are some parts of the source code that only concern the THINK C compiler, like the configuration of the console window and some header files; these are conveniently enclosed in the usual macro guards.

#if defined(THINK_C)
    /* The title of the console is a Pascal string, not a C string! */
    unsigned char title[7] = { 6, 'C', 'o', 'n', 'w', 'a', 'y' };
    console_options.nrows = 35;
    console_options.ncols = 126;
    console_options.pause_atexit = 0;
    console_options.title = title;
    cshow(stdout);
#endif

(Do you see that variable title? I’ll come back to that later.)

Among its many capabilities, THINK C is able to build a native Mac OS application out of this code, conveniently available and double-clickable on the desktop of your Mac.

Metrowerks CodeWarrior

I’m sure that those who wrote apps for the “Classic” Mac OS System back in the 1990s remember Metrowerks CodeWarrior, the ultimate IDE that made THINK C obsolete, and that reigned supreme until Apple provided the world with Xcode for Mac OS X.

You can download Metrowerks CodeWarrior from the Macintosh Repository as a “StuffIt” archive (you will need the “StuffIt Expander” app, also available at the Macintosh Repository).

The C libraries bundled with CodeWarrior don’t have a sleep() function available, but there’s one called Delay() in the Macintosh toolbox, so we just wrap that call inside the wait() function.

#if defined(__MWERKS__)
#include <Timer.h>
#endif

/* ... */

void wait(unsigned int seconds) {
#if defined(__MWERKS__)
    unsigned long ticks = seconds * 60;
    Delay(ticks, NULL);
#endif
}

As shown in the image above, the CodeWarrior editor did feature syntax highlighting, and plenty of other goodies.

DOS

Let’s try compiling this code on an early 1990s IBM PC running DOS with some noteworthy compilers of the time.

Borland C++

Borland C++ was the “professional” counterpart to Turbo C++, and it can be downloaded from WinWorld. It bundled quite an advanced IDE and development experience for its time! I also downloaded the manuals from the Internet Archive, which were also very useful.

As you can imagine, the program has some Borland-specific lines of code enclosed within the usual guards:

/* ... */
#if defined(__BORLANDC__)
#include <conio.h> /* clrscr() */
#include <dos.h>   /* sleep() */
/* ... */

Borland C++ compiles this code in a breeze, and generates the usual CONWAY.EXE file, as expected.

Watcom C/C++

FreeDOS comes bundled with Watcom C/C++ off-the-box, a very capable C compiler with support for DOS, Windows (95 and NT), and OS/2, in 16 and 32 bits. According to WinWorld, Watcom C/C++ was used to compile DOOM for 32-bit DOS, and the screenshots also show the Windows IDE.

With a few DOS commands and the inclusion of the __WATCOMC__ macro in the code, our app can be compiled with the Watcom compiler.

@ECHO OFF

SET PATH=%PATH%;C:\DEVEL\WATCOMC\BINW
SET WATCOM=C:\DEVEL\WATCOMC
WCC386 /i=C:\DEVEL\WATCOMC\H WORLD.C
WCC386 /i=C:\DEVEL\WATCOMC\H CONWAY.C
WCC386 /i=C:\DEVEL\WATCOMC\H HELPER.C
WLINK NAME CONWAY SYSTEM 386 F HELPER.OBJ F WORLD.OBJ F CONWAY.OBJ

WinWorld offers the binaries for various versions of the Watcom compiler, and I found the Watcom C/C++ manuals (from around 2000) on openwatcom.org.

Tip: to copy your code files inside a FreeDOS virtual machine running under VirtualBox, you can use this unofficial guest addition that works like a charm.

Microsoft C/C++ 6.0

I also made this code compile and run with the Microsoft C/C++ 6, available for download on WinWorld, from 1990.

I tried using version 7, but it turns out that it requires Windows 3.1 to run (the things Microsoft did to impose Windows on the marketplace…), and I wanted to have a setup as minimalist as possible for this exercise.

One of the things that the Microsoft C/C++ 6.0 standard C library did not provide was a sleep() function, so I asked ChatGPT for one, and it didn’t disappoint. This implementation does not “loop forever” (a common brute-force approach) but instead uses the INT 0x28 DOS interruption to idle the CPU for the requested time. It even takes into account the rollover that happens when the day ends and a new one begins!

void wait(unsigned int seconds) {
#ifdef __MSC_VER
#define MK_FP(seg, ofs) ((void far *)(((unsigned long)(seg) << 16) | (unsigned)(ofs)))
    unsigned long startTime, elapsedTime;
    union REGS regs;

    // Convert seconds to timer ticks (18.2 ticks per second)
    unsigned long ticksToWait = (unsigned long)seconds * 18;

    // Get the current timer ticks (BIOS Data Area 0x40:0x6C)
    startTime = *(unsigned long far *)MK_FP(0x0040, 0x006C);

    do {
        // Get the current timer ticks again
        elapsedTime = *(unsigned long far *)MK_FP(0x0040, 0x006C) - startTime;

        // Handle overflow of timer ticks (midnight rollover)
        if ((long)elapsedTime < 0) {    // Cast to signed long for comparison
            elapsedTime += 86400L * 18; // Number of ticks in a day
        }

        // Call DOS idle interrupt (INT 0x28)
        int86(0x28, &regs, &regs);

    } while (elapsedTime < ticksToWait);
#endif
}

In general, I found quite difficult to get Microsoft’s compiler to work, due to the lack of documentation and having to try various switch combinations until the app compiled… and then ran without crashing! Yes, after I got it to compile, it would crash with a “R6000 stack overflow error”, the solution of which consists in increasing the size of the stack, with ad hoc switches for both the compiler (/F) and the linker (/STACK:).

The winning combination of flags and options was the following:

C:\>CL /IC:\C600\INCLUDE /F 8000 /G2 /AL /FPc87 /D__MSC_VER CONWAY.C HELPER.C WORLD.C /link /STACK:8000 C:\C600\LIB\LLIBC7

First I stored this command in a Batch file, as one would in DOS, but then I asked ChatGPT to transform this command line into a MAKEFILE to be used with NMAKE, Microsoft’s implementation for DOS and Windows of the make utility, and here’s the result:

# Define compiler and linker settings
CC=CL
CFLAGS=/IC:\C600\INCLUDE /F 8000 /G2 /AL /FPc87 /D__MSC_VER
LFLAGS=/STACK:8000
LIBS=C:\C600\LIB\LLIBC7

# Define source and output files
SRC=CONWAY.C WORLD.C TESTS.C HELPER.C
OBJ=CONWAY.OBJ WORLD.OBJ TESTS.OBJ HELPER.OBJ
EXE=CONWAY.EXE TESTS.EXE

# Default target: build the executable
all: $(EXE)

# Rules to build the executables
CONWAY.EXE: CONWAY.OBJ WORLD.OBJ HELPER.OBJ
	$(CC) CONWAY.OBJ WORLD.OBJ HELPER.OBJ /link $(LFLAGS) $(LIBS)

TESTS.EXE: TESTS.OBJ WORLD.OBJ HELPER.OBJ
	$(CC) TESTS.OBJ WORLD.OBJ HELPER.OBJ /link $(LFLAGS) $(LIBS)

# Rule to compile the source file into an object file
$(OBJ): $(SRC)
	$(CC) $(CFLAGS) -c $(SRC)

(Yes, it looks a lot like a standard Makefile, bar the uppercase filename.)

Here is an explanation of the compiler switches used:

  • /I provides the path to include files
  • /F 8000 increases the size of the stack to 0x8000
  • /G2 generates 80286 code
  • /AL uses the large memory model
  • /FPc87 generates non-emulated math coprocessor code (8087)
  • /D__MSC_VER defines the standard MS C compiler macro, not defined - by default in version 6 (1990)
  • /link gives options to the linker:
    • /STACK:8000 also increases the stack size
    • And finally the name of the C library to link with.

Mix Software PowerC

I found out that the code could also be compiled and run off-the-box using the Mix Software PowerC compiler for DOS, which as you can imagine I had no idea it even existed. I’m pretty sure you’ve never heard of this one either.

Its library of functions included everything I needed, including a clrscrn() and a sleep() function. The home page looks like it hasn’t been updated since the late 1990s; I’m surprised it’s still online and available at the time of this publication. The compiler itself can be downloaded from WinWorld, as usual.

In any case, I couldn’t find the matching preprocessor macro that identifies this compiler, however, so I’m passing the value __POWERC__ manually when invoking the compiler:

C:\>PC /d__POWERC__ /e CONWAY.C WORLD.C HELPER.C

Some Observations

Here are some interesting observations I made, and some “aha!” moments I experienced while working on this code:

  • Some of those early ANSI C compilers whine if you mix variable declarations and assignments; to make your source files fully compatible, I had to remember to declare all variables at the beginning of the function, and then use them afterward. Yes, this includes variables used in loops and such. This behavior was removed in C99, but some (not all) of the early compilers I tried for this article already allowed to declare variables next to their first use.
  • Function prototypes in ANSI C that do not take arguments must use the word void in the list of arguments; otherwise, some compilers assume that they take a variable list of arguments instead.
  • Not specific to ANSI C, but I’ve noticed that the Clang compiler is in general much more pedantic than GCC, and would raise more warnings. Of course, this is great, removing compiler warnings is a favorite sport of mine.
  • While writing the API for the manipulation of world_t structs I tried to apply as many teachings as I could from Jerry Cain’s Stanford course. Watch it.
  • Borland’s developer tooling was so much more advanced than Microsoft’s and Symantec’s back in the day. It’s so sad they threw it all away. Borland C++ is an incredibly fast, advanced, and productive IDE, even by today standards. (And that blue color background is iconic.)
  • In the THINK C example above I’m showing the use of a Pascal string in C code, used to define the title of the console window on the Mac. This was absolutely new to me. It is a nod to the Pascal-based toolkit that Apple provided to create native Mac applications back in the day.

Other Interesting Compilers

Looking around while preparing this article I discovered quite a few more ANSI C compilers worthy of mention; I have tried to compile the code of this app with some of them, but without success so far.

Bonustrack

If you’re interested in the subject, watch this awesome video: “Tips for C Programming” by Nic Barker, author of the Clay library. I certainly learned a lot watching it!

And then read Andrew Koenig’s 1989 classics: a paper and a book called “C Traps and Pitfalls”.

Update, 2025-02-14: Added support for Borland C++ 2.0 for OS/2. The source code required a only small addition, because the function used in the signal() call must have a special annotation, _USERENTRY, as explained in the bundled documentation. In the specific folder of the project you will find *.PRJ files, to be used from within the IDE, or *.MAK “makefiles” to be used in a full-screen OS/2 command window:

[C:\] make -f conway.mak
[C:\] make -f tests.mak

Update, 2025-02-28: I’ve just learned about the unix2dos command family, which allows text files to be converted back and forth from Mac, DOS, and Unix file formats.