- •Advanced CORBA® Programming with C++
- •Review
- •Dedication
- •Preface
- •Prerequisites
- •Scope of this Book
- •Acknowledgments
- •Chapter 1. Introduction
- •1.1 Introduction
- •1.2 Organization of the Book
- •1.3 CORBA Version
- •1.4 Typographical Conventions
- •1.5 Source Code Examples
- •1.6 Vendor Dependencies
- •1.7 Contacting the Authors
- •Part I: Introduction to CORBA
- •Chapter 2. An Overview of CORBA
- •2.1 Introduction
- •2.2 The Object Management Group
- •2.3 Concepts and Terminology
- •2.4 CORBA Features
- •2.5 Request Invocation
- •2.6 General CORBA Application Development
- •2.7 Summary
- •Chapter 3. A Minimal CORBA Application
- •3.1 Chapter Overview
- •3.2 Writing and Compiling an IDL Definition
- •3.3 Writing and Compiling a Server
- •3.4 Writing and Compiling a Client
- •3.5 Running Client and Server
- •3.6 Summary
- •Part II: Core CORBA
- •Chapter 4. The OMG Interface Definition Language
- •4.1 Chapter Overview
- •4.2 Introduction
- •4.3 Compilation
- •4.4 Source Files
- •4.5 Lexical Rules
- •4.6 Basic IDL Types
- •4.7 User-Defined Types
- •4.8 Interfaces and Operations
- •4.9 User Exceptions
- •4.10 System Exceptions
- •4.11 System Exceptions or User Exceptions?
- •4.12 Oneway Operations
- •4.13 Contexts
- •4.14 Attributes
- •4.15 Modules
- •4.16 Forward Declarations
- •4.17 Inheritance
- •4.18 Names and Scoping
- •4.19 Repository Identifiers and pragma Directives
- •4.20 Standard Include Files
- •4.21 Recent IDL Extensions
- •4.22 Summary
- •Chapter 5. IDL for a Climate Control System
- •5.1 Chapter Overview
- •5.2 The Climate Control System
- •5.3 IDL for the Climate Control System
- •5.4 The Complete Specification
- •Chapter 6. Basic IDL-to-C++ Mapping
- •6.1 Chapter Overview
- •6.2 Introduction
- •6.3 Mapping for Identifiers
- •6.4 Mapping for Modules
- •6.5 The CORBA Module
- •6.6 Mapping for Basic Types
- •6.7 Mapping for Constants
- •6.8 Mapping for Enumerated Types
- •6.9 Variable-Length Types and _var Types
- •6.10 The String_var Wrapper Class
- •6.11 Mapping for Wide Strings
- •6.12 Mapping for Fixed-Point Types
- •6.13 Mapping for Structures
- •6.14 Mapping for Sequences
- •6.15 Mapping for Arrays
- •6.16 Mapping for Unions
- •6.17 Mapping for Recursive Structures and Unions
- •6.18 Mapping for Type Definitions
- •6.19 User-Defined Types and _var Classes
- •6.20 Summary
- •Chapter 7. Client-Side C++ Mapping
- •7.1 Chapter Overview
- •7.2 Introduction
- •7.3 Mapping for Interfaces
- •7.4 Object Reference Types
- •7.5 Life Cycle of Object References
- •7.6 Semantics of _ptr References
- •7.7 Pseudo-Objects
- •7.8 ORB Initialization
- •7.9 Initial References
- •7.10 Stringified References
- •7.11 The Object Pseudo-Interface
- •7.12 _var References
- •7.13 Mapping for Operations and Attributes
- •7.14 Parameter Passing Rules
- •7.15 Mapping for Exceptions
- •7.16 Mapping for Contexts
- •7.17 Summary
- •Chapter 8. Developing a Client for the Climate Control System
- •8.1 Chapter Overview
- •8.2 Introduction
- •8.3 Overall Client Structure
- •8.4 Included Files
- •8.5 Helper Functions
- •8.6 The main Program
- •8.7 The Complete Client Code
- •8.8 Summary
- •Chapter 9. Server-Side C++ Mapping
- •9.1 Chapter Overview
- •9.2 Introduction
- •9.3 Mapping for Interfaces
- •9.4 Servant Classes
- •9.5 Object Incarnation
- •9.6 Server main
- •9.7 Parameter Passing Rules
- •9.8 Raising Exceptions
- •9.9 Tie Classes
- •9.10 Summary
- •Chapter 10. Developing a Server for the Climate Control System
- •10.1 Chapter Overview
- •10.2 Introduction
- •10.3 The Instrument Control Protocol API
- •10.4 Designing the Thermometer Servant Class
- •10.5 Implementing the Thermometer Servant Class
- •10.6 Designing the Thermostat Servant Class
- •10.7 Implementing the Thermostat Servant Class
- •10.8 Designing the Controller Servant Class
- •10.9 Implementing the Controller Servant Class
- •10.10 Implementing the Server main Function
- •10.11 The Complete Server Code
- •10.12 Summary
- •Chapter 11. The Portable Object Adapter
- •11.1 Chapter Overview
- •11.2 Introduction
- •11.3 POA Fundamentals
- •11.4 POA Policies
- •11.5 POA Creation
- •11.6 Servant IDL Type
- •11.7 Object Creation and Activation
- •11.8 Reference, ObjectId, and Servant
- •11.9 Object Deactivation
- •11.10 Request Flow Control
- •11.11 ORB Event Handling
- •11.12 POA Activation
- •11.13 POA Destruction
- •11.14 Applying POA Policies
- •11.15 Summary
- •Chapter 12. Object Life Cycle
- •12.1 Chapter Overview
- •12.2 Introduction
- •12.3 Object Factories
- •12.4 Destroying, Copying, and Moving Objects
- •12.5 A Critique of the Life Cycle Service
- •12.6 The Evictor Pattern
- •12.7 Garbage Collection of Servants
- •12.8 Garbage Collection of CORBA Objects
- •12.9 Summary
- •Part III: CORBA Mechanisms
- •Chapter 13. GIOP, IIOP, and IORs
- •13.1 Chapter Overview
- •13.2 An Overview of GIOP
- •13.3 Common Data Representation
- •13.4 GIOP Message Formats
- •13.5 GIOP Connection Management
- •13.6 Detecting Disorderly Shutdown
- •13.7 An Overview of IIOP
- •13.8 Structure of an IOR
- •13.9 Bidirectional IIOP
- •13.10 Summary
- •14.1 Chapter Overview
- •14.2 Binding Modes
- •14.3 Direct Binding
- •14.4 Indirect Binding via an Implementation Repository
- •14.5 Migration, Reliability, Performance, and Scalability
- •14.6 Activation Modes
- •14.7 Race Conditions
- •14.8 Security Considerations
- •14.9 Summary
- •Part VI: Dynamic CORBA
- •Chapter 15 C++ Mapping for Type any
- •15.1 Chapter Overview
- •15.2 Introduction
- •15.3 Type any C++ Mapping
- •15.4 Pitfalls in Type Definitions
- •15.5 Summary
- •Chapter 16. Type Codes
- •16.1 Chapter Overview
- •16.2 Introduction
- •16.3 The TypeCode Pseudo-Object
- •16.4 C++ Mapping for the TypeCode Pseudo-Object
- •16.5 Type Code Comparisons
- •16.6 Type Code Constants
- •16.7 Type Code Comparison for Type any
- •16.8 Creating Type Codes Dynamically
- •16.9 Summary
- •Chapter 17. Type DynAny
- •17.1 Chapter Overview
- •17.2 Introduction
- •17.3 The DynAny Interface
- •17.4 C++ Mapping for DynAny
- •17.5 Using DynAny for Generic Display
- •17.6 Obtaining Type Information
- •17.7 Summary
- •Part V: CORBAservices
- •Chapter 18. The OMG Naming Service
- •18.1 Chapter Overview
- •18.2 Introduction
- •18.3 Basic Concepts
- •18.4 Structure of the Naming Service IDL
- •18.5 Semantics of Names
- •18.6 Naming Context IDL
- •18.7 Iterators
- •18.8 Pitfalls in the Naming Service
- •18.9 The Names Library
- •18.10 Naming Service Tools
- •18.11 What to Advertise
- •18.12 When to Advertise
- •18.13 Federated Naming
- •18.14 Adding Naming to the Climate Control System
- •18.15 Summary
- •Chapter 19. The OMG Trading Service
- •19.1 Chapter Overview
- •19.2 Introduction
- •19.3 Trading Concepts and Terminology
- •19.4 IDL Overview
- •19.5 The Service Type Repository
- •19.6 The Trader Interfaces
- •19.7 Exporting Service Offers
- •19.8 Withdrawing Service Offers
- •19.9 Modifying Service Offers
- •19.10 The Trader Constraint Language
- •19.11 Importing Service Offers
- •19.12 Bulk Withdrawal
- •19.13 The Admin Interface
- •19.14 Inspecting Service Offers
- •19.15 Exporting Dynamic Properties
- •19.16 Trader Federation
- •19.17 Trader Tools
- •19.18 Architectural Considerations
- •19.19 What to Advertise
- •19.20 Avoiding Duplicate Service Offers
- •19.21 Adding Trading to the Climate Control System
- •19.22 Summary
- •Chapter 20. The OMG Event Service
- •20.1 Chapter Overview
- •20.2 Introduction
- •20.3 Distributed Callbacks
- •20.4 Event Service Basics
- •20.5 Event Service Interfaces
- •20.6 Implementing Consumers and Suppliers
- •20.7 Choosing an Event Model
- •20.8 Event Service Limitations
- •20.9 Summary
- •Part VI: Power CORBA
- •Chapter 21. Multithreaded Applications
- •21.1 Chapter Overview
- •21.2 Introduction
- •21.3 Motivation for Multithreaded Programs
- •21.4 Fundamentals of Multithreaded Servers
- •21.5 Multithreading Strategies
- •21.6 Implementing a Multithreaded Server
- •21.7 Servant Activators and the Evictor Pattern
- •21.8 Summary
- •22.1 Chapter Overview
- •22.2 Introduction
- •22.3 Reducing Messaging Overhead
- •22.4 Optimizing Server Implementations
- •22.5 Federating Services
- •22.6 Improving Physical Design
- •22.7 Summary
- •Appendix A. Source Code for the ICP Simulator
- •Appendix B. CORBA Resources
- •Bibliography
IT-SC book: Advanced CORBA® Programming with C++
After a client invokes an operation, the client loses its thread of control until the operation completes. CORBA does not provide standard APIs to intercept call dispatch on the client side. This makes it impossible to implement client-side caching transparently. Either we must create C++ wrapper classes for object references and implement the caching functionality in these wrapper classes, or we must modify the IDL for an object so that it presents itself as data instead of as an interface. Neither approach is particularly elegant.[2]
[2] Note that some ORBs offer proprietary extensions that allow you to replace the normal client-side proxy with a class of your own, known as a smart proxy. The smart proxy adds client-side caching transparently to the main application logic. However, smart proxies are not portable.
Client-side caching suffers from cache coherency problems. Cache coherency is lost if each of several clients caches the state for the same object and one or more clients invoke an update operation on the object. Even though each client writes its update straight through to the object by sending a remote message, other clients do not know this has happened and now hold an out-of-date copy.
To realize the performance benefits associated with client-side caching, you may be prepared either to dilute your object model or to use proprietary interfaces. However, we urge you not to underestimate the potential problems caused by loss of cache coherency. You will find quite a bit of CORBA literature that suggests solving the cache coherency problem by making a callback from the server to each client that holds a local copy of an updated object. The callbacks inform the clients that they are holding an out-of-date copy and possibly refresh the state of that copy.
However, the callback approach for cache coherency suffers from all the problems presented in Section 20.3 and is very difficult to scale. In addition, it is extremely difficult to maintain cache coherency for multiple clients without race conditions. Naive approaches lose as much performance in maintaining coherency as they gain by having client-side caching in the first place; typically, implementing more sophisticated approaches is too expensive as part of normal application development.
If you are considering the use of client-side caching, we suggest that you limit caching to situations in which clients have a natural one-to-one relationship with the objects whose state they cache. Provided each object is cached only by exactly one client, you avoid all cache coherency problems. If you want to apply client-side caching to objects that are shared by a number of clients, we recommend that you consider using the OMG Concurrency Control Service or Transaction Service (see [21] for details on both services).
22.4 Optimizing Server Implementations
Because CORBA is server-centric, most opportunities to improve performance and scalability present themselves on the server side. Because we have already seen the mechanisms involved, we briefly summarize the design techniques here. Note that these techniques are not mutually exclusive. Because you can create multiple POAs with
862
IT-SC book: Advanced CORBA® Programming with C++
different policies in the same server, you can apply more than one technique for different objects or even dynamically choose a technique at run time based on the access patterns of clients.
22.4.1 Threaded Servers
Threading should undoubtedly be at the top of your list when it comes to performance improvements. If a server is single-threaded, all calls from clients are serialized at the server end. If individual operations do a significant amount of work and run for more than a millisecond or so, single-threaded servers severely limit throughput. Note that threaded servers often perform better than non-threaded servers even on single-CPU machines because multithreaded servers can take advantage of I/O interleaving.
Keep in mind that you must plan for threading when you first design your server. It is highly unlikely that you will be able to add threading to a server that was designed as single-threaded program. Often, attempts to back-patch threading end up being more expensive than a complete reimplementation.
22.4.2 Separate Servant per Object
Typically, creating a permanent and separate servant for each CORBA object provides the best overall performance. Because each servant is permanently in memory, the ORB run time can dispatch calls directly without having to rely on a servant activator or locator to bring the servant into memory. The separate servant per object approach is most suitable for servers that can afford to hold all objects they provide in memory simultaneously.
22.4.3 Servant Locators and Activators
You can use servant locators or servant activators to activate servant instances on demand. Even if you have sufficient memory to hold all servants in memory simultaneously, servant activation can still be useful. If objects are expensive to initialize—for example, because initialization requires accessing a slow network—it may take too long to instantiate all servants during server start-up before an event loop is started. In this case, servant activation permits you to distribute the initialization cost over time instead of incurring it all at once during start-up. In addition, objects that are never used by clients are never initialized, whereas initialization during server start-up incurs the cost whether or not objects are used.
22.4.4 Evictor Pattern
The Evictor pattern (see Section 12.6) is most suitable for servers that need to scale to large numbers of objects but cannot hold a servant for all objects in memory simultaneously. In other words, the Evictor pattern sacrifices some performance in order to limit memory consumption. For many servers, the Evictor pattern provides excellent service, assuming that the server can hold at least the working set of objects in memory.
863
IT-SC book: Advanced CORBA® Programming with C++
22.4.5 Default Servants
Default servants allow you to implement an unlimited number of CORBA objects with a single C++ object instance. The main motivation for default servants is to increase scalability. Default servants allow you to exercise tight control over memory consumption at the price of losing some performance because default servants incur the cost of mapping object IDs to object state repeatedly for each operation. However, using default servants, the number of objects a server can support is effectively unlimited. Often, default servants are used as front-end objects for large database lookups; in that case, the number of objects a server can implement is limited only by the capacity of secondary storage.
22.4.6 Manufactured Object References
The create_reference_with_id operation on the POA interface decouples the life cycle of an object reference from the life cycle of its servant. This behavior is particularly useful if you must efficiently deliver object references as operation results. For example, the implementation of the list operation in the CCS controller benefits substantially from the ability to manufacture an object reference without having to instantiate a servant first. Note, however, that manufactured object references require you to also provide on-demand servant activation.
22.4.7 Server-Side Caching
The Evictor pattern provides an effective caching mechanism for object state if you have a distinct servant for each object. However, you can apply server-side caching at multiple levels. For example, a server that provides access to a database can choose to cache parts of the database in memory. You can combine such low-level caching with object-level caching to create a primary and a secondary cache. Such designs can result in excellent performance gains when properly matched to the access patterns of clients. In addition, server-side caching avoids the cache coherency problems of client-side caching (assuming that the server can guarantee coherency of its database cache).
22.5 Federating Services
Sooner or later, all the techniques just discussed fail. When client demand permanently outstrips server performance, no amount of clever caching can magically create the required performance. Typically, this situation arises in very large systems in which there are simply too many clients and objects for a single server to handle. In these situations, you have no choice except to distribute the processing load over a number of federated servers.
The OMG Naming, Trading, and Event Services are all examples of designs that federate naturally and easily. This is no accident—when you look at these services closely, federating them works for the following reasons.
864
IT-SC book: Advanced CORBA® Programming with C++
Each interface deals with a well-defined and orthogonal piece of functionality.
The servers in the federation are either ignorant of the fact that they are federated, or, alternatively, each server has knowledge only of its immediate neighboring servers and not of the federation as a whole.
Clearly, the first point is not particular to federated services; rather, it is a sign of welldefined interfaces in general. However, the second point is extremely important. Any attempt to federate more than four or five servers is likely to fail if the servers share global state in some form. Global state is the enemy of scalability [40]. For example, a federated design that requires every server in the federation to know about certain state changes cannot scale because the probability of at least one server being non-functional at any given time asymptotically approaches one as the number of servers increases [5].
If you decide on a federated design, make sure to strictly localize knowledge of the federation, and do not make any assumptions that rely on global state. You can use the Naming, Trading, and Event Services as a source of inspiration for your design. In addition, you should consider using the Trading Service if you want to provide a homogeneous view of the federated service to clients.
22.6 Improving Physical Design
Physical design refers to the way you distribute the functional components of an application over source files. In many ways, correct physical design of a system is just as important as the choice of the correct object model. If you correctly partition functionality over source files, maintainability and reusability of your code base will be greatly enhanced. Good physical design pays off as the system evolves over time because it reduces both complexity and the likelihood of errors being introduced as changes are made (see [11] for an excellent treatment of these topics).
In a CORBA context, it is useful to limit the extent to which CORBA-related functionality is visible throughout the system. This involves keeping the bulk of the source code free from CORBA artifacts and isolating all of the CORBA-related code in a few source files. Such an overall physical design is shown in Figure 22.1.
Figure 22.1 Separation of CORBA code from the business logic.
865
IT-SC book: Advanced CORBA® Programming with C++
This design separates the application code into two major sections. The core of the application, which contains the business logic (and typically most of the development investment), resides in a separate set of source files. None of these source files includes any ORB-related or IDL-generated header files. Instead, the core source files implement the bulk of the application using normal C++ classes and data types.
The core logic part of the code offers C++ interfaces to a delegation layer. The purpose of the delegation layer is to receive CORBA invocations from clients on one side and on the other side to delegate these invocations to the core logic in form of ordinary C++ method calls that use only C++ data types.
With this design, we achieve a clean separation of concerns. One part of the application—the delegation layer in a separate set of source files—deals with enabling the application for remote access via CORBA. The other (typically far larger) part of the application—the core logic in its own set of source files— implements the application semantics. Because the core source files do not include any CORBA header files, they are ignorant of the presence of CORBA in the system.
Using such a design offers quite a few advantages.
The design makes sense architecturally because it cleanly separates code related to remote communication from the main application body.
The header files generated by the IDL compiler for the C++ mapping can be large. Restricting the use of these headers to a small number of source files can yield dramatic reduction of compile and link times, with a corresponding reduction in development costs. Most of the source code is not concerned with the details of the C++ mapping. For large projects, the main advantage is that not every developer need be proficient in using the C++ mapping. Instead, the developers working on the core logic can use any established framework or class library they prefer.
866
IT-SC book: Advanced CORBA® Programming with C++
The core logic of the application can be tested separately from the CORBA-related functionality. You can use existing testing tools and debug the core logic without getting distracted by CORBA-related problems.
If your ORB contains a bug in its C++ mapping or in the way application code interacts with the skeleton, you can implement a work-around by touching only the delegation layer. Without such a layer, any work-around would likely affect a large number of source files and be much more costly to implement.
CORBA-related portability problems are isolated in the delegation layer. This is important if your code must work with ORBs from several vendors. Although the POA addresses most of the server-side portability issues, many applications are still written using the deprecated BOA. In addition, it is likely that the BOA legacy will be with us for some time to come. A delegation layer permits you to easily port the code between different implementations of the BOA and the POA while disturbing only a small part of the code base.
The delegation layer is a simple piece of code that contains almost no intellectual investment and can easily be written in a few days for even quite large interfaces. This means that you can afford to throw the delegation layer away if you move to a different ORB (or even an infrastructure other than CORBA) instead of trying to endlessly port the core logic. This is particularly important for long-lived applications that are maintained and adapted to different environments over many years.
These advantages are attractive, but, as always, they are balanced by a number of drawbacks.
A delegation layer as outlined here is difficult to back-patch into existing code, so typically it can be implemented only for new development projects.
The delegation layer adds to the run-time overhead of the application. For one thing, it must translate every incoming IDL type into a corresponding C++ type. Second, after the C++ call completes, it must translate any results delivered as C++ types back into IDL types.
The delegation layer creates a slight increase in code size. In addition, depending on the number of objects the application must support, it can increase the data size because of the need to map from servant instances in the delegation layer to corresponding C++ instances in the core logic. For every pair of such objects, you must keep an entry in a data structure similar to the Active Object Map.
As a rule, the advantages of a delegation layer far outweigh the disadvantages. Typically, the additional CPU time spent on copying between IDL and C++ data types is small compared with the overall execution time, so you will notice a performance degradation only if you are moving large amounts of data across the IDL interfaces. Similarly, the
867
