Portable programming practices
It would be great if every operating system acted exactly the same, so that we could write one program one way, and it would “just work” wherever we ran it. But that’s not how programming works – at least, not if your program needs to do anything that’s nontrivial, such as using specific features of a user interface library.
For small changes when programming across platforms, you can use #if
to let the C preprocessor include different source code to support different environments. But for writing portable programs that require larger changes, it might be easier to create programming wrappers that create an “API” or Application Programming Interface so that your program can use those API functions and effectively “hide” the underlying platform-specific interfaces.
Define the interfaces
Let’s look at a simple example to see how you might create your own “API” to hide the system-specific programming interfaces. This is a variation on the classic “Hello world” program that prints a string to the screen:
#include <stdio.h>
int main()
{
puts("Hello world");
return 0;
}
For this demonstration, let’s create a visual version of the program that centers the text on the screen. When programming on Linux, we can use the ncurses library of functions. On FreeDOS, we can display text directly to the console using the conio and graph functions.
First, we’ll define a set of functions that we’ll use as interfaces to control screen output:
extern int init_screen(void);
extern int end_screen(void);
extern int center_title(const char *s);
extern int pause(void);
init_screen
will initialize the screen, and make it ready to print text to itend_screen
will reset the screen back to a normal mode, such as when we are done with the program and ready to quitcenter_title
will center a short string (text) to the screen, both vertically and horizontallypause
will wait for the user to press a key
With these function definitions, we can write a short program to display the text “Hello world” to the screen, then wait for the user to press any key and exit to the operating system:
#include <stdio.h>
extern int init_screen(void);
extern int end_screen(void);
extern int center_title(const char *s);
extern int pause(void);
int main()
{
if (init_screen() == 0) {
puts("cannot initialize screen");
return 1;
}
center_title("Hello world");
pause();
end_screen();
return 0;
}
The Linux functions
The Linux version of this program should run in text mode, using the ncurses library to display text to the terminal. We can create a set of functions in a file called screen.c
that provides wrappers to the ncurses functions:
#include <curses.h>
#include <string.h> /* strlen */
int init_screen(void)
{
initscr();
cbreak();
noecho();
return LINES;
}
int end_screen(void)
{
if (endwin() == OK) {
return 1; /* success */
}
else {
return 0; /* fail */
}
}
int center_title(const char *s)
{
int len;
len = strlen(s);
mvaddstr(LINES / 2, (COLS - len) / 2, s);
if (refresh() == OK) {
return len; /* success */
}
else {
return 0; /* fail */
}
}
The functions isolate the extra code that we need to use the ncurses library. For example, the init_screen
wrapper function calls initscr
to initalize the screen using ncurses, then cbreak
to disable line buffering and erase/kill processing, and noecho
to prevent functions like getch
from printing the keystrokes to the screen. The init_screen
wrapper returns the number of lines defined on the screen.
This is a rather simple implementation of init_screen
. A more robust version would detect if initscr
failed, and exit early. But we’ll use this version so it’s easier to compare with the version we’ll write later for FreeDOS, which also returns the number of lines on the screen.
To manage input, we can define the pause
function in a file called input.c
. Splitting up the interfaces into separate files isn’t strictly necessary, but it can make managing larger programs much easier because each file won’t be too long.
#include <curses.h>
int pause(void)
{
return getch();
}
Because input.c
and screen.c
are both written to be specific to the Linux platform, you should save these in a separate directory called linux
. Using a directory like this helps with project organization, and makes it easier to add similar support for other operating systems just by adding a new directory.
The FreeDOS functions
We can create similar versions of these API wrappers that target the OpenWatcom C compiler on FreeDOS. Using OpenWatcom C, programs can access the console or video mode directly, which means the program can display text very quickly. The interfaces are different from the ncurses library on Linux, but these changes are not made visible to the hello.c
program because they are hidden behind the wrapper functions. For example, the screen functions might be defined in a file called screen.c
:
#include <conio.h>
#include <graph.h>
#include <string.h> /* strlen */
int init_screen(void)
{
return _setvideomode(_TEXTC80); /* color 80x25 */
}
int end_screen(void)
{
return _setvideomode(_DEFAULTMODE);
}
int center_title(const char *s)
{
int len;
len = strlen(s);
_settextposition(25 / 2, (80 - len) / 2);
_outtext(s);
return (len);
}
These wrapper functions don’t need to be complex, but they include all of the platform-specific actions. For example, this version of init_screen
is a one-line function that calls _setvideomode
from OpenWatcom C. The _setvideomode
puts the console into to requested mode (in this case, color text with 80 columns and 25 lines) and returns the number of lines in the new video mode, or zero if the mode failed.
We can similarly define the pause
function in a file called input.c
, like this:
#include <conio.h>
int pause(void)
{
int key;
key = getch();
if (key == 0) {
/* extended key, call it again */
getch();
}
/* returns 0 if extended key */
return key;
}
The getch
function in OpenWatcom is itself a kind of wrapper to the INT 16,0
BIOS function to get a single key from the keyboard. The BIOS returns the scan code (between 0 and 255) of the key pressed by the user. If the key is an extended key such as F1, then the BIOS function returns zero, and you need to call it a second time to retrieve the scan code. That’s why the pause
function uses getch
a second time if the first usage returned a zero value.
Just as above, save both the screen.c
and input.c
functions in a directory for the platform. In this case, save them in a dos
directory.
Compiling for different systems
With these platform-specific functions isolated to different files, it’s fairly straightforward to compile a separate version of the program for each platform: one for Linux, and one for FreeDOS.
To compile on Linux, use this command line:
$ gcc -Wall -o hello hello.c linux/*.c -lncurses
The -Wall
option says to print all warnings; I often compile programs using this option so I can write warning-free code. For this example, the -Wall
option is not necessary, but it’s good to have.
The -lncurses
option at the end tells the compile to link with the ncurses
library.
Running the hello
program displays the “Hello world” text in the center of the terminal, and waits for the user to press a key:
To compile the program on FreeDOS, first copy the hello.c
file and dos
directory to a virtual machine running FreeDOS, then use OpenWatcom C to compile it:
> wcl -q hello.c dos\*.c
The -q
option tells the OpenWatcom C compiler to run “quietly.” This suppresses any information messages, and only prints warnings and errors. Without -q
, OpenWatcom tends to prints a lot of extra information that we don’t need here.
This creates a hello.exe
program that displays the text “Hello world” in the center of the screen, and waits for the user to press any key:
Writing with wrappers
Using wrapper functions like this can make it easier to write programs that support very different platforms. While #if
can make small changes for portable programs, sometimes you need to make larger changes for different platforms, such as screen display or even a graphical environment. Isolating the platform-specific code in these wrapper functions helps to create more portable applications.