A.5 Hide Platform Differences Whenever Possible

I l @ ve RuBoard

Multiplatform software development presents many challenges to class de-signers because functionality can differ widely across platforms. These differences often take one of the following forms:

  • Missing and impossible ” Some platforms simply don't supply a capability that others do; for example, some platforms do not not have a zombie concept (from Chapter 8) so there is no way and no need to offer a way to avoid them.

  • Missing but can be emulated ” Some platforms don't provide a capability that others do, but they do supply enough other functionality to emulate the missing capability; for example, the condition variable and readers/writer locks described in Chapter 10 are emulated on certain OS platforms.

  • Missing but an equally good feature is available ” Some platforms do not provide a capability, such as the Sockets API, but do provide an equivalent capability, such as TLI. In other situations, both features may be available, but the platform's implementation of one is more favorable than the other. For example, one may have better performance or fewer defects.

This section describes how to deal with these situations to arrive at a set of C++ classes that allow development of easily portable networked applications.

A.5.1 Allow Source to Build Whatever Is Beneficial

Problem: Platform feature sets sometimes diverge in ways that wrapper facades can't hide. For example, a wrapper facade can't magically invent kernel-thread semantics if it simply doesn't exist in the platform's feature set. The class designer now has a problem: Should a class be designed that lacks a method, or set of methods , corresponding to the missing capability?

Software that's ported to enough platforms will eventually encounter one in which an important feature isn't supported. The first reaction to this problem may be to avoid offering a class or a class method, corresponding to that capability, forcing the compiler to detect the problem at compile time. This choice is appropriate when the missing feature is of central importance to the application. For example, the ability to create a new process is a central feature to many networked applications. If a platform doesn't offer multiprocessing, it's often advantageous to know that at compile time. There are may cases, however, in which an application should be allowed to run and react to any potential issues at run time, where alternate approaches can be attempted.

Solution Allow source to build whatever's beneficial. There are situations in which it's better to allow source code to compile correctly, even when the functionality it's trying to access isn't present and can't be emulated. This may be due to:

  • The ability to detect the issue at run time and allow the application to select an alternate plan of action. This case often arises in class library design because designers can't predict all the situations in which the class may be used. In such cases, it may be better to implement a method that returns a "not implemented" indication to the caller. Thus, the caller has the freedom to choose another technique or capability, report the error and try to contine, or terminate. ACE defines a macro called ACE_NOTSUP_RETURN that serves this purpose.

  • Ignoring the missing feature yields the same effect as having it. For this case, consider a UNIX application that uses the ACE_Process_Options::avoid_zombies() method to avoid dealing with zombie processes (see page 168). If the application is ported to Win32, there's no such concept as a zombie process. In this case, it's perfectly acceptable to call the avoid_zombies() method and have it indicate success because the original intent of the method has been accomplished by doing nothing.

A.5.2 Emulate Missing Capabilities

Problem: Due to the wide range of capabilities and standards implemented in today's computing platforms, there's often divergence in feature sets. When porting to a new platform, this divergence yields an area of missing functionality that a project has come to rely on from previous platforms. A careful analysis of the concepts behind the features, however, often reveals alternate capabilities that can be combined to emulate the missing functionality.

Contrary to popular belief, standards are not a panacea for portable software. There are a great many standards, and platforms implement different sets of standards, often changing between OS releases. Even within a standard some features are often optional. For example, the semaphore and real-time scheduling class capabilities are optional parts of Pthreads.

When software has been developed for one platform, or set of platforms, an effort to port it to a new platform often reveals that capabilities used by the software aren't available on the new platform. This can cause a ripple effect throughout the project as changes are made to work around the missing feature. If a careful analysis of the new platform's feature set is made, however, it may reveal a set of features that, when combined, can be used to emulate the missing capability. If a project uses C++ wrapper facade classes intelligently, therefore, it may be able to avoid a series of expensive design and code changes.

Solution Emulate missing capabilities. Classes designed according to the Wrapper Facade pattern can encapsulate a platform's native capabilities in convenient and type-safe ways. They can also contain native code that uses a platform's existing features to emulate some capability not provided otherwise . For example, the following C++ code illustrates how the ACE condition variable and mutex classes can be used to implement a process-scoped semaphore wrapper facade for OS platforms that don't implement it natively.

 class ACE_Thread_Semaphore { private:   ACE_Thread_Mutex mutex_; // Serialize access.   // Wait for <count_> to become non-zero.   ACE_Condition_Thread_Mutex count_nonzero_;   u_long count_; // Keep track of the semaphore count.   u_long waiters_; // Keeps track of the number of waiters. public:   ACE_Thread_Semaphore (u_int count = 1)     : count_nonzero_ (mutex ), // Associate mutex and condition.       count_ (count),       waiters_ (0) {} 

Note how the initializer for count_nonzero_ binds the mutex_ object to itself, in accordance with the principle of associating cohesive objects explicitly, described in Section A.3.4 on page 245.

The acquire() method blocks the calling thread until the semaphore count becomes greater than 0, as shown below.

 int acquire () {   ACE_GUARD_RETURN (ACE_Thread_Mutex, guard, mutex_, -1);   int result = 0;   // Count # of waiters so we can signal them in <release()>.   waiters_++;   // Put calling thread to sleep waiting on semaphore.   while (count_ == 0 && result == 0)     // Release/reacquire <mutex_>     result = count_nonzero_.wait ();   --waiters_;   if (result == 0) --count_; return result; } 

For completeness, we implement the ACE_Thread_Semaphore's release() method below, which increments the semaphore count, potentially unblocking a thread that's waiting on the count_nonzero_ condition variable.

 int release () {     ACE_GUARD_RETURN (ACE_Thread_Mutex, guard, mutex_, -1);     // Notify waiters that the semaphore has been released.     if (waiters_ > 0) count_nonzero_.signal ();     ++count_;     return 0;   }   // ... Other methods omitted ... }; 

Note how the ACE_Thread_Semaphore class's acquire() and release() methods both use the ACE_GUARD_RETURN macro (described in Sidebar 22 on page 216), which encapsulates the Scoped Locking idiom [SSRB00] to ensure the mutex_ is locked and unlocked automatically. This design is yet another example of the principle of simplifying for the common case (described in Section A.3 on page 238).

A.5.3 Handle Variability via Parameterized Types

Problem: Networked applications and middleware often must run on a range of platforms that vary greatly in the availability and efficiency of OS capabilities. For example, certain OS platforms may possess different underlying networking APIs, such as Sockets but not TLI or vice versa. Likewise, different OS platforms may implement these APIs more or less efficiently . When writing reusable software in such heterogeneous platforms, the following forces must be resolved:

  • Different applications may require different configurations of middleware strategies, such as different synchronization or IPC mechanisms. Adding new or improved strategies should be straightforward. Ideally, each application function or class should be limited to a single copy to avoid version skew.

  • The mechanism selected for variation should not unduly affect runtime performance. In particular, inheritance and dynamic binding can incur additional run-time overhead due to the indirection of virtual methods [HLS97].

Solution Handle variability via parameterized types rather than inheritance and dynamic binding. Parameterized types decouple applications from reliance on specific strategies, such as synchronization or IPC APIs, without incurring run-time overhead. Although parameterized types can incur compile- and link-time overhead, they generally compile into efficient code [Bja00].

For example, encapsulating the Socket API with C++ classes (rather than stand-alone C functions) helps improve portability by allowing the wholesale replacement of network programming mechanisms via parameterized types. The following code illustrates this principle by applying generative [CE00] and generic [Ale01] programming techniques to modify the echo_server() so that it's a C++ function template.

 template <class ACCEPTOR> int echo_server (const typename ACCEPTOR::PEER_ADDR &addr) {   // Connection factory.   ACCEPTOR acceptor;   // Data transfer object.   typename ACCEPTOR::PEER_STREAM peer_stream;   // Peer address object.   typename ACCEPTOR::PEER_ADDR peer_addr;   int result = 0;   // Initialize passive mode server and accept new connection.   if (acceptor.open (addr) != -1       && acceptor.accept (peer_stream, &peer_addr) != -1) {     char buf[BUFSIZ];     for (size_t n; (n = peer_stream.recv (buf, sizeof buf)) > 0;)       if (peer_stream.send_n (buf, n) != n) {         result = -1;         break;       }     peer_stream.close ();   }   return result; } 

By using ACE and C++ templates, applications can be written to be parameterized transparently with either C++ Socket or TLI wrapper facades, depending on the properties of the underlying OS platform:

 // Conditionally select IPC mechanism. #if defined (USE_SOCKETS) typedef ACE_SOCK_Acceptor ACCEPTOR; #elif defined (USE_TLI) typedef ACE_TLI_Acceptor ACCEPTOR; #endif /* USE_SOCKETS. */ int driver_function (u_short port_num)  {   // ...   // Invoke the <echo_server()> with appropriate network   // programming APIs. Note use of template traits for <addr>.   typename ACCEPTOR::PEER_ADDR addr (port_num);   echo_server<ACCEPTOR> (addr); } 

This technique works for the following reasons:

  • The ACE C++ Socket and TLI wrapper facade classes expose an object-oriented interface with a common signature. In cases where interfaces aren't originally designed to be consistent, the Adapter pattern [GHJV95] can be applied to make them consistent, as described in Section A.3.3.

  • C++ templates support signature-based type conformance that does not require type parameters to encompass all potential functionality. Instead, templates parameterize application code that is designed to invoke only a subset of methods that are common to the various network programming methods, such as open() , close() , send() , and recv() .

In general, parameterized types are less intrusive and more extensible than alternatives, such as implementing multiple versions of the echo_server() function or littering conditional compilation directives throughout application source code.

I l @ ve RuBoard


C++ Network Programming
C++ Network Programming, Volume I: Mastering Complexity with ACE and Patterns
ISBN: 0201604647
EAN: 2147483647
Year: 2001
Pages: 101

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net