[Up: Guided tour through MICO]
[Previous: State of development] [Next: Implementation Overview]

Subsections


Sample Program

To get you started with MICO, this section presents an example of how to turn a single-process object oriented program into a MICO application.


Standalone program

Imagine a bank which maintains accounts of its customers. An object which implements such a bank account offers three operations3.1: deposit a certain amount of money, withdraw a certain amount of money, and an operation called balance that returns the current account balance. The state of an account object consists of the current balance. The following C++ code fragment shows the class declaration for such an account object:

  class Account {
      long _current_balance;
  public:
      Account ();
      void deposit (unsigned long amount);
      void withdraw (unsigned long amount);
      long balance ();
  };

The above class declaration describes the interface and the state of an account object, the actual implementation which reflects the behavior of an account, is shown below:

  Account::Account ()
  {
      _current_balance = 0;
  }
  void Account::deposit (unsigned long amount)
  {
      _current_balance += amount;
  }
  void Account::withdraw (unsigned long amount)
  {
      _current_balance -= amount;
  }
  long Account::balance ()
  {
      return _current_balance;
  }

Here is a piece of code that makes use of a bank account:

  #include <iostream.h>

  int main (int argc, char *argv[])
  {
      Account acc;

      acc.deposit (700);
      acc.withdraw (250);
      cout << "balance is " << acc.balance() << endl;

      return 0;
  }

Since a new account has the initial balance of $0$, the above code will print out ``balance is 450''.


MICO application

Now we want to turn the standalone implementation from the previous section into a MICO application. Because CORBA objects can be implemented in different programming languages3.2 the specification of an object's interface and implementation have to be separated. The implementation is done using the selected programming language, the interface is specified using the so called Interface Definition Language (IDL). Basically the CORBA IDL looks like C++ reduced to class and type declarations (i.e., you cannot write down the implementation of a class method using IDL). Here is the interface declaration for our account object in CORBA IDL:

  interface Account {
      void deposit (in unsigned long amount);
      void withdraw (in unsigned long amount);
      long balance ();
  };

As you can see it looks quite similar to the class declaration in section 3.3.1. The in declarator declares amount as an input parameter to the deposit() and withdraw() methods. Usually one would save the above declaration to a file called account.idl.

The next step is to run this interface declaration through the IDL compiler that will generate code in the selected implementation programming language (C++ in our example). The MICO IDL compiler is called idl and is used like this:

  idl --boa --no-poa account.idl

The IDL compiler will generate two files: account.h and account.cc (see figure 3.2). The former contains class declarations for the base class of the account object implementation and the stub class a client will use to invoke methods on remote account objects. The latter contains implementations of those classes and some supporting code. For each interface declared in an IDL-file, the MICO IDL compiler will produce three C++ classes3.3.

Figure 3.2: Creation process of a MICO application.
\begin{figure}
\begin{center}
\ \psfig{file=pics/stub-gen.eps,width=11cm}\end{center}\end{figure}

The three classes are depicted in figure 3.3 between the two dashed lines. The class Account serves as a base class. It contains all definitions which belong to the interface Account, like local declarations of user defined data structures. This class also defines a pure virtual function for each operation contained in the interface. The following shows a bit of the code contained in class Account:

  // Code excerpt from account.h
  class Account : virtual public CORBA::Object {
      ...
  public:
      ...
      virtual void deposit (CORBA::ULong amount) = 0;
      virtual void withdraw (CORBA::ULong amount) = 0;
      virtual CORBA::Long balance () = 0;
  }

The class Account_skel is derived from Account. It adds a dispatcher for the operations defined in class Account. But it does not define the pure virtual functions of class Account. The classes Account and Account_skel are therefore abstract base classes in C++ terminology. To implement the account object you have to subclass Account_skel providing implementations for the pure virtual methods deposit(), withdraw() and balance().

The class Account_stub is derived from class Account as well. In contrast to class Account_skel it defines the pure virtual functions. The implementation of these functions which is automatically generated by the IDL-compiler is responsible for the parameter marshalling. The code for Account_stub looks like this:

  // Code excerpt from account.h and account.cc
  class Account;
  typedef Account *Account_ptr;

  class Account_stub : virtual public Account {
      ...
  public:
      ...
      void deposit (CORBA::ULong amount)
      {
         // Marshalling code for deposit
      }
      void withdraw (CORBA::ULong amount)
      {
         // Marshalling code for withdraw
      }
      CORBA::Long balance ()
      {
         // Marshalling code for balance
      }
  }

This makes Account_stub a concrete C++ class which can be instantiated. The programmer never uses the class Account_stub directly. Access is only provided through class Account as will be explained later.

Figure 3.3: Inheritance relationship between stub- and skeleton classes.
\begin{figure}
\begin{center}
\ \psfig{file=pics/stub-skel-hierarchy.eps,width=10cm}\end{center}\end{figure}

It is worthwile to see where the classes Account and Account_skel are derived from. Account inherits from Object, the base class for all CORBA objects. This class is located in the MICO library. The more interesting inheritance path is for Account_skel. Account_skel inherits from StaticMethodDispatcher, a class located again in the MICO library. This class is responsible for dispatching a method invocation. It maintains a list of method dispatchers3.4. The class StaticMethodDispatcher inherits from StaticImplementation. This class mirrors the behaviour of the dynamic skeleton interface (DSI), but is more efficiently designed.

Up until now we have written the interface of an account object using CORBA IDL, saved it as account.idl, ran it through the IDL compiler which left us with two files called account.cc and account.h that contain the class declarations for the account implementation base class (Account_skel) and the client stub (Account_stub). Figure 3.2 illustrates this. What is left to do is to subclass Account_skel (implementing the pure virtual methods) and write a program that uses the bank account. Here we go:

 1: #include "account.h"
 2: 
 3: class Account_impl : virtual public Account_skel
 4: {
 5: private:
 6:   CORBA::Long _current_balance;
 7: 
 8: public:
 9:   Account_impl()
10:   {
11:     _current_balance = 0;
12:   };
13:   void deposit( CORBA::ULong amount )
14:   {
15:     _current_balance += amount;
16:   };
17:   void withdraw( CORBA::ULong amount )
18:   {
19:     _current_balance -= amount;
20:   };
21:   CORBA::Long balance()
22:   {
23:     return _current_balance;
24:   };
25: };
26: 
27: 
28: int main( int argc, char *argv[] )
29: {
30:   // ORB initialization
31:   CORBA::ORB_var orb = CORBA::ORB_init( argc, argv, "mico-local-orb" );
32:   CORBA::BOA_var boa = orb->BOA_init( argc, argv, "mico-local-boa" );
33: 
34:   // server side
35:   Account_impl* server = new Account_impl;
36:   CORBA::String_var ref = orb->object_to_string( server );
37:   cout << "Server reference: " << ref << endl;
38:   
39:   //----------------------------------------------------------------------
40:   
41:   // client side
42:   CORBA::Object_var obj = orb->string_to_object( ref );
43:   Account_var client = Account::_narrow( obj );
44: 
45:   client->deposit( 700 );
46:   client->withdraw( 250 );
47:   cout << "Balance is " << client->balance() << endl;
48: 
49:   // We don't need the server object any more. This code belongs
50:   // to the server implementation
51:   CORBA::release( server );
52:   return 0;
53: }

Lines 3-25 contain the implementation of the account object, which is quite similar to the implementation in section 3.3.1. Note that the class Account_impl inherits the from class Account_skel, which contains the dispatcher for this interface, via a virtual public derivation. Although the keyword virtual is not required in this case, it is a good practise to write it anyway. This will become important when interface inheritance is discussed in section 5.5.

The main() function falls into two parts which are seperated by the horizontal line (line 39): Above the separator is the server part that provides an account object, below the line is the client code which invokes methods on the account object provided by the server part. Theoretically the two parts could be moved to two seperate programs and run on two distinct machines and almost nothing had to be changed in the code. This will be shown in the next section.

In line 32 the MICO initialization function is used to obtain a pointer to the Object Request Broker (ORB) object--a central part of each CORBA implementation. Among others the ORB provides methods to convert object references into a string representation and vice versa. In line 35 an account object called server is instantiated. Note that it is not permitted to allocate CORBA objects on the run-time stack. This is because the CORBA standard prescribes that every object has to be deleted with a special function called CORBA::release(). Automatic allocation of an object would invoke its destructor when the program moves out of scope which is not permissible. In our little sample program the server object is deleted explicitly in line 51.

In line 36 the ORB is used to convert the object reference into a string that somehow has to be transmitted to the client (e.g., using Email, a name service or a trader). In our example client and server run in the same address space (i.e. the same process) so we can turn the string into an object reference back again in line 42. Line 43 uses the Account::_narrow() method to downcast the object reference to an Account_var. The rest of main() just uses the account object which was instantiated in line 35.

Account_var is a smart pointer to Account instances. That is an Account_var behaves like an Account_ptr except that the storage of the referenced object is automatically freed via the aforementioned release() function when the Account_var is destroyed. If you use Account_ptr instead you would have to use CORBA::release() explicitly to free the object when you are done with it (never use delete instead of CORBA::release()).

Assuming the above code is saved to a file called account_impl.cc you can compile the code like this3.5:

  mico-c++ -I. -c account_impl.cc -o account_impl.o
  mico-c++ -I. -c account.cc -o account.o
  mico-ld -I. -o account account_impl.o account.o -lmico2.3.6

This will generate an executable called account. Running it produces the following output:

  Server reference: IOR:010000001000000049444c3a4163636f756e743a312e3\
  0000200000000000000300000000101000013000000752d6d61792e7468696e6b6f\
  6e652e636f6d00007b0900000c000000424f410a20b0530000055f0301000000240\
  0000001000000010000000100000014000000010000000100010000000000090101\
  0000000000
  Balance is 450

You can find the source code for this example in the demo/boa/account directory within the MICO source tree. Note that the IOR may look different on different systems. This is because it contains information which depend on the hostname, port number and object ID for the server object among other things. There is a tool called iordump (see directory mico/tools/iordump) which shows the content of the IOR. Feeding the IOR above into iordump yields the following output:

    Repo Id:  IDL:Account:1.0

IIOP Profile
    Version:  1.0
    Address:  inet:u-may.thinkone.com:2427
   Location:  iioploc://u-may.thinkone.com:2427/BOA%0a%20%b0S%00%00%05%5f%03
        Key:  42 4f 41 0a 20 b0 53 00 00 05 5f 03             BOA. .S..._.

Multiple Components Profile
 Components:  Native Codesets:
              normal: ISO 8859-1:1987; Latin Alphabet No. 1
                wide: ISO/IEC 10646-1:1993; UTF-16, UCS Transformation Format\
                                               16-bit form
        Key:  00                                              .

Separating client and server

CORBA would be pretty useless if you always had to run the object implementation (server) and the client that uses the server in the same process. Here is how to separate the client and server parts of the example in the previous section into two processes running on the same or on different machines3.6.

One problem you have to cope with when moving object implementation and client into separate address spaces is how the client gets to know the server. The solution to this problem is called a naming service.


Stringified Object References

The example in section 3.3.2 already used the ORB methods object_to_string() and string_to_object() to make a stringified representation of an object reference and to turn back this string into an object, respectively.

When separating client and server you have to find a way to transmit the stringified object reference from the server to the client. If client and server run on machines that share a single file system you can make the server write the string into a file which is read by the client. Here is how to do it:

 1: // file account_server.cc
 2:
 3: #include <iostream.h>
 4: #include <fstream.h>
 5: #include "account.h"
 6: 
 7: class Account_impl : virtual public Account_skel
 8: {
 9:   // unchanged, see section "MICO Application"
10:   // ...
11: };
12: 
13: 
14: int main( int argc, char *argv[] )
15: {
16:   // ORB initialization
17:   CORBA::ORB_var orb = CORBA::ORB_init( argc, argv, "mico-local-orb" );
18:   CORBA::BOA_var boa = orb->BOA_init( argc, argv, "mico-local-boa" );
19: 
20:   Account_impl* server = new Account_impl;
21:   CORBA::String_var ref = orb->object_to_string( server );
22:   ofstream out ("/tmp/account.objid");
23:   out << ref << endl;
24:   out.close ();
25:
26:   boa->impl_is_ready( CORBA::ImplementationDef::_nil() );
27:   orb->run ();
28:   CORBA::release( server );
29:   return 0;
30: }

Account_impl, the implementation of the account object in lines 7-11 is the same as in section 3.3.2. The main() function performs ORB and BOA3.7initialization in lines 16-18, which will evaluate and remove CORBA specific command line options from argv, see section 4.1.1 for details. In line 20 an account object is created, lines 21-24 obtain a stringified object reference for this object and write it to a file called account.objid.

In line 26 the impl_is_ready() method of the BOA is called to activate the objects implemented by the server. The ORB method run(), which is invoked in line 27 will enter a loop to process incoming invocations3.8. Just before returning from main(), CORBA::release() is used in line 28 to destroy the account server object.

 1: // file account_client.cc
 2:
 3: #include <iostream.h>
 4: #include <fstream.h>
 5: #include "account.h"
 6: 
 7: int main( int argc, char *argv[] )
 8: {
 9:   // ORB initialization
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:
13:   ifstream in ("/tmp/account.objid");
14:   char ref[1000];
15:   in >> ref;
16:   in.close ();
17:
18:   CORBA::Object_var obj = orb->string_to_object (ref);
19:   Account_var client = Account::_narrow( obj );
20: 
21:   client->deposit( 700 );
22:   client->withdraw( 250 );
23:   cout << "Balance is " << client->balance() << endl;
24:
25:   return 0;
26: }

After ORB and BOA initialization the client's main() function reads the stringified object reference in lines 13-16 and turns it back into an account object stub in lines 18-19. After making some method invocations in lines 21-23 client will be destroyed automatically because we used and Account_var smart pointer.

Compile the client and server programs like this:

  mico-c++ -I. -c account_server.cc -o account_server.o
  mico-c++ -I. -c account_client.cc -o account_client.o
  mico-c++ -I. -c account.cc -o account.o
  mico-ld -o server account_server.o account.o -lmico2.3.6
  mico-ld -o client account_client.o account.o -lmico2.3.6

First run server and then client in a different shell. The output from client will look like this:

  Balance is 450

Note that running the client several times without restarting the server inbetween will increase the balance the client prints out by 450 each time! You should also note that client and server do not necessarily have to run on the same machine. The stringified object reference, which is written to a file called /tmp/account.objid, contains the IP address and port number of the server's address. This way the client can locate the server over the network. The same example would also work in a heterogeneous environment. In that case you would have to compile two versions of account.o, one for each hardware architecture. But the conversion of the parameters due to different data representations is taken care of by MICO.

Naming Service

What we have actually done in the last section is to implement some very simple kind of naming service on top of the file system. A naming service is a mapping between names and addresses which allows you to look up the address for a given name. For example a phone directory is a naming service: it maps people's names to phone numbers.

In the CORBA context a naming service maps names to object references. The simple naming service we implemented in the previous section maps file names to stringified object references. The OMG has defined a more elaborate naming service as a set of CORBA objects, an implementation of which is now shipped with MICO. To use the name service you have to

There is a program called nsadmin that can be used to browse and change the contents of the naming service. The demo/services/naming directory contains an example how to use the name service.


The MICO Binder (CORBA Extension)

There is still one problem left: How do you get an object reference for the naming service itself? Especially if the naming service and the client reside on machines that do not share a file system that could be used to pass around stringified object references as in the previous section3.9. Because the CORBA standard does not offer a solution to this problem MICO has to invent its own. Because it might be useful for other purposes as well we decided to make the solution available to you, dear user. Note that using this feature makes your programs incompatible with other CORBA implementations.

The MICO Binder is a very simple naming service that maps (Address, RepositoryId) pairs to object references. A RepositoryId is a string that identifies a CORBA IDL-object and consists of the absolute name of the IDL-object and a version number. RepositoryId's are generated by the IDL compiler. The RepositoryId for the Account interface looks like this:

  IDL:Account:1.0

See section $[$6.6$]$ of [5] for details on RepositoryId's. An Address identifies one process on one computer. MICO currently defines three kinds of addresses: internet addresses, unix addresses, and local addresses. An internet address is a string with the format

  inet:<host name>:<port number>

which refers to the process on machine <host name> that owns the TCP port <port number>. Unix addresses look like

  unix:<socket file name>

and refer to the process on the current machine that owns the unix-domain socket3.10 bound to <socket file name>. Local addresses look like

  local:

and refer to the process they are used in (i.e., this process). Here is an adaption of the account example which uses the MICO binder:

 1: // file account_server2.cc
 2:
 3: #include "account.h"
 4:
 5: class Account_impl : virtual public Account_skel
 6: {
 7:   // unchanged, see section "MICO Application"
 8:   // ...
 9: };
10: 
11: 
12: int main( int argc, char *argv[] )
13: {
14:   // ORB initialization
15:   CORBA::ORB_var orb = CORBA::ORB_init( argc, argv, "mico-local-orb" );
16:   CORBA::BOA_var boa = orb->BOA_init( argc, argv, "mico-local-boa" );
17: 
18:   Account_impl* server = new Account_impl;
19:
20:   boa->impl_is_ready( CORBA::ImplementationDef::_nil() );
21:   orb->run ();
22:   CORBA::release( server );
23:   return 0;
24: }

The server is essentially the same as in 3.3.3 except that it does not write a stringified object reference to a file. Here is the client:

 1: // file account_client2.cc
 2:
 3: #include "account.h"
 4:
 5: 
 6: int main( int argc, char *argv[] )
 7: {
 8:   // ORB initialization
 9:   CORBA::ORB_var orb = CORBA::ORB_init( argc, argv, "mico-local-orb" );
10:   CORBA::BOA_var boa = orb->BOA_init( argc, argv, "mico-local-boa" );
11:
12:   CORBA::Object_var obj
13:     = orb->bind ("IDL:Account:1.0", "inet:localhost:8888");
14:   if (CORBA::is_nil (obj)) {
15:      // no such object found ...
16:   }
17:   Account_var client = Account::_narrow( obj );
18: 
19:   client->deposit( 700 );
20:   client->withdraw( 250 );
21:   cout << "Balance is " << client->balance() << endl;
22:
23:   return 0;
24: }

After completing ORB and BOA initialization the client uses bind() to bind to an object with repository id IDL:Account:1.0 that is running in the process that owns port 8888 on the same machine. Lines 14-16 check if the bind failed. Everything else is the same as in section 3.3.3. Compile:

  mico-c++ -I. -c account.cc -o account.o
  mico-c++ -I. -c account_server2.cc -o account_server2.o
  mico-c++ -I. -c account_client2.cc -o account_client2.o
  mico-ld -o server2 account.o account_server2.o -lmico2.3.6
  mico-ld -o client2 account.o account_client2.o -lmico2.3.6

Start the server like this, telling it to run on port number 8888:

  ./server2 -ORBIIOPAddr inet:localhost:8888

Run the client in a different shell without any arguments. It should behave the same way as the client from section 3.3.3.

If a server offers several objects (lets say A and B) of the same type (i.e., with the same repository id) and a client wants to bind to A it needs a means to distinguish objects of the same type. This is accomplished by assigning objects an identifier during creation in the server and specifying this identifier as an extra argument to bind() in the client. The identifier is of type BOA::ReferenceData, which is a sequence of octets. You can use ORB::string_to_tag() and ORB::tag_to_string() to convert a string into such an identifier and vice versa. Here are the changes to the server code:

 1: #include "account.h"
 2: 
 3: class Account_impl : virtual public Account_skel {
 4: public:
 5:  Account_impl (const CORBA::BOA::ReferenceData &refdata)
 6:     : Account_skel (refdata)
 7:   {
 8:     _current_balance = 0;
 9:   }
10:   // remaining parts unchanged
11: };
12:  
13: int main( int argc, char *argv[] )
14: {
15:   ...
16:   CORBA::BOA::ReferenceData_var id
17:     = CORBA::ORB::string_to_tag ("foo");
18:   Account_impl* server = new Account_impl (id);
19:   ...
20: }

Changes to the client:

 1: #include "account.h"
 2: 
 3: int main( int argc, char *argv[] )
 4: {
 5:   ...
 6:   CORBA::BOA::ReferenceData_var id
 7:     = CORBA::ORB::string_to_tag ("foo");
 8:   CORBA::Object_var obj
 9:     = orb->bind ("IDL:Account:1.0", id, "inet:localhost:8888");
10:   ...
11: }

To avoid hardcoding the address of the server into the client you can leave out the second argument to bind() and specify a list of addresses to try using the -ORBBindAddr command line option. For example

  ./client -ORBBindAddr local: -ORBBindAddr inet:localhost:8888

will make bind() try to bind to an account object in the same process and if that fails it will try to bind to an account object running in the server than owns port 8888 on the same machine. Note that addresses specified using -ORBBindAddr are only taken into account if you to not specify an explicit address.

The demo/boa/account2 directory contains an example that uses the MICO binder.


[Previous: State of development] [Next: Implementation Overview]
[Up: Guided tour through MICO]

Frank Pilhofer
2001-09-28