Previous | Contents | Index |
A pure virtual class operation is defined as an operation with a null body definition. It is indicated by the initializer =0 following the function prototype. Pure virtual functions require that a derived class provide an implementation for the operation. They act as placeholders in a base class definition. The use of a pure virtual operation in a class definition implies that no objects of that class type can be instantiated and that there must be a derived class present to make use of the base class type in a program. For these reasons, a class definition containing pure virtual operations is also known as an abstract base class.
An interface class is one that hides an implementation from the user. It only presents to the user the necessary components of an object that make it useful. An abstract base class is one type of interface class.
A C++ static member function is a class operation that has a static attribute associated with it. Static member functions differ from normal member functions in that no object is needed to invoke the operation. Instead, the operation is accessed by concatenating the class name and operation name separated by the C++ :: operator. This is similar to a normal C function in scope.
The following code fragment defines an abstract base class base that contains a pure virtual function and a static member function and shows how each is accessed. The base class acts as an interface to the derived class.
class Base { public: virtual void op1() = 0; // a pure virtual operation static void op2() { cout << "I'm in Base::op2()n"; } }; class Derived : public base { public: void op1() { cout << "I'm in Derived::op1()n"; } }; Base wrong; // error: can not instantiate an abstract base class Base *base; // can only have pointers to an abstract base class Derived derived; // a derived object derived.op1(); // prints "I'm in derived::op1()" base::op2(); // prints "I'm in base::op2()" - // static member function base = &derived; // a derived object 'is-a' base object base-) op1(); // prints "I'm in derived::op1()" - polymorphism |
The first step in developing a distributed application is to write an interface definition. The interface definition ties the client and server application code together. It is a formal mechanism that describes the set of procedures the interface offers. The interface definition declares the name of the procedure, the data type of the value it returns, and the order and data types of its parameters.
A programmer writes the interface definition file using the IDL. The IDL compiler produces application header files that support the data types in the interface definition. If you write your code using information from the interface definition and include the IDL-generated header file in your source file, the client and server will be compatible. The IDL compiler also generates the stub files.
The DCE programming model, described in the following sections, has not changed to support objects and C++ bindings. The process of developing a DCE application is as follows:
The generated output is a header file containing an interface class hierarchy and client and server C++ stub files (instead of C stub files). The class hierarchy is described in the following sections.
Figure 13-1 shows the DCE interface development model.
Figure 13-1 DCE Interface Development Model
The header file generated by the IDL compiler under the -lang cxx option contains two class definitions, as follows:
These class definitions are for C++ bindings to an object exporting the interface defined in the IDL input file.
Additionally, the application developer must provide a class definition
and implementation derived from the abstract base class that implements
all of the operations defined in the IDL file, called the
manager class. (See Section 13.2.1.3 for more
information.) The following sections describe the class hierarchy.
13.2.1.1 Abstract Base Class
The abstract base class is the interface to a DCE object that is visible to the user. (It is also known as the interface class.) This class is derived from an IDL-provided base class that is used to encapsulate information necessary to support distributed objects. Each operation defined in the IDL input file is represented by a member function in the abstract base class. Operations defined to be static in the IDL file are generated as static member functions. The other operations are generated as pure virtual functions.
Several other static member functions are also generated into the
abstract base class to provide common capabilities for all DCE
distributed objects.
13.2.1.2 Proxy Class
The proxy class is used by clients to communicate with remote servers. The proxy class is generated by the IDL compiler into the header file and is derived from the abstract base class. It contains methods that implement all of the pure virtual member functions of the abstract base class. Each of these methods performs the actual packaging of parameters and forwarding of the operation call to the remote server where the object is implemented.
The proxy class in Object-Oriented RPC is similar to the traditional
client stub in procedural DCE. There are also several IDL-generated
methods contained in the proxy class to support remote operations.
13.2.1.3 Manager Class
The developer of the server side of a DCE distributed object application is required to supply a class definition and implementation derived from the abstract base class that implements all of the operations defined in the IDL file.
The server stub uses the abstract base class when invoking methods. The polymorphic behavior of the class hierarchy forwards the invocation to the manager class implementation.
The manager class in Object-Oriented RPC is similar to the traditional manager code in procedural DCE. Objects that the server application instantiates on behalf of client applications are termed manager objects.
Figure 13-2 shows the class hierarchy for DCE distributed objects.
Figure 13-2 Class Hierarchy for DCE Distributed Objects
An entry point vector contains the manager entry points for the remote procedures defined in the IDL file. You need not specify this vector with Object-Oriented RPC because a default entry point vector stub is generated by the IDL compiler when you specify the -lang cxx option.
Server applications that call the DCE API routine
rpc_server_register_if() should pass a NULL value as the third
parameter to represent the entry point vector address. This ensures
that the generated entry point vector is used in the call.
13.2.3 Obtaining an Object Reference
Before you can send a message to a remote object, you must obtain an object reference. The object reference represents the C++ this pointer. There are two ways to obtain an object reference, as follows:
Figure 13-3 shows object reference information.
Figure 13-3 Object Reference Information
A client that has obtained an object reference and assigned it to an interface pointer can invoke methods using normal C++ syntax. The interface pointer returned from the DCE runtime is actually a pointer to an interface proxy class, which has an is-a relationship with the abstract base class. The polymorphic behavior of the class hierarchy causes a method invocation to be passed to the proxy class, which packages the arguments and forwards them to the server process identified by the encapsulated object reference.
When the server process receives the operation invocation, it decodes
the UUID from the binding handle and uses it to retrieve the object
address from the DCE runtime. The parameters are then unpackaged and
forwarded to the object method by the server stub. Output parameters
and return results are packaged and returned to the caller when the
method operation is completed.
13.3 Using the Features of Object-Oriented RPC
The following sections discuss the features of Object-Oriented RPC and
how to use them. These sections contain code fragments from an example
DCE application involving an interface to a matrix supporting several
arithmetic operations. The complete example application is described in
Chapter 19.
13.3.1 Automatically Compiling Stub Files
You can direct the IDL compiler to process the IDL file and produce either compiled object modules, source files, or both.
The client stub contains implementations of all the static base class and proxy class operations. As in C++, static member functions do not require a this pointer and, therefore, do not require an object reference to be invoked. They can use one of the three binding methods DCE provides to connect to a remote procedure: by name, by binding handle, or by UUID (see Section 12.3.6). Normal member functions that require an object carry out remoteness by using the encapsulated object reference information to build a binding handle. The IDL compiler generates code to hide this task from the user.
The server stub contains a normal DCE remote procedure routine for each
supported operation and carries out the tasks of translating the
binding handle to a C++ this pointer and passing parameters
and control to the object method.
13.3.2 Controlling the Location of an Included File
You can use an attribute configuration file (ACF) to control some aspects of RPC on the client side without affecting the server side. The opposite is also true. These aspects should not be in the interface definition because you may not want to force them on all clients and servers.
The ACF statement include causes a file to be #-included into the generated header file. With this statement, you control into which file the header is #-included. Following are ACF attributes that you can specify with the include statement:
Using no attribute on this statement forces the include line to be
generated only into the header file.
13.3.3 Generating Static Member Functions for Interfaces
You can specify a static member function within an interface definition if you compile it with the -lang cxx option. You can declare an interface operation static in the following ways:
IDL operations declared static are generated as static member functions in the generated C++ interface class. Static operations require an implementation in the server application separate from the normal manager class.
You can access static operations from the application using normal C++ syntax by concatenating the interface name and the operation name separated by the :: operator. Static operations are unique because they do not require a this pointer (object reference) when invoked as do all other class methods.
An alternate form of this attribute allows you to specify the routine to be called by the server stub. This is useful when a client and server stub are linked into the same application. You can map the operation name in the server stub to another name and avoid a name conflict between the class member function and the manager operation.
Assigning a [cxx_static] attribute to all operations in an IDL file and compiling the IDL file with the -lang cxx option turned on is equivalent to using traditional DCE IDL programming, except that the generated stubs are C++ object files and the operation names are prefixed with the <INTERFACE> :: syntax.
Following is an example of the static keyword.
File: matrix.idl [ uuid(D784B260-B5DB-11CB-832C-08002B2A1BCA) ] interface Matrix { . . . static Matrix * newMatrix( [in] double& v11, [in] double& v12, [in] double& v21, [in] double& v22 ); Matrix * createMatrix( [in] double& v11, [in] double& v12, [in] double& v21, [in] double& v22 ); Matrix * createAnotherMatrix( [in] double& v11, [in] double& v12, [in] double& v21, [in] double& v22 ); . . . } |
Following is an example of the cxx_static keyword. The operator name createAnotherMatrix is mapped to the name ClassFactory in the server stub.
File: matrix.acf interface Matrix { /* ** Add the include file to the server stub ** to declare the operation prototype for ** ClassFactory */ [sstub] include "statics"; [cxx_static] createMatrix(); [cxx_static(ClassFactory)] createAnotherMatrix(); } |
Following is an example illustrating the use of static interface functions.
File: client.cxx Matrix *m1 = Matrix::newMatrix(1, 2, 3, 4); Matrix *m2 = Matrix::createMatrix(1, 2, 3, 4); // the next line has the affect of calling the // ClassFactory() operation in the server application Matrix *m3 = Matrix::createAnotherMatrix(1, 2, 3, 4); |
In the preceding example, the [sstub] include statement allows
the function prototype for the manager function ClassFactory
defined in statics.h to be included into the server stub so
that the compiler recognizes the reference.
13.3.4 Specifying Object Interfaces as IDL Parameters
A data type can represent a DCE interface, and you can pass variables of that type as parameters to remote methods. When an IDL interface input file is compiled with the -lang cxx option, an abstract C++ class definition is created from the interface definition. Because the class is an abstract class, no objects can be instantiated of that type. Pointers to the class type are created instead.
Variables declared as interface class pointers can be declared in the IDL definition as [in] parameters, [out] parameters, [in,out] parameters, or return values. These variables may also be array members, structure members, or union members.
You can use the generated C++ interface class in an application as a
valid data type, and you can define pointers to that type. You can
assign and manipulate these pointers like any other C++ class pointer
type.
13.3.5 Registering Objects in the DCE Namespace
Server applications create objects and advertise them to the DCE environment. Clients can then bind to an object and send messages to it. These objects are called named objects. You can register objects in the DCE namespace by calling an operation available to all DCE RPC objects as a member function of the interface. This function registers the object with the name service. (Clients can bind to a registered object in one of several ways, as discussed in the following section.)
A Boolean default parameter to this operation determines whether the end point map for the named object is updated or replaced. The default value for the parameter is TRUE. When a FALSE value is passed as this argument, the end point map for the object is added to rather than replaced. This will allow multiple servers for the same interface, but a distinct set of named objects to be running on the same machine concurrently.
Following is an example of how a server registers an object with the DCE name service.
. . . char *IdentityMatrixName = "/.:/objects/identityMatrix"; Matrix *IdentityMatrix = new MatrixMgr(1, 0, 0, 1); IdentityMatrix->register_named_object( (unsigned_char_t *) IdentityMatrixName ); . . . |
There are several ways in which you can bind a local interface pointer to a remote object. The user can also bind a local interface pointer to a local object by simply instantiating an object of the manager class type and assigning the object to a variable declared to be an interface pointer. Whether the actual object is local or remote is handled by the DCE runtime. You can have both types of objects in the same application.
You can bind to an object that is registered by a server with the DCE name service in one of three ways:
The IDL compiler generates three overloaded static member functions into the interface class for binding clients to remote named objects. All three functions return an interface pointer. These functions are discussed in the following sections.
Note that the binding is not fully resolved until an operation is invoked on the interface. The bind operations access the name service as necessary to encapsulate enough information so that the object binding can be resolved.
Objects that are advertised by a server with the name service and bound
to by clients cannot be deleted by the clients. These types of named
objects are long-lived and can only be deleted by the server
application. Invoking a delete operation on an interface bound to a
named object only deletes the client's reference to the object and
causes no message to be sent to the server.
13.3.6.1 Binding to Remote Objects by Name
A client can bind to an object registered with the DCE name service by using the overloaded bind() member functions of the interface class. The unsigned_char_t * argument to this function specifies the CDS name of the object for which to search in the CDS database.
The function <INTERFACE> *bind(unsigned_char_t*) allows the client to connect to the object by CDS name.
Following is an example of binding to an object by CDS name.
File: client.cxx: . . . Matrix *m; m = Matrix::bind( (unsigned_char_t *) "/.:/objects/identityMatrix"); . . . |
If the client knows the binding handle of the object it wants to communicate with, it can use another overloaded bind() member function to obtain the object reference. This form of the bind operation does not involve any DCE name service operations.
The function <INTERFACE> *bind(handle_t) allows the client to connect to the object by using an explicit binding handle.
Following is an example of binding to an object by binding handle.
File: client.cxx: . . . const char * IdentityMatrixBH = "dcea4900-65ba-11cd-bb34-08002b3d8412@ncacn_ip_tcp:16.31.48.49"; rpc_binding_handle_t binding_handle; Matrix *m; unsigned32 status; rpc_binding_from_string_binding( (unsigned_char_t *) IdentityMatrixBH, &binding_handle, &status ); m = Matrix::bind(binding_handle); . . . |
The server developer can choose to assign unique identifiers to long-lived objects and advertise those identifiers in some way. A client can bind to such an object by using a UUID. The CDS namespace is searched for an entry matching the UUID, starting at the default entry as specified by the RPC_DEFAULT_ENTRY environment variable. The search is limited to the hierarchy indicated by the default entry. To more effectively manage binding handles in this manner, you can create a CDS group entry with entries for the various objects as members of the group. The RPC_DEFAULT_ENTRY environment variable identifies the group name.
The function <INTERFACE> *bind(uuid_t) allows the client to connect to the object by using a unique identifier.
Following is an example of binding to an object by UUID.
File: client.cxx: . . . #define IdentityMatrixId "dcea4900-65ba-11cd-bb34-08002b3d8412" uuid_t objectID; Matrix *m; unsigned32 status; uuid_from_string( (unsigned_char_t *) IdentityMatrixId, &objectID, &status ); m = Matrix::bind(objectID); . . . |
A server can advertise an object without creating the object until it is needed. For example, a bank server can advertise bank accounts, but it might not create account objects in its address space for every known account until the account is needed for a transaction. Similarly, persistent objects can be stored outside of the process on a disk. You can allow servers to manage object instantiation by specifying a server function to perform server object management. The ACF attribute [cxx_lookup (name)], when applied to an IDL interface, causes the generated server stub to invoke another operation when a reference to the object determines that the object is not currently registered with the runtime. The attribute identifies the operation name and the user must supply the operation implementation.
The prototype for a user-supplied function for a [cxx_lookup] attribute is as follows:
<INTERFACE> * <OPERATION>( [in] uuid_t * u) |
The uuid_t pointer parameter points to the unique identifier from the object reference. The lookup operation can use the identifier to determine the specific object to be instantiated. A null value returned from this operation indicates that the server could not find or instantiate the object.
Following is an example of the cxx_lookup ACF attribute.
File: matrix.acf [cxx_lookup(server_lookup)] interface Matrix { /* ** Add the include file to the server stub ** to declare the operation prototype */ [sstub] include "lookup"; } |
Previous | Next | Contents | Index |