Jump to content
  • Advertisement
Sign in to follow this  
AxeGuywithanAxe

CrossPlatform Code

This topic is 764 days old which is more than the 365 day threshold we allow for new replies. Please post a new topic.

If you intended to correct an error in the post then please contact us.

Recommended Posts

Hey guys, I just wanted to get an opinion on the best way to handle cross platform code.

I know that there are three to four major ways i.e;

 

typedef method

#ifdef WIN32
  typedef CWindowsMemory CPlatformMemory
#else 
  typedef CMaxOSMemory   CPlatformMemory
#end 

//cross platform virtual alloc
 void* Memory = CPlatformMemory::VirtualAlloc(...)

free functions with cpp method implemented


//Memory.h"


void* appVirtualAlloc(...)

//Memory.cpp
void* appVirtualAlloc(...)
{
 #ifdef _WIN32
   return _VirtualAlloc(...)
  #else 
   ...
  #endif
} 

or pimpl methods? I personally like the typedef method because it acts like a namespace that is specific to each platform and allows separate header and cpp methods, leaving really clean code.

 

 

 

Share this post


Link to post
Share on other sites
Advertisement
Just food for thought:

If you #ifdef in a header to different implementations, you will likely end up at some point leaking platform-specifics into your headers. Headers, being textual inclusion mechanisms, leak any implementation details (like other headers) to all translation units including the header. Poorly structured code (found in a great many otherwise high-quality products) often looks something like this:

// bad bad bad, don't do this
#if defined(_WIN32)
#  include <windows.h> // includes 24,569,394,347 other definitions you don't need or care about
typedef SomeWindowsThing MyGenericThing;
#else
#  include <sys/something.h> // includes a few dozen other definitions you don't need or care about
typedef SomePosixThing MyGenericThing;
#endif

With Windows in particular, this leads to all kinds of nastiness. The min/max macros are common ones, because there's a great many other macros in windows.h that pollute the macro namespace when included. Not to mention very generic unnamespaced functions and types. Then, because those types are available, programmers end up using them without realizing them. And then the code fails to compile in CI even though it compiled locally, and portability is a pain.

You want to include as absolutely few headers as humanly possible. This largely implies that you need to put more abstractions into regular source files rather than header files. You can't always do this without efficiency problems - and you often can put platform-specific constructs in header using forward declarations - but it remains a good rule of thumb to avoid putting any complexity in a header file that could instead be in a source file.

So far as sprinkling #ifdef's in sources or using separate sources, I'd definitely lean towards separate sources. Example:

// this is foo.h
void foo();
// this is foo_win32.cpp
#include <windows.h>
void foo() {
  some_windows_code();
}
// this is foo_posix.cpp
#include <sys/something.h>
void foo() {
  some_linux_code();
}

And then your build system only compiles the *_win32.cpp files on Windows, the *_posix.cpp files on Linux/BSD/etc., and so on.

Note that this same strategy works anywhere you need conditional compilation besides just platforms. OpenGL vs DirectX? SSE4 vs FPU? Server vs Client? Let the build system handle the differences.

Plus, when possible, that lets you compile multiple variations much more effectively. If you have a single foo.cpp using #ifdef's, it becomes more a pain to compile multiple variants of foo and keep the objects around without _also_ reocmpiling all the other files that have no variants. It can be handy to compile the 95% of your code that has no variations once and just once per platform, compile the tiny bit of the rest of the code that has variants, and then let the linker create different dll/exe outputs for each target variation. You get much faster compilation, better tooling, wider local testing (your developers don't have to remember to build all variants, because they're all built anyway), etc. With a proper cross-compiler, you might even be able to build all your platforms this way; the only thing you'll need to really reocmpile entirely for would be different architectures or compilers that generate incompatible object files (e.g. MSC vs GCC/MingW).

Short version: #ifdef's are best avoided to the extent possible, and this counts doubly so in headers.

Share this post


Link to post
Share on other sites

I use an runtime function dispatcher and a source file for each system.

This has the advantage that I can compile and run code, until I reach a not implemented system function which then assert and tell me that it's not implemented.

If I add many system functions on one platform then I can commit because the code compiles and execute on other platforms.

Later on I implement the functions in the order the executable exit with the specific assert and after that I implement the rest, which I can query by a util function.

The downside of this approach is a lot more code for a system function compared to the other approaches.

 

In the header(System/Environment.hpp) I define the functions and the dispatch routine.

namespace RadonFramework {  namespace System { namespace Environment {

/// This function will be called by RadonFramework_Init function.
void Dispatch();

/** This function will be called by RadonFraemwork_Init function to
* check if the dispatching was successfully.
**/
RF_Type::Bool IsSuccessfullyDispatched();
            
/// This function is for debugging purpose and return all unassigned functions.
void GetNotDispatchedFunctions(Collections::List<RF_Type::String>& Result);

/// Define function pointer.
using MemoryArchitectureOfOSCallback = MemoryArchitecture::Type(*)();
/// Return the memory architecture which the OS is using e.g. 64bit.
extern MemoryArchitectureOfOSCallback MemoryArchitectureOfOS;

The global dispatch routine and the variable of the function pointer are placed in it's own namespace and the variable is defined extern to be sure that it only exists once.

 

In the source file(System/Environment.cpp) I define the variable which contains the function pointer and define the default dispatcher for each system function.

There are also some util functions e.g. GetNotDispatchedFunctions is used in a seperate executable which runs in the CI build for each platform and show me if there are missing implementations.

/// Runtime dispatch function.
MemoryArchitecture::Type MemoryArchitectureOfOS_SystemAPIDispatcher()
{
    MemoryArchitectureOfOS = 0;
    Dispatch();
    Assert(MemoryArchitectureOfOS != MemoryArchitectureOfOS_SystemAPIDispatcher &&
           MemoryArchitectureOfOS != 0,
           "Funtion was called and couldn't be dispatched");
    return MemoryArchitectureOfOS();
}

/// First call to MemoryArchitectureOfOS will dispatch itself.
MemoryArchitectureOfOSCallback RF_SysEnv::MemoryArchitectureOfOS = MemoryArchitectureOfOS_SystemAPIDispatcher;

RF_Type::Bool RF_SysEnv::IsSuccessfullyDispatched()
{
    RF_Type::Bool result = true;
    result = result && MemoryArchitectureOfOS != MemoryArchitectureOfOS_SystemAPIDispatcher && MemoryArchitectureOfOS != 0;
    return result;
}

/// Add all functions which were not dispatched or wasn't implemented on the running system.
void RF_SysEnv::GetNotDispatchedFunctions(List<RF_Type::String>& Result)
{
    if(MemoryArchitectureOfOS == MemoryArchitectureOfOS_SystemAPIDispatcher || MemoryArchitectureOfOS == 0)
    Result.AddLast("MemoryArchitectureOfOS"_rfs);
}

Now I add the implementation for each platform e.g.(System/EnvironmentWindows.cpp)

MemoryArchitecture::Type MemoryArchitectureOfOS()
{
    static const MemoryArchitecture::Type arch = false==Is32BitEmulation()
        && CompilerConfig::CompiledForArchitecture==MemoryArchitecture::_32Bit
        ? MemoryArchitecture::_32Bit : MemoryArchitecture::_64Bit;
    return arch;
}

void RadonFramework::System::Environment::Dispatch()
{
    // Assign the most generic implementation.
    MemoryArchitectureOfOS=::MemoryArchitectureOfOS;
    if(IsWindowsVistaOrGreater())
    {
        // Override with a specialized implementation.
...

In CMake I add the Windows source file on Windows OS, on Linux I add the UNIX and Linux source, OSX UNIX and OSX and so on.

Share this post


Link to post
Share on other sites

I would go for roughly the second option - define a common interface in a header file, but instead of having #ifdefs sprinkled throughout the implementation, consider having separate .cpp files for each platform. So you might have memory.h, memory_win32.cpp, memory_linux.cpp, memory_x360.cpp

 

We use this in large, fast growing projects together with a configuration header and a platform detection header (compile time using various macros to identify compiler set flags) and spread the cpp files over in own directories for each platform. Cpp files are also ifdef-ed to certain platform to keep the responsability on the compiler instead inside the toolchain

Share this post


Link to post
Share on other sites
Sign in to follow this  

  • Advertisement
×

Important Information

By using GameDev.net, you agree to our community Guidelines, Terms of Use, and Privacy Policy.

We are the game development community.

Whether you are an indie, hobbyist, AAA developer, or just trying to learn, GameDev.net is the place for you to learn, share, and connect with the games industry. Learn more About Us or sign up!

Sign me up!