8.2 Accessing Objects in Other Address Spaces

Objects that share the same scope can interact. They can access each other through their names, aliases for their names, or through pointers. An object can only be accessed where its name or a pointer to it is visible. Scope determines the visibility of object names . C++ has four basic levels of scope:

  • block scope

  • function scope

  • file scope

  • class scope

Recall that a block is defined in C++ by {} so that assigning Y to X in Example 8.1 would be illegal because Y is only visible within the block that it is declared in. The function main() does not know the name Y after the closing brace of the block where Y was declared.

Example 8.1 Simple example of block scope.
 int main(int argc, char argv[]) {    int X;    int Z;    {       int Y;       Z = Y;    // Legal       //...    }    X = Y ;     // Illegal, Y is no longer defined } 

However, the name Y is visible to any other code that occurs in the same block where Y is declared. A name has function scope when it is declared within the function or the function's declaration. In Example 8.1, X and Z are visible only to the function main() and cannot be accessed by other functions. File scope refers to source files. Since a C++ program can consist of multiple files, we can have objects that are visible within one file but not in another. Names that have file scope visibility are visible from the point they are declared until the end of the source file. Names with file scope visibility will not be declared in any particular function. They are usually referred to as global variables . Names that have object scope are visible to any member function declared as part of the object. We use scope as the first level of access to an object's capabilities. The object's private, protected, and public interfaces determine the second level. Although an object's name may be visible, private and protected members still have restricted access. Scope simply tells us if the object's name is visible. In a nondistributed program, scope is associated with a single address space. Two objects in the same address space can refer to each other by name or pointer and can interact simply by invoking each other's methods .

Example 8.2 Using objects which invoke methods of other objects of the same address space.
 //... some_object   A; another_object  B; dynamic_object  *C; C = new dynamic_object; //... B.doSomething(A.doSomething()); A.doSomething(B.doSomething()); C->doMore(A.doSomething()); //... 

In Example 8.2, objects A and B are within the same scope, B is visible to A and A is visible to B . A may call B 's member functions and vice versa. How is scope affected when two objects are on different machines? What happens when B is created by another program and is in a totally different address space? How will A know of B 's existence? More importantly, how will A know B 's name and interface? How can A call member functions that belong to B if B is part of another program? In Example 8.2, objects A and B are created at compile time and object C is created at runtime. They are part of the same program, they have the same scope, and their addresses are part of the same process. In order for a process to execute an instruction, it needs to know the address of the instruction. When the program in Example 8.2 is compiled, the addresses of objects A and B are stored in the executable. Therefore, the process that executes the program in Example 8.2 will know where objects A and B can be found. The address for object C is assigned during runtime. The exact location of the object C is unknown until the new() function has been called. However, the pointer C does have an address within the same space as objects A and B and therefore the process will use the pointer to get to the object. We have access to each object because we have access to their addresses either directly or indirectly. The object's variable name is simply an alias for the object's address. If the object's name is within our scope then we may access it. The trick is how we associate a remote object with our local scope. If we want to access object D that is in another address space we need some way to introduce the address of the remote object to our executing process. We need some way to associate the remote object with our local scope. We need a visible name that is an alias for an address in another process that might even be on another machine. In some cases the other machine might be on another network! It would be convenient if we could simply ask for the remote object by some agreed-upon description and receive a reference for the address of the remote object. Once we had the reference, we could then interact with the remote object in our local scope. Here is where a CORBA implementation can be used to do distributed programming.

8.2.1 IOR Access to Remote Objects

The IOR (Interoperable Object Reference) is the standard object reference format for distributed objects. Each CORBA Object has an IOR. The IOR is a handle that uniquely identifies the object. Whereas a pointer contains a simple machine address for an object, an IOR can contain a port number, a host name, an object key, and more. In C++ we use a pointer to access dynamically created objects. The pointer tells where the object is located in memory. When an object's pointer is dereferenced, the address is used to access the services of that object. The dereferencing process requires more effort when the object to be accessed is located in a different address space and possibly on a different computer. The pointer must contain enough information to resolve the object's exact location. If the object is located on another network, then the pointer must contain either directly or indirectly a network address, a network protocol, hostname, port address, object key, and physical address. The standard IOR acts as a kind of distributed pointer to a remote object. Figure 8-2 shows a high-level breakdown of some component contained in an IOR under the IIOP protocol.

Figure 8-2. A high-level breakdown of some component contained in an IOR under the IIOP protocol.

Logical Components of an IOR

HOST

PORT

OBJECT KEY

OTHER COMPONENTS

Identifies the Internet host.

Contains the TCP/IP port number where the target object is listening for requests .

A value that maps unambiguously to a particular object.

Additional information that may be used in making invocations, e.g., security.

The notion of a portable object reference is an important advancement in distributed computing. It allows local references to remote objects to appear virtually anywhere on the Internet or an intranet. This has important implications for multiagent systems where agents may need to travel between systems and throughout the Internet. The IOR standard creates some foundation for mobile objects and distributed agents . Once your program has access to an object's IOR, then an ORB (Object Request Broker) can be used to interact with the remote object through method invocation, parameter passing, return values, and so on.

8.2.2 ORBS (Object Request Brokers)

The ORB acts on behalf of your program. It sends messages to the remote object and returns messages from the remote object. The ORB acts as a middleman between your objects and the remote objects. The ORB takes care of all the details involved in routing a request from your program to the remote object, and routing the response from the remote object back to your program. It makes the communications between systems virtually transparent. The ORB removes the need to do socket programming between processes on different computers. Similarly, it removes the need to do pipe or fifo programming between processes on the same computer. It takes care of much of the network programming that is required for distributed programs. Furthermore, it hides the differences between operating systems, computer languages, and hardware. The local objects are not aware of what language the remote objects have been implemented in, what platform they are running on, or whether they are located on the Internet or some local intranet. The ORB uses the IOR to help facilitate communications between machines, networks, and objects. Notice in Figure 8-2 that an IOR does contain information that can be used to make TCP/IP connections. We present only a high-level partial description of the IOR because the IOR is meant to be a black box for the developer. The ORB uses the IOR to locate the target object. Once the target object is located, the ORB activates it and transmits any arguments that are necessary to call the object. The ORB waits for the request to complete and returns the necessary information to the calling object or an exception if the method invocation or call fails. Figure 8-3 contains a simplified overview of the steps that an ORB uses on behalf of a local object.

Figure 8-3. The simplified overview of the steps that an ORB uses on behalf of a local object.

SIMPLIFIED ORB METHOD INVOCATION STEPS

1)

Locate the remote object.

2)

Activate the module containing the target object if it is not already activated.

3)

Transmit arguments to the remote object.

4)

Wait for response from the invocation of the remote object's method.

5)

Return information to the local object or exception if the remote method invocation failed.

The steps in Figure 8-3 present a simplified overview of what the ORB does during an interaction with a remote object. These steps are almost transparent to the local object. The local object invokes one of the methods of the remote object and the ORB performs these steps on behalf of the local object. The ORB does a lot of processing with a few simple lines of code. Typically, a distributed object-oriented application requires at least two programs. Each program has one or more objects that will interact with each other across address spaces. The object interaction may be client-server, producer-consumer, or peer-to-peer in nature. Therefore, if we have two programs, one will act as the client and the other as the server, or one as the producer and the other as the consumer, or they will both be peers. Program 8.1 implements a consumer that invokes a simple remote adding machine object. The program shows how a remote object may be accessed and how an ORB is initialized and used.

Program 8.1
 1  using namespace std;  2  #include "adding_machine_impl.h"  3  #include <iostream>  4  #include <fstream>  5  #include <string>  6  7  8  int main(int argc, char *argv[])  9  { 10     CORBA::ORB_var Orb = CORBA::ORB_init(argc,argv,"mico-local-orb"); 11     CORBA::BOA_var Boa = Orb->BOA_init(argc,argv,"mico-local-boa"); 12     ifstream In("adding_machine.objid"); 13     string Ref; 14     if(!In.eof()){ 15     In >> Ref; 16     } 17     In.close(); 18     CORBA::Object_var Obj = Orb->string_to_object(Ref.data()); 19     adding_machine_var Machine = adding_machine::_narrow(Obj); 20     Machine->add(700); 21     Machine->subtract(250); 22     cout << "Result is " << Machine->result() << endl; 23     return(0); 24  } 25 26 

On line 10 the ORB is initialized. On line 15 the IOR for the adding_machine object is read from a file. One of the nice features of the IOR is that it can be stored as a simple string and communicated to other programs. Transmitting the IOR through command line arguments, stdin , environment variables, or files are the simplest methods. An IOR can be sent using e-mail or ftp. IORs can be shared through common file systems and can be downloaded from Web pages. Once a program has an IOR for a remote object, then an ORB can be used to access the remote object. We shall cover other techniques for communicating IORs later in this chapter. But the file system technique is enough to get us started. The IOR was originally converted from an object reference to its stringified form by the remote adding machine's ORB and written to a file. On line 18 the local Orb object converts the stringified IOR back to an object reference. On line 19 the object reference is used to instantiate an adding_machine object. The interesting thing about this adding_machine object is that when its methods are invoked they will cause code on the remote machine to execute. The calls on line 20, 21, and 22

 Machine->add(700); Machine->subtract(250); cout << "Result is " << Machine->result() << endl; 

although made in our local scope, refer to executable code in another address space and in this case on another machine. To the developer the Machine object's location is transparent. After the object has been created on line 19 it is used like any other C++ object. Although there are very specific differences between local object invocations and remote object invocations, [3] the object-oriented metaphor is maintained , and from the object-oriented programming perspective remote objects look and feel like local objects. The code in Program 8.1 is client code or consumer code because it uses the services of the adding_machine object. In order for this simple adding machine application to be complete, we need the code that implements the adding_machine object. The code in Program 8.2 shows the second component to our simple adding machine application.

[3] Remote objects invocation introduces latency, security requirements, and the possibility of partial failure.

Program 8.2
 1  #include <iostream> 2  #include <fstream> 3  #include "adding_machine_impl.h" 4 5 6 7 8  int main(int argc, char *argv[]) 9  { 10   CORBA::ORB_var Orb = CORBA::ORB_init(argc,argv,"mico-local-orb"); 11   CORBA::BOA_var Boa = Orb->BOA_init(argc,argv,"mico-local-boa"); 12   adding_machine_impl *AddingMachine = new adding_machine_impl; 13   CORBA::String_var Ref = Orb->object_to_string(AddingMachine); 14   ofstream Out("adding_machine.objid"); 15   Out << Ref << endl; 16   Out.close(); 17   Boa->impl_is_ready(CORBA::ImplementationDef::_nil()); 18   Orb->run(); 19   CORBA::release(AddingMachine); 20   return(0); 21  } 22 23 

Notice on line 10 that the producer program also has to initialize an Orb object. This is an important requirement for CORBA-based programs. Each program communicates with the aid of an ORB. Initializing the ORB is one of the first things that a CORBA program must do. On line 12, the actual adding_machine object is declared. This is the object that Program 8.1 will actually communicate with. On line 13, the object reference for the actual adding_machine object is converted to its stringified form. It's then written to a simple text file that can be easily read. Once the IOR is written to the file, the Orb object waits for a request. Each time one of its methods is called, it performs the necessary addition or subtraction to a persistent value. This value is accessed by calling the adding_machine's result() method. Programs 8.1 and 8.2 represent barebones CORBA programs that show the basic structure that CORBA programs will have. The code that makes the adding_machine object distributed begins with its CORBA class declaration. Each CORBA object starts out as an IDL (Interface Definition Language) design.

8.2.3 Interface Definition Language (IDL): A Closer Look at CORBA Objects

The IDL is the standard object-oriented design language used to design classes that will be used for distributed programming. It is used to express class interfaces and class relationships. It is used to specify member function prototypes , parameter types, and return types. One primary function of the IDL is to separate the class interface from the implementation. Therefore, the actual definitions of methods are not specified with the IDL. Neither the implementation of member functions nor data members are specified using IDL. The IDL only specifies the function interface. Table 8-1 contains the commonly used keywords in the IDL.

Table 8-1. IDL Keywords

IDL Keywords

     

abstract

enum

native

struct

any

factory

Object

supports

attribute

FALSE

octet

typedef

boolean

fixed

oneway

unsigned

case

float

out

union

char

in

raises

ValueBase

const

inout

readonly

valuetype

cell

interface

sequence

void

double

long

short

wchar

exception

module

string

 

The keywords in Table 8-1 are reserved words in a CORBA program. In addition to specifying the function interface for a class, the IDL is used to specify relationships between classes. The IDL supports:

  • user -defined types

  • user-defined sequences

  • array types

  • recursive types

  • exception semantics

  • modules (similar to namespaces)

  • single and multiple interitance

  • bitwise and arithmetic operators

Here is the IDL definition for adding_machine class from Example 8.2:

 interface adding_machine{    void add(in unsigned long X);    void subtract(in unsigned long X);    long result(); }; 

It begins with the CORBA keyword interface . Notice that this adding_machine declaration does not include any variables to hold the result of the additions and subtractions. Its add() and subtract() methods accept a single unsigned long as a parameter. The parameter is accompanied by the CORBA keyword in to denote that the parameter is an input parameter. This class declaration is stored in a separate source file and named adding_machine.idl . Source files containing IDL definitions must end in the .idl suffix. The source file containing the IDL declaration must be converted to C++ before it can be used. This conversion can be done using a preprocessor step or by a standalone program. All CORBA implementations include an IDL compiler. There are IDL compilers for C, Smalltalk, C++, Java, and so on. The IDL compiler converts IDL definitions into the appropriate language. In our case the IDL compiler converts the interface declaration into legitimate C++ code. Depending on the implementation of CORBA that you use, the IDL compiler is called with syntax that will be similar to:

 idl adding_machine.idl 

This command will produce a file that contains C++ code. Since our IDL definition is saved in a file named adding_machine.idl , the MICO IDL compiler produces a file named adding_machine.h that contains several C++ skeleton classes and some CORBA data types. Table 8-2 contains the basic IDL data types.

Table 8-2. Basic IDL Data Types

IDL Datatypes

Range

Size

long

2 31 to 2 31 1

> = 32 bits

short

2 15 to 2 15 1

> = 16 bits

unsigned long

0 to 2 32 1

> = 32 bits

unsigned short

0 to 2 16 1

> = 16 bits

float

IEEE single-precision

> = 32 bits

double

IEEE double-precision

> = 64 bits

char

ISO Latin-1

> = 8 bits

string

ISO Latin-1, except ASCII NULL

Variable length

boolean

TRUE or FALSE

Unspecified

octet

0-255

> = 8 bits

any

Runtime identifiable arbitrary type

Variable length

Even after the IDL compiler creates C++ code from the interface class, the implementation for the interface class methods are still undefined. The IDL compiler produces several C++ skeletons that are to be used as base classes. Example 8.3 shows two of several classes generated by our MICO IDL compiler from the file adding_machine.idl .

Example 8.3 Two classes generated by MICO IDL compiler from the adding_machine.idl .
 class adding_machine : virtual public CORBA::Object{ public:    virtual ~adding_machine();    #ifdef HAVE_TYPEDEF_OVERLOAD    typedef adding_machine_ptr _ptr_type;    typedef adding_machine_var _var_type;    #endif    static adding_machine_ ptr _narrow(CORBA::Object_ ptr obj);    static adding_machine_ ptr _narrow(CORBA::AbstractBase_ ptr                                        obj);    static adding_machine_ ptr _duplicate(adding_machine_ ptr                                          _obj);    {       CORBA::Object::_duplicate (_obj);       return _obj;    }    static adding_machine_ ptr _nil()    {       return 0;    }    virtual void *_narrow_helper(const char *repoid);    static vector<CORBA::Narrow_proto> *_narrow_helpers;    static bool _narrow_helper2(CORBA::Object_ptr obj);    virtual void add(CORBA::ULong X) = 0;    virtual void subtract(CORBA::ULong X) = 0;    virtual CORBA::Long result() = 0; protected:    adding_machine() {}; private:    adding_machine(const adding_machine&);    void operator=(const adding_machine&); }; class adding_machine_stub : virtual public adding_machine{ public:    virtual ~adding_machine_stub();    void add(CORBA::ULong X);    void subtract(CORBA::ULong X);    CORBA::Long result(); private:    void operator=(const adding_machine_stub&); }; 

adding_machine.idl is input to the compiler and adding_machine.h along with its skeleton classes is output from the compiler. The developer uses inheritance to actually provide implementations for the function interfaces declared in the IDL source file. For instance, Example 8.4 shows the user-defined class that provides the implementation for one of the skeleton classes produced by the IDL compiler.

Example 8.4 User-defined class implementation of skeleton classes.
 class adding_machine_impl : virtual public adding_machine_skel{ private:    CORBA::Long Result; public:    adding_machine_impl(void)    {       Result = 0;    };    void add(CORBA::ULong X)    {       Result = Result + X;    };    void subtract(CORBA::ULong X)    {       Result = Result - X;    };    CORBA::Long result(void)    {       return(Result);    }; }; 

One of the skeletons that IDL compiler creates from the adding_machine interface class is named adding_machine_skel . Notice that the IDL uses the name used in the interface definition to derive new classes. Our adding_machine_impl class provides the implementation for the function interfaces declared using the IDL. First, the adding_machine_impl class declares a data member named Result . Second, it declares the actual implementations for the add() , subtract() , and the result() methods. So while the adding_machine interface class specifies the declaration of these methods, the adding_machine_impl class provides implementation of the methods. The userdefined adding_machine_impl class will inherit a lot of functionality useful for distributed programming from the base class. This is the basic scheme when doing CORBA programming. An interface class is designed that represents the interfaces to be used. The IDL compiler is called to generate real C++ class skeletons from the interface definition. The developer derives a class from one of the skeletons and provides implementations for the methods defined in the interface class and data members that will be used to hold attributes of the object. Generating real C++ classes from IDL is a three-step process:

  1. Design the class interfaces, relationships, and hierarchies using the IDL.

  2. Use the IDL Compiler to generate real C++ skeletons from the IDL classes.

  3. Use inheritance to create descendants from one or more of the skeleton classes and implement the interface methods inherited from the skeleton classes.

We'll discuss this process in more detail later in this chapter. First, let's take a closer look at the basic structure of a consumer program.



Parallel and Distributed Programming Using C++
Parallel and Distributed Programming Using C++
ISBN: 0131013769
EAN: 2147483647
Year: 2002
Pages: 133

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