Dr. Dobb's is part of the Informa Tech Division of Informa PLC

This site is operated by a business or businesses owned by Informa PLC and all copyright resides with them. Informa PLC's registered office is 5 Howick Place, London SW1P 1WG. Registered in England and Wales. Number 8860726.


Channels ▼
RSS

C/C++

Cross-Platform Design Strategies


Jun99: Cross-Platform Design Strategies

Bob is president of NeoLogic Systems, and architect of the NeoAccess cross-platform object database engine. Bob can be reached at [email protected].


Cross-platform development is not a niche specialty. All programmers at some point have to ensure that their code compiles cleanly and runs efficiently in multiple environments, even if those environments are just different versions of the same compiler.

At NeoLogic, we've accumulated a lot of experience with this problem. NeoAccess, NeoLogic's cross-platform object database, ships with source code, which means it must compile and run on a large variety of platforms. In this article, I'll survey the key aspects of our cross-platform architecture that you can use to ensure that your feature-rich and extensible code can be readily utilized on multiple platforms. I'll also demonstrate our approach by sharing a set of thread classes we've developed for use on both Macintosh and Windows PCs.

Lowest Common Denominator

Many developers adopt a lowest common denominator approach, only using facilities that are uniformly available on all platforms. Any libraries or components you want to buy have to be available for all platforms in question, which greatly limits the options. Your application can end up looking only as good as the worst platform supported by the components or libraries chosen. This approach is unacceptable to all but the most captive markets -- the ability to deliver an application on multiple platforms should not result in a limited set of features.

Conditional Code

Another common approach is to start by writing an application for one platform, then port the application to one platform at a time, using conditional code to indicate differences between platforms. The problem with this approach is that, without a cross-platform architecture in the initial implementation, the amount of code that can be shared across platforms is limited and the maintainability of the source tree suffers with each successive port.

Design Strategies

In most cases, every platform you'd like to support does have the features you want, but those features are implemented in a totally different way. The challenge is to isolate those platform-specific features and communicate with them through an abstraction layer that will work for all platforms. This is accomplished by letting the visible interface of a platform-specific class define how client code accesses a function without regard for how the function is implemented. Encapsulation is preserved and visible complexity is reduced.

It is interesting to note that of the over 200 classes and templates in NeoAccess, fewer than 10 are platform specific. Many other well designed applications can expect to meet a standard metric: 95 percent platform independent, 5 percent platform specific.

The cross-platform design pattern isolates and encapsulates the implementation of platform-specific functions behind a platform-neutral interface. Only a small portion has to be rewritten for each platform. Typically, these classes are very straightforward -- they provide specific functionality on a given platform. In many ways, this implementation is the easy, even boring, part of coding.

The hard part is designing an interface that presents an appropriate environment-neutral set of services to client code. The interface to these services should be sufficiently high level to maximize the return from the application developer's effort and minimize the effort involved in moving application code to additional platforms. Ultimately, the collection of platform-specific classes of the cross-platform pattern will support all the features any platform-independent applications require. When you reach that point, all the code you write for applications will be platform independent, residing on top of this collection of classes.

Our object database engine was designed to be cross platform from the ground up. This was accomplished by using the cross-platform design pattern. I'll explain this pattern by presenting an exemplar set of thread classes that provide multithreading support on both Windows and MacOS.

Cross-Platform Thread Classes

Multithreading services provided by the operating system often differ from platform to platform, as does the programming interface to those services. In Windows, threads are preemptive -- another thread can preempt a running thread from execution, taking control of the processor on demand. In the Macintosh, threads are often cooperative, only being preempted by explicitly yielding execution.

To make the thread behavior consistent, all thread implementations in NeoAccess Release 6.0 are cooperative threads -- each thread yielding control explicitly. While this was trivial to implement on the Macintosh, it was somewhat more complex on the preemptive thread-based Windows. On Windows, a mutual exclusion semaphore was used. In this environment, all NeoAccess threads attempt to obtain this semaphore before proceeding. However, if another thread holds the semaphore, then other threads will block while waiting for the holding thread to yield. If the thread that holds the semaphore is preempted by the operating system, only nonNeoAccess user-interface threads will run. The code that implements these constructs is part of the implementation of the platform-specific thread class. As a result, client code is unaware of these details.

Interface

The CNeoThread class provides an environment-neutral interface to multithreading services. This base class is further subclassed to provide platform-specific implementations of the abstract interface and to deal with additional services not available on all platforms. Following the interface of the abstract base class and the services provided by the underlying operating system, platform-specific subclasses can be easily written.

Listing One shows how the platform-specific objects are hidden using typedefs. CNeoThreadBase is the typedef used to refer to the underlying base class of the CNeoThread abstract base class. When NeoAccess is built for use with MFC, CNeoThreadBase is defined to be CWin-Thread. On the Macintosh using the PowerPlant application framework, CNeo-ThreadBase is defined to be the base class of all PowerPlant threads, LThread. Either typedefs or #defines can be used to create such a class name mapping.

The subclass providing an environment-specific implementation to the CNeoThread implementation differs depending on the target run-time environment. Under Windows, this class is CNeoThreadMFC. On the Macintosh it is CNeoThreadPP. The implementation of these subclasses provides the environment-specific support of each run-time environment. The symbol CNeoThreadNative is the platform-independent name used to refer to the appropriate platform-specific subclass. Figure 1 shows how the classes fit together.

Listing Two is the platform-independent CNeoThread class used in all environments. The interface of CNeoThread is identical on all platforms; it's the abstract interface that the client code is written to. The actual code defining the CNeoThread class has been abbreviated for simplicity. The CNeoThreadBase class is used to define the base class of CNeoThread. As in Listing One, the definition of CNeoThreadBase differs depending on the target platform.

It's worth noting that the interface to CNeoThread is sufficient, but not exhaustive. The interface is designed to judiciously include all the functions a multithreaded application requires. Adding more functions than necessary risks increasing the effort necessary to support other run-time environments in the future.

Also, the features of this interface are not limited to those that are available on all supported platforms. For example, the block function includes an argument that can be used to specify how long the thread is willing to block waiting before it times out. The possible values are kNeoNever, kNeoForever, or some other value indicating the number of milliseconds the thread is willing to wait. Yet not every platform supports the ability to timeout while waiting for a resource. The description of this function stipulates that all platforms support kNeoNever and kNeoForever, and that those platforms that don't support a specific amount of time assume kNeoForever if any value other than kNeoNever is given.

Finally, note the aTime argument has a default value so that the client is free to ignore the timeout feature completely. This further minimizes visible complexity.

Platform-Specific Implementations

The CNeoThreadBase and CNeoThreadNative typedefs defined in Listing One refer to platform-specific thread classes. CNeo-ThreadBase refers to the base class of the platform-independent CNeoThread class. CNeoThreadNative defines the platform-specific thread class that implements the CNeoThread interface. These types provide platform-neutral class names that can be used in all environments.

For Windows, the symbol CNeoThreadNative refers to CNeoThreadMFC in Listing Three. This is a platform-specific class, designed for use with Windows and MFC. Note the declaration of the gNeoCritical critical section semaphore that precedes the definition of CNeoThreadMFC. Also note references to this semaphore in the implementations of some of the inline functions of CNeoThreadMFC.

When you look at the code in Listings Three and Four, you'll notice that many of the static function prototypes in both CNeoThreadMFC and CNeoThreadPP are identical. All platform-specific subclasses include the same set of static functions with identical calling conventions and can always be referred to using the CNeo-ThreadNative typedef. This results in a construct which is sometimes called "static virtual functions." This idiom is an extension of the idea of an abstract base class that provides a generic interface to which client code can be written. The CNeoThreadNative symbol extends the interface into the subclass by using the member functions, both static and otherwise, with a common interface across all platforms. While the prototypes of these functions are identical, their platform-specific implementations may differ.

All of the platform-specific code has been isolated in the conditional declaration, so that the software utilizing the objects (as well as the balance of the abstraction layer) need not know exactly what code is executing, or on what platform. The conditional declaration in Listing One took care of that, mapping the platform-specific classes to the standard CNeoThreadBase and CNeoThreadNative symbols. If additional implementations are to be added, new platform-specific classes are created. The conditional declaration is then expanded to test for the new platforms, and select the appropriate platform-specific classes.

It is important to note that, once the platform-specific classes are written, the multithreading specifics of each platform can be virtually forgotten. The developer of the class need only keep the concepts of how Macintosh or Windows threading works in his head for the duration of the development of the platform-specific thread class.

Using these Classes

Listing Five shows how simple life is now, with the thread objects completely abstracted away from the platform. Regardless of what platform is used, the code acts the same way -- only the underlying classes change, and that process is handled automatically at compile time.

Programs invariably change. In the typical business application, a program is designed to solve a particular business problem. As time goes by, the nature of the problem changes, and the business must change as well. Consequently, the business application must adapt to fit the new problem. For a long time now, we've seen the advantages of object-oriented programming for supporting the process of evolving applications to suit new business needs. Nowhere is this technique more useful than in a cross-platform implementation.

With the separation of platform-specific code from the application code, developers can more readily evolve an application as business requirements change. They can ignore platform code and focus only on code pertaining to the business. Revisions to the application need to occur only once. The code is then compiled for each platform. In this process, platform-specific code is unaffected.

Should a new platform be introduced for the application, the plan for implementing the application on the new platform is instantly and abundantly clear -- each of the platform-specific classes must be written for the new platform. The specifications of these objects are already clearly delineated in the existing application -- you just have to sit down and write it. These platform-specific classes will represent a relatively small portion of the application's source tree -- the bulk of it will instantly convert to the new platform, since there is nothing in the application objects that is unique to any platform.

Ultimately, the goal of effective cross-platform development is to write as much platform-independent code as possible. Platform-specific classes isolate the variances in behavior between the platforms, while still providing robust features to the platform-independent software. This is a design philosophy, not a language feature. You have to design your applications from the beginning to be platform independent.

DDJ

Listing One

#if defined(WINDOWS)
  typedef CWinThread CNeoThreadBase; // MFC's thread class
  // The base class of all application threads is Neo's MFC thread class
  typedef CNeoThreadMFC CNeoThreadNative;
#elif defined(macintosh)
  typedef LThread CNeoThreadBase; // PowerPlant's thread class
  // The base class of all application threads is Neo's PP thread class
  typedef CNeoThreadPP CNeoThreadNative;
#endif

Back to Article

Listing Two

class CNeoThread : public CNeoThreadBase {
public:
  CNeoThread(void **aArg,
  const NeoThreadOptions aOptions,
  const NeoPriority aPriority);
  virtual ~CNeoThread(void);

  virtual void block(CNeoSemaphoreNative *aSemaphore, const long aParam,
                     const NeoTime aTime = kNeoForever) = 0;
  virtual NeoThreadState getState(void) const = 0;
  virtual long run(void);
  virtual void setState(const NeoThreadState aState,
                        const NeoThreadID aNext = kNeoNoThread) = 0;
  void suspend(void) = 0;
  virtual void unblock(CNeoSemaphoreNative *aSemaphore) = 0;
  virtual void yield(CNeoThread *aTo = nil) = 0;
protected:
  NeoThreadOptions fOptions;
};

Back to Article

Listing Three

extern CRITICAL_SECTION gNeoCritical;
class CNeoThreadMFC : public CNeoThread {
public:
  CNeoThreadMFC(NeoThreadOptions aOptions,
                const NeoPriority aPriority,
                NeoUserThreadFunc aUserFunc);
  virtual OSErr block(CNeoSemaphoreNative *aSemaphore,
                      const long aParam, NeoTime aTime = kNeoForever);
  virtual NeoThreadState getState(void) const {return fState;}
  void resume(void) {
        fState = kNeoThreadReadyState;
        ResumeThread();
  }
  virtual void setState(const NeoThreadState aState,
                        const NeoThreadID aNext = kNeoNoThread);
  virtual void sleep(unsigned long aTime);
  void suspend(void);
  virtual OSErr unblock(CNeoSemaphoreNative *aSemaphore);
  virtual void yield(CNeoThread *aTo = nil);

  static void BeginCriticalSection(void) {}
  static void EndCriticalSection(void) {}
  static void InitThreads(void) {
        ::InitializeCriticalSection(&gNeoCritical);
}
  virtual void Sleep(unsigned long aTime) {sleep(aTime);}
  static void YieldTo(CNeoThread *aTo = nil) {
                GetCurrent()->yield(aTo);
  }
protected:
  NeoThreadState fState;
};

Back to Article

Listing Four

class CNeoThreadPP : public CNeoThread {
public:
  /** Instance Member Functions **/
  CNeoThreadPP(void **aArg, const NeoThreadOptions aOptions,
                const NeoPriority aPriority);
  /** Access Member Functions **/

  virtual OSErr block(CNeoSemaphoreNative *aSemaphore, const long aParam,
                      const NeoTime aTime = kNeoForever);
  virtual NeoThreadState getState(void) const;
  virtual void setState(const NeoThreadState aState,
                        NeoThreadID aNext = kNoThreadID);
  void suspend(void) {Suspend();}
  virtual OSErr unblock(CNeoSemaphoreNative *aSemaphore);
  virtual void yield(CNeoThread *aTo = nil) {
          CNeoThread::Yield(aTo);
  }
  /** Static Member Functions **/
  static void BeginCriticalSection(void) {EnterCritical();}
  static void EndCriticalSection(void) {ExitCritical();}
  static CNeoThreadPP * GetCurrent(void) {
        return (CNeoThreadPP *)GetCurrentThread();
  }
  static void InitThreads(void);
  static void YieldTo(CNeoThread *aTo = nil) {
        CNeoThread::Yield(aTo);
  }
protected:
  /** Macintosh-Specific Member Functions **/
  static void * GetTaskRef(void) {return sThreadTaskRef;}
  static void IOComplete(ParmBlkPtr pbPtr);
  static void AsyncIOResume(CNeoThreadPP *aThread);
  void setIOCompleteProc(NeoThreadBlock *aBlock,
                                         NeoCompletionProc aProc = nil);
  OSErr waitUntilIOCompletes(NeoThreadBlock *aThreadBlock,
  OSErr & volatile aError);
  virtual void setEpilogue(NeoThreadEpilogue aEpilogue, void *aParam);
};

Back to Article

Listing Five

class CMyThread : public CNeoThreadNative {
public:
  CMyThread(void **aArg = nil,
            const NeoThreadOptions aOptions = kCreateIfNeeded,
            const NeoPriority aPriority = kNeoPriorityNormal);
  virtual ~CMyThread(void);
  /** Access Member Functions **/
  virtual long run(void);
};

Back to Article


Copyright © 1999, Dr. Dobb's Journal

Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.