Reputation: 6385
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 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?
Upvotes: 2
Views: 1180
Reputation: 123548
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 #ifdef
s . 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.
Upvotes: 3
Reputation: 1253
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.
Upvotes: 0
Reputation: 3717
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.
Upvotes: 0
Reputation: 327
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;
}
Upvotes: 2
Reputation: 1619
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();
}
Upvotes: 1
Reputation: 84189
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.
Upvotes: 6
Reputation:
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 ;)
Upvotes: 1
Reputation: 43317
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).
Upvotes: 1