Object Interconnections: Dynamic CORBA, Part 3 - The
Dynamic Skeleton Interface
Some server
applications, such as gateways or monitors, cannot know a priori the types or
identities of the objects they must serve. In this column, we show how the
CORBA Dynamic Skeleton Interface enables developers to construct such
applications portably.
Introduction
Welcome to
the third installment of our four part series covering Dynamic CORBA. In Part 1
[1], we discussed the CORBA DII
(Dynamic Invocation Interface), which allows applications to invoke operations
on target objects without having compile-time knowledge of target interfaces.
In Part 2 [2], we explained the basics of the
Dynamic Any, which enables applications to handle any
value of any IDL type, whether simple or complex, without having compile-time
information about that type. The Dynamic Any is
essential for constructing truly dynamic CORBA client or server applications.
In this
column, we present the CORBA DSI (Dynamic Skeleton Interface), which is
essentially the server-side counterpart of the DII. Some server applications,
such as gateways or monitors, cannot know a priori the types or
identities of the objects they must serve. The DSI enables CORBA developers to
construct such applications portably. There are many capabilities associated
with the DSI, so we first cover the basics and then present the more advanced
features.
Dynamic Skeleton Basics
To build
working C++ DSI applications, you need to understand the following two key
classes:
1. The CORBA::ServerRequest
class, which is a pseudo-object that is the server-side equivalent to the
client-side CORBA::Request. When the POA dispatches a request to a DSI
servant, it passes an instance of ServerRequest
to communicate all information about the request needed to allow the servant to
fulfill it.
2. The PortableServer::DynamicImplementation servant base class, from which
concrete DSI servant classes inherit. This base class provides the following
pure virtual methods that DSI servant classes are expected to override:
o void invoke (ServerRequest_ptr
server_request), which is upcalled by the
POA to allow the servant to handle requests. The POA passes the ServerRequest representing the request being
processed to this method.
o RepositoryId _primary_interface
(const ObjectId& id, POA_ptr
poa), which is called by the ORB/POA run time when it
needs the repository ID string identifying the most-derived interface of the
object that the servant is incarnating. The ORB run time passes ObjectId for the specific object whose repository ID
is requested by a DSI servant since such a servant normally incarnates multiple
objects. The second argument supplies a reference to the particular POA in
which the repository ID request is occurring. The _primary_interface
method is called when the application calls POA methods, such as servant_to_reference and id_to_reference,
so that the run time can create object references containing the appropriate
repository ID.
The _primary_interface
method is straightforward, so our examples below focus on the complexities of
the invoke method.
In our first
example, we return to the simple IDL we used in Part 1 of this series, but this
time we use it to show a simple DSI server.
// IDL
interface A {
void op ();
};
When a
client invokes A::op on an object incarnated by a DSI servant, the
dispatching POA delivers a ServerRequest
object to the invoke method on the servant. ServerRequest
supplies methods that allow the servant to deal with all aspects of the
request, including determining its name, examining its input arguments and
setting its output arguments, setting a return value, and raising exceptions.
The first
thing the servant must do to handle a request in its invoke
method is to determine which operation is being invoked. It does this by
calling the operation method on ServerRequest,
like this:
std::string op = server_request->operation
();
if (op == "op") {
// process op
request
}
The operation
method returns the simple name of the IDL operation being invoked. Note that we
store the returned name into a C++ std::string. Like
the methods provided by the DII Request class, ServerRequest's
methods do not follow normal C++ mapping rules. Since the operation
method returns a const char* for which it maintains ownership, we're
able to copy the returned string into a std::string here without
creating a memory leak.
After the
servant determines which operation it's servicing, it must then deal with the
operation's arguments. With the DSI, the servant is responsible for informing
the ORB of the number of arguments the operation expects, along with their
types and their directions (i.e., whether the argument is in, inout, or out). The situation is actually the
same for static skeleton invocation on the server. In that case, however, the
skeletons generated automatically by the IDL compiler take care of providing
argument information to the ORB, thereby shielding the servant developer from
these details.
You might
think that the ORB would automatically know what's in a marshaled request. To
minimize time and space overhead, however, CORBA is designed such that the
receiver knows what to expect. CORBA does not mandate self-describing requests
because it would mean that unnecessary information would be sent over the
network for the majority of cases. As a result, IIOP requests never contain TypeCodes to describe the argument types (except, of
course, for the TypeCodes that are always
marshaled for Any parameters). When a server
ORB receives a request, the request's header provides only enough information
to allow the ORB and POA to dispatch the request. Until the skeleton tells it
how to demarshal everything, therefore, the server
ORB must view the arguments as an array of indecipherable bytes.
The servant
tells the ORB what arguments to expect by calling the ServerRequest::arguments
method. Although the op method in our example has no arguments, the
servant must still call arguments exactly once, whether the invoked
operation has arguments or not. The servant passes argument information in the
form of an NVList, as shown below:
CORBA::NVList_var args;
orb->create_list (0, args);
The ORB's create_list method takes two parameters: (1) a count
of the number of items the list should contain and (2) an out NVList argument that the ORB uses to return the
result. In our example, we expect zero arguments, so create_list
returns an empty NVList, which we then pass to
the arguments operation, like this:
server_request->arguments (args);
Our next
example will show the details of argument processing.
The final
step is to complete the operation invocation by either returning normally or by
raising an exception. To indicate a normal return, the servant calls ServerRequest::set_result,
as we'll show in our next example. To raise an exception, the servant calls ServerRequest::set_exception.
For example, the code below shows how to throw a BAD_OPERATION exception
to inform clients that they've invoked a method that the target object does not
support:
CORBA::BAD_OPERATION exception;
CORBA::Any any;
any <<= exception;
server_request->set_exception (any);
Both set_result and set_exception
take a single Any parameter. For set_result, Any's
type must match the return type of the invoked operation. For set_exception, Any
must contain an exception that is allowed by the operation's raises
clause. A servant may call either set_result
or set_exception, but not both. Moreover,
whichever one it calls, it can call it only once.
A More Realistic DSI Use Case
Our example
above was straightforward, but overly simplistic since in practice DSI servants
usually deal with arguments and return values. To show this, let's return to
our stock Info example from our previous column. The IDL for this
example is shown below:
// IDL
module Stock {
exception Invalid_Stock {};
struct Info {
long high;
long low;
long last;
};
interface Quoter {
Info get_quote (in string stock_name) raises
(Invalid_Stock);
};
};
Let's focus
on the argument handling required to implement get_quote
in a DSI servant. We create an NVList as
before, but this time, we must populate it with type and argument direction
information. For simplicity, our example will assume that our servant already
knows the necessary information, so it doesn't need to interact with the IFR
(Interface Repository).
CORBA::NVList_var args;
orb->create_list (1, args);
args->add (CORBA::ARG_IN)->value ()->replace
(CORBA::tk_string, 0);
server_request->arguments (args);
Here, we've
changed the second line to create an NVList
with one argument. The most significant differences occur on the third line,
however, where we add a NamedValue to our NVList and indicate that it will be an in
argument by setting its Flag value to CORBA::ARG_IN. The add
method returns this new NamedValue, for which
the NVList retains ownership. On this returned
NamedValue, we invoke the value method
to gain access to the Any it holds, and on that
Any we use the replace method to set the argument type. In our
example, the argument's type is string, so we use the CORBA::tk_string enumerator to indicate this. The second
argument to replace is normally a pointer to the value to be stored in Any. In our case, however, the value will be demarshaled by the ORB, so we simply indicate that we are
not setting the value by passing a null pointer.
When we pass
this populated NVList to the arguments
method, the ORB demarshals the string argument
and inserts it into the Any inside the NVList. After arguments returns, we can
access the value of the argument using regular Any
extraction or by using the Dynamic Any. We'll use the latter, since we
explained the Dynamic Any in our previous column [2] and also because we'll need to use
it to deal with the Info return value. We first obtain a Dynamic Any factory, which we then use in conjunction with the
argument stored in the NVList to create a
Dynamic Any:
CORBA::Object_var obj =
orb->resolve_initial_references
("DynAnyFactory");
DynamicAny::DynAnyFactory_var dynfactory =
DynamicAny::DynAnyFactory::_narrow
(obj);
DynamicAny::DynAny_var dynany =
dynfactory->create_dyn_any (*args->item (0)->value ());
CORBA::String_var stock = dynany->get_string ();
Since we
know that our argument is a string, we can easily access its value
through the basic DynAny interface and store
it into our stock variable.
After we've
obtained the stock name, our servant would presumably use it to access a
database of stock information to retrieve the values needed to populate our Info
return value. Let's assume we've already retrieved the necessary data and
stored it into three long variables named stock_high_value,
stock_low_value, and stock_current_value.
Let's also assume that we've already created a Dynamic Any
to hold our Info struct, exactly as
shown in our previous column [2]. We then insert our stock values into our Info
struct as follows:
dynstruct->insert_long
(stock_high_value);
dynstruct->next ();
dynstruct->insert_long
(stock_low_value);
dynstruct->next ();
dynstruct->insert_long
(stock_current_value);
To return
this Info value from our DSI servant's invoke
method, we obtain an Any from our Dynamic Any and then call ServerRequest::set_result
to indicate that it's our return value.
CORBA::Any_var any = dynstruct->to_any ();
server_request->set_result (any);
Alternatively,
if the input argument is a stock name for which we have no information, we want
to raise a Stock::InvalidStock exception.
Assuming we had enough information to create a Dynamic Any
to hold such an exception (for an actual dynamic application, we would need IFR
access), we could raise it as our exception by obtaining its value as an Any
and then passing that Any to ServerRequest::set_exception:
CORBA::Any_var any = dynexception->to_any ();
server_request->set_exception (any);
POA Considerations
A DSI
servant normally incarnates multiple CORBA objects concurrently. Under these
circumstances, the DSI servant is best used as a default servant [3], which means that it incarnates all
objects served by the POA in which it's registered. Such a POA should be
created with the following policies:
ˇ
USE_DEFAULT_SERVANT, which directs the POA to dispatch requests to its default servant
that's set by the application using the POA::set_servant
method.
ˇ
MULTIPLE_ID, which
allows the default servant to incarnate multiple objects simultaneously.
ˇ
USER_ID, which
indicates that the application, not the system, supplies the object IDs used to
identify objects in this POA.
ˇ
NON_RETAIN, which
means we're not registering any other servants with our POA other than our
default servant, so this policy ensures that the POA avoids wasting space and
time by creating and looking up servants in an Active Object Map.
With these
POA policies in place, including the default ORB_CTRL_MODEL (which
allows a POA to process multiple requests simultaneously), a DSI servant can
expect to service requests for multiple CORBA objects concurrently. This
introduces another problem, however: for any given request, how does the
servant know which object it's expected to incarnate?
We have
shown how a DSI servant can obtain the name of the operation it's servicing,
via the ServerRequest::operation method.
While the target object ID is technically part of the server request as well,
it cannot be obtained through ServerRequest.
Instead, the servant must obtain this information from the dispatching POA via
its Current object, which supplies the following operations:
ˇ
get_POA, which returns the POA that's dispatching the current
request.
ˇ
get_object_id, which returns the ID of the target object for the
current request.
ˇ
get_reference, which returns an object reference for the target
object.
ˇ
get_servant, which returns the servant that's incarnating the
target object for the current request.
The DSI
servant gains access to the POA Current through the ORB's resolve_initial_references call:
CORBA::Object_var obj =
orb->resolve_initial_references
("POACurrent");
PortableServer::Current_var current =
PortableServer::Current::_narrow
(obj);
Since the
POA Current resides in thread-specific storage [4], each thread can access the POA Current in
this manner to obtain information about its specific target object, even when
the same servant is servicing multiple requests for separate objects
concurrently.
Concluding Remarks
The CORBA
DII discussed in [1] provides the means to assemble and invoke a request
dynamically, which is useful only to an application playing the role of a CORBA
client. CORBA servers can also benefit from this type of dynamically
request-processing capability, however. Dynamic CORBA therefore provides the
DSI that enables servers to receive and handle requests without compile-time
knowledge of operations or their signatures. In Dynamic CORBA, the DSI defines
a ServerRequest that contains methods that
allow the servant's invoke method to demarshal
request arguments into NamedValue and NVList containers, as well as to marshal the reply
to the client.
This column
described the Dynamic CORBA DSI mechanisms and illustrated how these mechanisms
can be applied to develop a server that's more flexible than one developed
using static skeletons. There are two primary tradeoffs between using DSI and
static skeletons in a CORBA server:
1. Static versus dynamic type
information. A static
skeleton has all necessary type information about arguments and return values
compiled into it, which enables the IDL compiler to perform advanced
optimizations [5]. In contrast, the DSI approach
normally requires type information to be discovered at run time, which is much
more flexible but harder to optimize. The normal source for this information is
the IFR. Our DII and DSI examples in this column series to date have not used
the IFR, which we will show in our next and final column on Dynamic CORBA.
2. Complexity versus flexibility. A static skeleton turns an
operation invocation into an ordinary call on the servant's C++ method call
that implements the operation, which is easy to program but not very flexible
since the servant's type must be known at compile time. A DSI servant, on the
other hand, is much more flexible since it is possible for it to implement a
wide range of operations that aren't known until run time. It is much more
complex to program the DSI, however, since servant developers must not only
implement the operation(s), but it must also follow the precise protocol
required to inform the ORB of the number and types of arguments to demarshal, and to set the return value or raise an
exception.
Because of
these issues, the skill set required for Dynamic CORBA is greater than that
required for developing static CORBA applications. Because static applications
follow a natural function invocation paradigm, a minimum of C++ programming
knowledge is needed to write a simple static CORBA client and server. Getting
started is easy, given that ORB installations usually contain demo applications
that a novice can easily modify to create a working system. Some ORBs even
supply programming wizards or code generators that handle all of the details
for you, allowing you to make quick work of all your small to medium static
CORBA applications. Dynamic CORBA, on the other hand, requires a much broader
understanding of CORBA's facilities, including Request and ServerRequest pseudo-objects, TypeCodes,
Any, Dynamic Any, NVLists, NamedValues, and for truly dynamic applications, the
IFR and all of its constituent interfaces and data types. It's no surprise,
therefore, that most CORBA C++ developers stick with static applications.
If you have
comments, questions, or suggestions regarding Dynamic CORBA or our column in
general, please let us know at mailto:object_connect@cs.wustl.edu.
References
[1] Steve Vinoski and
Douglas C. Schmidt. "Object Interconnections: Dynamic CORBA: Part 1, The Dynamic
Invocation Interface," C/C++ Users Journal, July 2002,
<www.cuj.com/experts/2007/vinoski.htm>.
[2] Steve Vinoski and
Douglas C. Schmidt. "Object Interconnections: Dynamic CORBA: Part 2, Dynamic Any," C/C++ Users Journal, September 2002,
<www.cuj.com/experts/2009/vinoski.htm>.
[3] Michi Henning and Steve Vinoski. Advanced CORBA Programming with
C++ (Addison-Wesley, 1999).
[4] Douglas C. Schmidt, Michael Stal,
Hans Rohnert, and Frank Buschmann. Pattern-Oriented Software
Architecture: Patterns for Concurrent and Networked Objects (Wiley and
Sons, 2000).
[5] Eric Eide, Kevin Frei, Bryan Ford, Jay Lepreau,
and Gary Lindstrom. "Flick: A Flexible, Optimizing IDL Compiler," Proceedings of
ACM SIGPLAN '97 Conference on Programming Language Design and Implementation
(PLDI), Las Vegas, NV, June 1997.
Steve Vinoski, <www.iona.com/hyplan/vinoski/>, is vice
president of Platform Technologies and chief architect for IONA Technologies
and is also an IONA Fellow. A frequent speaker at technical conferences, he has
been giving CORBA tutorials around the globe since 1993. Steve helped put
together several important OMG specifications, including CORBA 1.2, 2.0, 2.2,
and 2.3; the OMG IDL C++ Language Mapping; the ORB Portability Specification;
and the Objects By Value Specification. In 1996, he was a charter member of the
OMG Architecture Board. He is currently the chair of the OMG IDL C++ Mapping
Revision Task Force. He and Michi Henning are the
authors of Advanced
CORBA Programming with C++, published in January 1999 by Addison Wesley
Longman. Steve also represents IONA in the W3C (World Wide Web Consortium) Web
Services Architecture Working Group.
Doug Schmidt, <www.ece.uci.edu/~schmidt/>,
is an associate professor at the University of California, Irvine. His research
focuses on patterns, optimization principles, and empirical analyses of
object-oriented techniques that facilitate the development of high-performance,
real-time distributed object computing middleware on parallel processing
platforms running over high-speed networks and embedded system interconnects.
He is the lead author of the books Pattern-Oriented Software Architecture:
Patterns for Concurrent and Networked Objects, published in 2000 by Wiley and
Sons, and C++ Network Programming: Mastering Complexity with ACE and Patterns,
published in 2002 by Addison-Wesley. He can be contacted at schmidt@uci.edu.