Programming across platforms

0

The C programming language remains an extremely popular language due to its relatively simple syntax that allows you to write all kinds of programs. And because the C language is portable across so many systems, you can find a C compiler pretty much everywhere, to run your code on anything.

But that portability can itself be a challenge, because while you can find a compiler for any system, that doesn’t mean that they all work exactly the same. For example, C programs on Linux typically use curses to create text-mode programs that run on the terminal. But another platform might not use the terminal concept; notably, the DOS operating system used direct access to the video hardware to operate at the console using the conio library.

If you’re writing a C program that should operate on multiple systems, you can use the C preprocessor to provide a mechanism that recognizes what system you’re compiling for, and insert the appropriate code.

Preprocessor directives

The preprocessor is part of the standard C compilation process, but you may not really think about it. For example, the line:

#include <stdio.h>

… at the start of any program is an example of a preprocessor directive to include the stdio.h header file at that point in the code.

The preprocessor operates this way on lines that start with # like this. And the preprocessor has other directives or command actions that it can take, including the #define action. In C programming, the #define allows you to define a macro that never changes its value and that you can reference later, such as the value of pi with:

#define PI 3.141

But the definition can be used in other ways too, including identifying the system at compile-time.

Defining the system

The C preprocessor creates a definition that identifies the platform you’re compiling for. Let’s say you’re on Linux and compiling a program for Linux; your C compiler will automatically define a macro that identifies the system as Linux.

It’s very common for this definition to be written in all lowercase, and begin and end with two underscores, like __linux__ to identify the system as Linux. At the same time, because Linux is a Unix-like operating system, your C compiler also creates another definition called __unix__.

Every Unix platform that I’ve used follows this standard, although different systems and C compilers might use a single underscore, or no underscore at all. On DOS, C compilers define a similar macro, usually __DOS__ to recognize the system as DOS, whether that’s FreeDOS, MS-DOS, or another DOS.

Programming across systems

You can use these definitions in your code to automatically insert specific support for each operating system when compiling your program. This is a common method when porting an application from one system to another.

Let’s look at a very simple example so we can follow along with what’s going on behind the scenes:

#include <stdio.h>

int main()
{
    puts("Hello world");

#if defined(__unix__)
    puts("This is Unix");
#endif

#if defined(__linux__)
    puts("Yes, it's Linux");
#endif

#if defined(__DOS__)
    puts("We're running on DOS");
#endif

    return 0;
}

This uses the #if defined() to determine if a macro has been defined by the C preprocessor. It doesn’t matter what the value of this macro is, just that it has been defined.

If the system is a Unix-like system, the __unix__ value will be defined, and the program will add the instruction to print “This is Unix.” If it’s running on Linux, the C compiler will define __linux__ so the program can insert the instruction to print “Yes, it’s Linux.” On another system, such as AIX or HP-UX, the __linux__ macro will not be defined, so this instruction won’t get inserted into the program.

On DOS systems, the compiler will instead define the __DOS__ value, so the program can use that to add the instruction to print “We’re running on DOS.” This only happens when compiling for DOS systems; this line doesn’t get added to the source code when compiling for another platform like Linux.

Effectively, when compiling on Linux, it’s as though the program’s source code looks like this:

#include <stdio.h>

int main()
{
    puts("Hello world");

    puts("This is Unix");

    puts("Yes, it's Linux");

    return 0;
}

If we save this program as hello.c and compile it, we will see only those three lines printed to the terminal:

$ gcc -o hello hello.c

$ ./hello
Hello world
This is Unix
Yes, it's Linux

Compiling the same program on FreeDOS using OpenWatcom C generates a program that only prints the “Hello” and “DOS” lines:

>wcl -q hello.c

>hello
Hello world
We're running on DOS

Cross-platform programming

Programmers often need to maintain programs that support a variety of targets, not just one. Using these C compiler preprocessor directives to detect the operating system can make it easier to support multiple platforms at once. For example, a program might need to support curses functions when used on Linux or other Unix-like systems, but use the conio functions to access the console when compiling the same program on FreeDOS.

Check your compiler’s manual for details about how to find the definition for your platform.

Leave a Reply