We ❤️ Open Source

A community education resource

10 min read

From TurboC to Open Watcom: A tale of two C compilers and their interfaces

Why C is not always GNU C: Leveraging platform-specific libraries for developers.

In the 1980s, Henry Spencer captured several “rules” about C programming in a list called The Ten Commandments for C Programmers, including this advice:

Thou shalt foreswear, renounce, and abjure the vile heresy which claimeth that “All the world’s a VAX,” and have no commerce with the benighted heathens who cling to this barbarous belief, that the days of thy program may be long even though the days of thy current machine be short.

The annotated edition of the Ten Commandments also notes that the same may be said of other common systems, such as Sun and ‘386 systems, both of which were popular at the time. I apply this lesson more generally in reminding people that not everything is GNU C.

While it’s true that C compilers must follow the same language specification such as C11 (published in 2011) or C23 (published in 2024), it’s important to remember that compilers can differ in the language extensions. For example, libraries to support specific hardware or windowing environments may differ from system to system.

It’s in the details

This difference was evident in the early days of C programming. I learned C programming in the early 1990s when C had only recently been standardized by the American National Standards Institute (ANSI). Sometimes referred to as “C89” (for the year it was ratified) or “C90” (for the year it was adopted by the International Organization for Standardization, or ISO) this “ANSI C” language defined the C standard library and common programming interfaces. But the language standard only addressed cross-platform programming standards. It did not (and could not) address platform-specific programming interfaces.

One interface you might know is curses (or ncurses on most Linux systems). This is a popular library to support terminal programming on Unix-like systems to create “text-based user interfaces” or “TUIs.” By design, ncurses is compatible with curses, so you should be able to compile the same program written for Unix in the 1980s (for curses) on a modern Linux system (with ncurses) without having to make changes.

DOS systems from the 1980s and 1990s used a different programming interface to create text-based user interfaces. The conio set of functions provided direct console input and output. Console access is different from terminal access, and provides direct access to the display hardware. However, while some of the conio functions are similar between different DOS compiler implementations, not every interface works the same or is even named the same. Any similarities between compilers is usually because vendors usually wanted to make it easy for programmers to move off a competitor’s product.

For example, to test if the user has pressed a key on the keyboard, programs can use kbhit from conio, which returns a true value if a key is waiting in the input buffer. The key’s code can be retrieved with getch.

But the details of how getch is implemented might differ. I first learned C programming on DOS with Borland’s C compiler, a proprietary compiler that was very popular at the time. Borland’s TurboC and TurboC/C++ remain “closed” in 2025 although the current owner has made them available for download at no cost.

Read more: 5 FreeDOS editors I love

Borland's TurboC/C++ compiler, version 1.01
Borland’s TurboC/C++ compiler, version 1.01

TurboC’s getch returns the character pressed on the keyboard; pressing A returns 65. If the user pressed an extended key, like F1, then getch returns 0 and the program needs to call getch again to get the scan code of the extended key.

Open Watcom C provides an identical getch implementation. Open Watcom is an open source C compiler, originally released as Watcom C (proprietary, later released as Open Watcom under the Open Watcom Public License).

Open Watcom C on DOS
Open Watcom C on DOS

Under either C compiler, a program to get a key and print its value would first call getch, then evaluate its return value. If zero, the program would know it was an extended key, and would need to call getch again to retrieve the scan code. A sample implementation might look like this:

#include <stdio.h>
#include <conio.h>

int main()
{
   int ch;

   puts("press a key:");

   ch = getch();

   if (ch == 0) {
       puts("extended");
       ch = getch();
   }

   printf("%d\n", ch);

   return 0;
}

However, the getch in BCC (or Bruce’s C Compiler) returns a two-byte value; the high value is the keyboard scan code, and the low value is the character. This is because BCC is a very early C compiler that actually converts C to assembly, then assembles that into a DOS program. Behind the scenes, the BCC getch implementation actually calls the BIOS using INT 16, function 00h which returns the BIOS scan code in AH (high value) and the ASCII value in AL (low value).

BCC is not completely ANSI C
BCC is not completely ANSI C

For example, pressing A returns 30 in the high value and 65 (ASCII A) as the low value, but pressing a returns 30 as the high value and 97 (ASCII a) in the low value. Similarly, Z returns 44 in the high value and 90 as the low value, while z returns 44 as the high value and 122 in the low value.

#include <stdio.h>
#include <conio.h>

int main()
{
   int ch;

   ch = getch();
   printf("high,low = %d,%d\n", ch>>8, ch & 255);

   return 0;
}

Read more: How to navigate the command line

Same library, different implementations

One area that really stands out as being implemented differently depending on the compiler is how to print text to the console. In TurboC, these interfaces are defined in conio.h, but Open Watcom implements them in graph.h. The functions also have completely different names. For example, to set the text color and background color, TurboC uses textcolor and textbackground from conio.h and Open Watcom uses _settextcolor and _setbkcolor from graph.h.

The functions to define a “window” or region on the screen are also different in each implementation, as are screen coordinates. Open Watcom uses row,col coordinates while TurboC uses col,row (that’s x,y) coordinates. To move to column 1 and row 2 within the current text window, Open Watcom uses _settextposition(2,1) in graph.h but TurboC uses gotoxy(2,1) from conio.h.

Let’s demonstrate this with a short program that does a few things typical of a text-based user interface:

  1. Clear the screen to use white text on a blue background (a common DOS color scheme)
  2. Center a title in bright white on a cyan background
  3. Display a red window with white text, plus a black “shadow”
  4. Print a status message at the bottom in black text with a white background
  5. Waits for the user to press a key before exiting back to DOS

Open Watcom’s conio

Writing this program in Open Watcom C requires that you first set the DOS video mode with _setvideomode(_TEXTC80) to put the display into text mode with 80 columns. This function returns the number of lines or rows available (such as 25 for an 80×25 display) or 0 if it could not set the mode. The program can then use _setbkcolor and _settextcolor to define the colors, _settextwindow to define a window, and _clearscreen to erase the screen or window. To make the program easier to read, I’ve also defined several functions to display a window, set a title, or print a status line:

#include <stdio.h>
#include <string.h>

#include <conio.h>
#include <graph.h>

void set_title(const char *title)
{
   _settextwindow(1, 1, 1, 80);
   _setbkcolor(3);                    /* cyan */
   _clearscreen(_GWINDOW);

   _settextposition(1, 40 - (strlen(title) / 2));
   _settextcolor(15);                 /* br white */
   _outtext(title);
}

void set_status(const char *status)
{
   _settextwindow(25, 1, 25, 80);
   _setbkcolor(7);                    /* white */
   _clearscreen(_GWINDOW);

   _settextposition(1, 2);
   _settextcolor(0);                  /* black */
   _outtext(status);
}

void info_win(const char *text)
{
   _settextwindow(6, 21, 16, 61);
   _setbkcolor(0);                    /* black */
   _clearscreen(_GWINDOW);

   _settextwindow(5, 20, 15, 60);
   _setbkcolor(4);                    /* red */
   _clearscreen(_GWINDOW);

   _settextposition(3, 3);
   _settextcolor(7);                  /* white */
   _outtext(text);
}

int main()
{
   if (_setvideomode(_TEXTC80) == 0) {
       puts("cannot set mode");
       return 0;
   }

   /* white text on blue bg */

   _setbkcolor(1);                    /* blue */
   _settextcolor(7);                  /* white */
   _clearscreen(_GCLEARSCREEN);

   /* do a test */

   set_title("Title");
   info_win("This is a test of OpenWatcom conio.");
   set_status("press any key to quit...");

   if (getch() == 0) {
       getch();
   }

   /* done */

   _setvideomode(_DEFAULTMODE);

   return 0;
}

If we save this program and compile it with Open Watcom, we can run it to generate this sample interface:

Sample program with Open Watcom
Sample program with Open Watcom

TurboC’s conio

Writing the same program with TurboC requires updating the functions that define the video properties. TurboC did not have a separate function to initialize the display; the programmer was expected to do the work to clear the screen and do any necessary setup on their own. Also note that the screen coordinates use col,row instead of the row,col system used in Open Watcom.

Another key difference is how TurboC and Open Watcom erase the screen. Open Watcom’s _clearscreen accepts an argument that will either clear the current window or (with _GCLEARSCREEN) reset the window and clear the screen. Setting the video mode with _setvideomode also erases the screen by default.

In contrast, TurboC leaves that to the programmer. TurboC’s clrscr only erases the current window; to clear the whole screen, the program first needs to reset the window from 1,1 (upper left) to 80,25 (bottom right) then use the clrscr to clear it. Not resetting the window before exiting the program can leave the display in a strange state, which is why I defined a separate function called resetwin to reset the window and colors before erasing the screen.

#include <stdio.h>
#include <string.h>

#include <conio.h>

void resetwin()
{
   window(1, 1, 80, 25);              /* full screen */
   textcolor(7);                      /* white */
   textbackground(0);                 /* black */
   clrscr();
}

void set_title(const char *title)
{
   window(1, 1, 80, 1);               /* x,y */
   textbackground(3);                 /* cyan */
   clrscr();

   gotoxy(40 - (strlen(title) / 2), 1);
   textcolor(15);                     /* br white */
   cputs(title);
}

void set_status(const char *status)
{
   window(1, 25, 80, 25);
   textbackground(7);                 /* white */
   clrscr();

   gotoxy(2, 1);
   textcolor(0);                      /* black */
   cputs(status);
}

void info_win(const char *text)
{
   window(21, 6, 61, 16);
   textbackground(0);                 /* black */
   clrscr();

   window(20, 5, 60, 15);
   textbackground(4);                 /* red */
   clrscr();

   gotoxy(3, 3);
   textcolor(7);                      /* white */
   cputs(text);
}

int main()
{
   /* white text on blue bg */

   textbackground(1);                 /* blue */
   textcolor(7);                      /* white */
   clrscr();

   /* do a test */

   set_title("Title");
   info_win("This is a test of TurboC conio.");
   set_status("press any key to quit...");

   if (getch() == 0) {
       getch();
   }

   /* done */

   resetwin();

   return 0;
}

If we save this program and compile it with TurboC, we can run it to generate this sample interface:

Sample program with TurboC
Sample program with TurboC

Not everything is the same

It can be tempting to think that “all the world uses GNU C” but this is an unsafe assumption. Some modern systems may differ in specific details, and older systems may have their own unique interfaces that are quite different to how you might be used to doing things on Linux in 2025. Using these implementation-specific features comes at a cost of portability. But these differences can expose great power to any programmers who know how to use them. Explore your compiler’s documentation to see what other features you can leverage to make your program more interesting and to fully use the power of the system you’re running on.

More from We Love Open Source

About the Author

Jim Hall is an open source software advocate and developer, best known for usability testing in GNOME and as the founder + project coordinator of FreeDOS. At work, Jim is CEO of Hallmentum, an IT executive consulting company that provides hands-on IT Leadership training, workshops, and coaching.

Read Jim's Full Bio

The opinions expressed on this website are those of each author, not of the author's employer or All Things Open/We Love Open Source.

Want to contribute your open source content?

Contribute to We ❤️ Open Source

Help educate our community by contributing a blog post, tutorial, or how-to.

This year we're hosting two world-class events!

Join us for AllThingsOpen.ai, March 17-18, and All Things Open 2025, October 12-14.

Open Source Meetups

We host some of the most active open source meetups in the U.S. Get more info and RSVP to an upcoming event.