views:

367

answers:

8

This question is intentionally very generic and I'm not much of a C programmer although I dabble here and there. The following code is intentionally vague and probably won't compile, but I hope you get the point...

Handling platform specifics seems dynamically in a compiled language like C seems unnecessary and even scary:

int main(int argc, char *argv[]) {
    if (windows)
        dowindowsroutine();
    else
        dounixroutine();
    return 0;
}

However, handling platform specifics through really very basic macros seems gross too as the function gets chopped up into small pieces which may not compile properly (read answer to http://stackoverflow.com/questions/1644868/c-define-macro-for-debug-printing for a similar problem).

int main(int argc, char *argv[]) {
    #ifdef windows
    dowindowsroutine();
    #else
    dounixroutine();
    #endif
    return 0;
}

So what's the "right" way to do this? Is it a case-by-case basis? Is there a good way to keep these gross macros out of functions entirely? I remember reading somewhere (probably in the kernel docs or something related) that macros (more importantly, complex macro logic) is meant for header files, not .c files. How do you handle this kind of stuff?

I'm sick of "spaghetti code" with ifdef's inside of functions... I spose there are some cases where it may be OK, but the majority of code I see abuse it.

Note: I've seen some perl XS code look like it wraps function prototypes to and things, but is that the only way? Isn't that somewhat gross in the community? Or is that OK? Coming from a mostly "scripted" background of perl, python, shell,... It's hard for me to tell.

Update: Let me be more clear, the problem I'm trying to avoid is that I don't want choppy code. I want to be able to ensure that if my code breaks at compile time in linux, it also breaks at compile time in windows. With the choppy code, its possible to break windows from compiling, but not linux and vice versa. Is this kind of thing possible? The closest thing to this so far is ifdef'ing the entire function, but the function names are the same, is there a better solution where there is one interface, but the OS specific parts have their OS name embedded into the name?

+1  A: 

Quite often the code fragments become large enough that it looks like this:

#if WINDOWS
void dowindowsroutine()
{
}
#else
void dounixroutine()
{
}
#endif

int main(int argc, char *argv[]) {
    #if WINDOWS
        dowindowsroutine();
    #else
        dounixroutine();
    #endif
    return 0;
}

Yes, here's an example of where you would indent preprocessor macros.

Sometimes it becomes large enough that we do it in the linker (linking against impwindows.o or impunix.o depending on makefile switch).

Joshua
But what if dowindowsroutine() starts to take arguments and dounixroutine() either doesn't need to? Or dounixroutine() needs to start taking arguments too, but that doesn't doesn't stay in sync? The two segments start diverging. Is there a better way to keep these fragments in sync?
xyld
@xlyd if the fragment arguments start diverging a lot you're doing something wrong.
Joshua
+1  A: 

If you don't like ifdefs inside of functions... just write your code in such a way that the parts you would ifdef out, are taken into it's own function. And ifdef those functions. But just write your program naturally, don't re-write the entire program twice, one for windows and one for linux ;)

boytheo
+5  A: 

I think the right way to handle this is to split your code base into platform specific modules and have them assembled at build time (this of course requires some sort of platform abstraction layer.) That way your Unix code is not littered with Windows calls, and vise versa. Effectively you move your ifdef headache to the makefiles.

Nikolai N Fetissov
Do you see a lot of the logic in these copied between the modules? Kind of what @boytheo wrote in his answer? Or is it only the diverging parts?
xyld
Well, if you're writing a generic library or a tool to be build on every last platform, you might invest some time into abstracting the OS primitives you are using (think Qt, GTK, Java, etc.) In the application world ... how often you migrate your production from Windows to Linux and back?
Nikolai N Fetissov
+1  A: 

There is no absolute truth.

The way I would do it. I would abstract the routine away, and only call a single function that would check what platform you're using. I've included that as well because I like it so there.

#define PLATFORM_WINDOWS          0
#define PLATFORM_LINUX          1
#define PLATFORM_MACINTOSH      2
#define PLATFORM_WM6           3
#define PLATFORM_ANDROID       4
#define PLATFORM_SOMETHINGELSE    1000

#define COMPILER_VS6          1200
#define COMPILER_VSDOTNET    1300
#define COMPILER_VS2005    1400
#define COMPILER_VS2008    1500
#define COMPILER_GPP          100

#ifndef PLATFORM

    /*
        Detect which platform this is being run on.
        Thanks go to Pixel Toaster for most of the flags.
    */

    #if defined(WIN32) || defined(WIN64) || defined(_WIN32) || defined(_WIN64)
        #define PLATFORM         PLATFORM_WINDOWS
        #define PLATFORM_NAME       "Windows"
    #elif defined(__APPLE__) || defined(__MACH__)
        #define PLATFORM         PLATFORM_MACINTOSH
        #define PLATFORM_NAME       "Macintosh"
    #elif defined(linux) || defined(__linux) || defined(__linux__) || defined(__CYGWIN__)
        #define PLATFORM         PLATFORM_LINUX
        #define PLATFORM_NAME       "Linux"
    #else
        #define PLATFORM         PLATFORM_SOMETHINGELSE
        #define PLATFORM_NAME       "Something Else"
    #endif

    /*
        How many bits is this system?
    */

    // Windows
    #if (defined(_WIN64) || defined(WIN64))
        #define PLATFORM_BITS       64
    // Macintosh
    #elif (defined(__LP64__) || defined(_LP64) || defined(__ppc64__))
        #define PLATFORM_BITS       64
    // Linux
    #elif (defined(__x86_64__) || defined(__64BIT__) || (__WORDSIZE == 64))
        #define PLATFORM_BITS       64
    #else
        #define PLATFORM_BITS       32
    #endif

    /*
        Determine which compiler was used to compile this program.
    */

    #ifdef _MSC_VER
        #define COMPILER_VERSION       _MSC_VER

        #if (COMPILER_VERSION >= 1500)
            #define COMPILER        COMPILER_VS2008
            #define COMPILER_NAME      "Visual Studio 2008"
        #elif (COMPILER_VERSION >= 1400)
            #define COMPILER        COMPILER_VS2005
            #define COMPILER_NAME      "Visual Studio 2005"
        #elif (COMPILER_VERSION >= 1300)
            #define COMPILER        COMPILER_VSDOTNET
            #define COMPILER_NAME      "Visual Studio .NET"
        #elif (COMPILER_VERSION >= 1200)
            #define COMPILER        COMPILER_VS6
            #define COMPILER_NAME      "Visual Studio 6"
        #else
            #error This version of Visual Studio is not supported.
        #endif
#elif defined(__GNUC__)
        // TODO: get actual compiler information for G++

        #define COMPILER_VERSION       (__GNUC__ * 10000 + __GNUC_MINOR__ * 100 + __GNUC_PATCHLEVEL__)

        #define COMPILER_NAME       "G++"

        #define PLATFORM         PLATFORM_LINUX
    #endif  

    /*
        Compiler specific options
    */

    #if PLATFORM == PLATFORM_WINDOWS
        // Support for Windows 98
        #if COMPILER_VERSION >= COMPILER_VS6 && COMPILER_VERSION < COMPILER_VSDOTNET
            #pragma comment(linker, "/OPT:NOWIN98")
        #endif

        #if COMPILER_VERSION >= COMPILER_VSDOTNET
            #define FAST_CALL __declspec(noinline) __fastcall
        #else
            #define FAST_CALL __fastcall
        #endif
    #endif

#endif

#define MAIN   int main(int argc, char *argv[])

void DoRoutine()
{
#if PLATFORM == PLATFORM_WINDOWS
    // do stuff
#elif PLATFORM == PLATFORM_LINUX
    // do other stuff
#endif
}

MAIN
{
    DoRoutine();
}
knight666
+2  A: 

In C++ or some other object-oriented language you could abstract the platform away and use a factory (etc) to instantiate the right kind of platform-specific implementation.

That said, if you need/want to #ifdef as outlined above I'd suggest something more like:

#if WINDOWS
void routine()
{
    /* windows implementation here */
}
#else
void routine()
{
    /* non-windows implementation here */
}
#endif

int main(int argc, char *argv[]) {
    routine();
    return 0;
}
Rob Pelletier
I like it because there is no ambiguity as to what exactly the end code looked like. There is no "choppy-ness" to the code. The symbols are exactly the same.
xyld
A: 

You could create separate shared libraries for each platform (.so or .dll), then dynamically load the appropriate library at runtime. Each library would contain the platform-specific code. You would need a wrapper for the OS-specific load library calls, but that would probably be your only #ifdef'd function.

Todd Stout
A: 

If you are really serious about handling platform dependent code (Everyone else, shield your eyes now!), you should look into imake or autoconf/automake, especially the latter.

Imake, originally used by the X window system, works something like knight666's answer; it uses a bunch of system-dependent configuration files to determine what exactly your system is and to allow you to handle the differences between systems. As far as I know, it never saw much actual usage, and judging by the Wikipedia page, even the X window system does not use it any more. Imake is a bit of a crawling horror.

Autoconf/automake (and libtool) are a bundle of shell scripts, Makefile templates, and m4 macros which generate the "configure" script known and loved by everyone who has built software on anything vaguely Unixish. The configure script runs a bunch of tests and then defines preprocessor macros and writes a Makefile, both of which allow your code to handle the system dependencies. If you are interested, a fairly decent book, GNU Autoconf, Automake, and Libtool, is available, although it looks to be hideously out of date. Autoconf and automake are a bit of a crawling horror.

Ideally, you would create a "platform abstraction layer" that hides the system-specific calls without duplicating any of your application logic. Typically, you will end up with #ifdef's or a code build system that only compiles the parts of the platform abstraction layer that match your specific platform.

Doing dynamic things like "if (windows) { doWindowsRoutine(); }" typically will not work, because doWindowsRoutine will not compile on a non-Windows machine.

In all, the whole area is more than a bit of a crawling horror.

Tommy McGuire
The question is more along the lines of how to keep your code from becoming a choppy mess of ifdefs, not so much about using platform agnostic tools. I know about autoconf/automake, and I'm not sure they address this problem directly. But then again, I'm not all that familiar with them.
xyld
Well, putting the system-dependent code in separate files (which is moderately well supported by autoconf) partially eliminates ifdef choppiness, but then you have to deal with multiple, optional module choppiness.
Tommy McGuire
+2  A: 

I've spent the better part of my career having to support multiple platforms concurrently. This is generally how I've approached the problem in the past: abstract out the platform-dependent bits and hide them behind a portable interface, create separate implementations of that interface for each platform you need to support, and use appropriate makefile magic to build what you need.

Example:

/**
 * MyGenericModule.h
 */
 typedef myAbstractType ...;
 void genericFunction(MyAbstractType param);
 ...

This interface provides platform-neutral types and prototypes that the application code will reference.

 /**
  * Windows implementation
  */
  #include "MyGenericModule.h"
  #include "WindowsSpecificHeader.h"
  ...
  void genericFunction(MyAbstractType param)
  {
    WindowsSpecificType lparam = convertToWindows(param);
    /**
     * Implement Windows-specific logic here
     */
  }

This is the Windows version (WindowsModule.c).

 /**
  * Unix implementation
  */
  #include "MyGenericModule.h"
  #include "UnixSpecificHeader.h"
  ...
  void genericFunction(MyAbstractType param)
  {
    UnixSpecificType lparam = convertToUnix(param);
    /**
     * Implement Unix-specific logic here
     */
  }

And this is the Unix-specific version.

Now it's just a matter of setting up the right rules in your makefiles. Whether you build everything statically or build the platform-specific code into .dlls is driven less by portability and more by what makes sense for the application in question. Most of what I've done was linked statically.

Yes, it's a pain in the ass, but it scales much better than using the preprocessor. I've worked on "portable" code that was a nigh-unreadable rat's nest of #ifdefs . Never again.

The trick is getting the abstract types right, so that they contain everything the underlying implementation needs without overly burdening the programmer with implementation details. Yes, it's hard to do well, and I don't have any good "cookbook" examples to demonstrate.

John Bode