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:
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 BeneficialProblem: 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:
A.5.2 Emulate Missing CapabilitiesProblem: 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 TypesProblem: 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:
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:
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 |