Writing portable C programs
The C programming language makes it easy to write powerful, flexible programs. And because you can find C compilers for pretty much any operating system, you can port your programs to other platforms. If your program is pretty basic, and only uses the C standard library of functions, then porting should be a pretty straightforward process. Just copy your program to the other platform, recompile it, and you’re done.
But if your program requires features specific to one operating system, then you need to consider how to support other systems when writing portable C programs.
That’s where using preprocessor directives can come in handy. The C preprocessor is part of the compilation process, but you may never really think about it. The preprocessor examines the C source files and takes action on any lines like #include
to insert the contents of another file at that point. The preprocessor also responds to #define
directives, to define a constant value in a program, such as with this:
#define PI 3.141
But the preprocessor can also evaluate other things on its own. One neat feature is to determine if certain values have already been defined, and insert code (or not) depending on those values.
Know the system
The C preprocessor also defines values for the system that you are compiling for. For example, if you are compiling a program on Linux, the GNU C Compiler will define __linux__
as a constant. With this value, you can insert code if specific to the Linux operating system, or insert other code if not.
Check your compiler’s documentation for the values that will be defined for each target platform, but usually these are written with two underscores before and after the value, like __linux__
for Linux systems, __unix__
for Unix systems, or __DOS__
for DOS systems like FreeDOS.
For example, let’s say your program needed to generate a random number between 1 and 100. On Linux, you can use the getrandom
system call to let the kernel fill a variable with random bits like this:
int secret;
unsigned int randdata;
getrandom(&randdata, sizeof(int), GRND_NONBLOCK);
secret = randdata % 100 + 1;
But that only works on Linux. Other operating systems may not have the getrandom
system call. To generate a random number from 1 to 100 on these other systems, you might need to “fall back” to the C standard library functions with srand
to seed the random number generator function, usually based on the current time, and rand
to generate a random value from zero to some very large maximum number:
int secret;
srand((unsigned int) time(NULL));
secret = rand() % 100 + 1;
You can write a more portable version by using #if
to determine if the __linux__
value is defined at compile-time, and inserting the appropriate code depending on the system you are compiling for: either Linux or some other platform.
int secret;
#if defined(__linux__)
unsigned int randdata;
getrandom(&randdata, sizeof(int), GRND_NONBLOCK);
secret = randdata % 100 + 1;
#else
srand((unsigned int) time(NULL));
secret = rand() % 100 + 1;
#endif
A portable number-guessing game
Let’s see this in practice by writing a number-guessing game in C that should run on any operating system. When compiling for Linux, the program uses getrandom
to generate truly random bits. But when compiling for another system like FreeDOS, the program uses srand
and rand
to generate pseudo-random values.
In this “Guess the number” game, the program generates a secret random value between 1 and 100. Then the user must guess the number. At each guess, the program gives the user a hint, printing “Too low” or “Too high” until the user guesses the correct value. For debugging, we’ll also print the secret number at the start of the program:
#include <stdio.h>
#if defined(__linux__)
#include <sys/random.h>
#else
#include <time.h>
#include <stdlib.h>
#endif
int main()
{
int secret, guess;
#if defined(__linux__)
unsigned int randdata;
getrandom(&randdata, sizeof(int), GRND_NONBLOCK);
secret = randdata % 100 + 1;
#else
srand((unsigned int) time(NULL));
secret = rand() % 100 + 1;
#endif
printf("debugging: %d\n", secret);
puts("Guess a random number from 1 to 100");
do {
puts("Your guess?");
scanf("%d", &guess);
if (guess < secret) {
puts("Too low");
}
else if (guess > secret) {
puts("Too high");
}
} while (guess != secret);
puts("That's right!");
return 0;
}
The program uses #if
several times. At the start of the source file, the program uses #if
to include the correct header files: sys/random.h
if compiling on Linux, or time.h
and stdlib.h
for other systems. Later, the program uses a similar #if
to insert the appropriate code to generate a random number between 1 and 100.
During the compilation process, the C preprocessor inserts the appropriate code and replaces constants (like GRND_NONBLOCK
) with hard-coded values. When compiled on a Linux system, it’s as though the program were written like this:
int main()
{
int secret, guess;
unsigned int randdata;
getrandom(&randdata, sizeof(int),
0x01
);
secret = randdata % 100 + 1;
printf("debugging: %d\n", secret);
puts("Guess a random number from 1 to 100");
do {
puts("Your guess?");
scanf("%d", &guess);
if (guess < secret) {
puts("Too low");
}
else if (guess > secret) {
puts("Too high");
}
} while (guess != secret);
puts("That's right!");
return 0;
}
In this case, the 0x01
is the value of GRND_NONBLOCK
after the C preprocessor expands the named constant into a value.
Let’s play
If we save the program as guess.c
and compile it on a Linux system, the program uses getrandom
to generate the random secret value:
$ gcc -Wall -o guess guess.c
$ ./guess
debugging: 81
Guess a random number from 1 to 100
Your guess?
1
Too low
Your guess?
100
Too high
Your guess?
80
Too low
Your guess?
82
Too high
Your guess?
81
That's right!
If we copy the guess.c
source file to another system, such as FreeDOS, and compile it there, the program compiles the same. But behind the scenes, the program is using rand
to generate pseudo-random values:
> wcl -q guess.c
The -q
command line option to the OpenWatcom C Compiler (wcl
) makes the compiler operate silently. Otherwise, OpenWatcom tends to print lots of extra information that we don’t need to see here.
> guess
debugging: 52
Guess a random number from 1 to 100
Your guess?
1
Too low
Your guess?
100
Too high
Your guess?
50
Too low
Your guess?
53
Too high
Your guess?
52
That's right!
Writing portable programs
Programmers will need to create programs that support different operating systems. You can’t assume all the world runs Linux. But you can make it easier to write portable C programs by using #if
in your code to detect the target system and use the appropriate code. This method works well for changes that aren’t too large, such as this example of generating a random number using either a Linux system call or the C standard library.