CORBA, Part II: Advanced Features

A look at some of CORBA's advanced features, including Naming Services.

In Part I of this series on CORBA, we presented a brief look at CORBA, what it is and how it used. In Part II we are going to look at two advanced features of CORBA. The first feature we are going to look at deals with the advanced data structures supported by the CORBA IDL. In particular, we are going to look at sequences and unions. The second feature deals with the CORBA services included as part of the CORBA standard. In particular, we are going to look at the CORBA Naming Service and how it can be used to located CORBA objects.

Sequences and Unions

Anyone who has programmed using the C or C++ languages knows that pointers and, in the case of C++, references, are valuable tools for manipulating and processing data. Because CORBA objects are global objects and may exist in different address spaces, data pointers cannot be passed between objects. For example, a linked list structure in one object cannot be passed to another object as part of a method invocation.

Even though this lack of support for data pointers would seem to be a severe restriction, in practice it rarely turns out to be one. For example, the linked-list data structure can be converted to an array, which is supported by CORBA, with the link pointers being converted into array indices. Even though arrays can be used for such situations as linked lists, they also are restrictive in the sense that the size of the array must be know a priori. This is necessary because the underlying transport must know how many bytes to send when an array is transferred between objects.

In order to deal with this type of situation, CORBA provides the sequence data type. One can view the sequence data type simply as a variable length one-dimensional array. As we can see in the example, one can create either a bounded or unbounded sequence, the difference being whether the maximum length of the sequence is specified.

The IDL code in Listing 1 is an updated version of our original IDL. It now includes a new interface method, invokeOperation(), that has three arguments, two of which are sequences. The IDL begins by defining a new enumerated data type called DataType. Enumerations in CORBA are the same as enumerations in C/C++.

Listing 1. Updated IDL Code



enum DataType {
    DT_SHORT,
    DT_LONG,
    DT_USHORT,
    DT_ULONG,
    DT_FLOAT,
    DT_DOUBLE,
    DT_CHAR,
    DT_STRING,
    DT_BOOLEAN
};

union DataValue switch (DataType) {
  case DT_SHORT:
    short shortData;
  case DT_LONG:
    long longData;
  case DT_USHORT:
    unsigned short ushortData;
  case DT_ULONG:
    unsigned long ulongData;
  case DT_FLOAT:
    float floatData;
  case DT_DOUBLE:
    double doubleData;
  case DT_CHAR:
    char charData;
  case DT_STRING:
    string stringData;
  case DT_BOOLEAN:
    boolean booleanData;
};

typedef sequence<DataValue> Results;
typedef sequence<DataValue> Arguments; 


interface CPULoad {
  void getLoadAvgs(out float oneMinAvg, out float fiveMinAvg,
                   out float tenMinAvg);
  void invokeOperation(in string operation, in Arguments args,
                       out Results res);
}; 



Following the DataType specification is the DataValue union, a union of all possible data types (as specified by DataType). Unions in CORBA are different from unions in C/C++ in that only one field of the union is active at any one time. The selection of which field is active is based on a discriminator. As can been seen, the CORBA IDL uses a switch-like syntax to specify the discriminator's type. How to select which field is active is based on the value of the discriminator. For DataValue, the discriminator's type is DataType, and it is used to select the field that corresponds to one of the possible values of DataType.

Following the union specification, we create two new data types, Arguments and Results, both of which are a sequence of DataValues. These new types actually are unbounded sequences, because we have not specified the maximum length of the sequence. If we had wanted to create a bounded (fixed length) sequence, we would have specified the maximum length of the sequence following the sequence's data type, for example, sequence<DataValue, 100>.

Finally, we use these two new data types to extend the CPULoad interface from the Part I article to include a new method, invokeOperation(). The goal of this method is to act as a generic interface method for the CPULoad interface. Clients can invoke a variety of different operations by specifying the name (operation) of the operation they wish to invoke, along with a sequence of arguments for that specific operation (args). Upon completion, the results of the operation is returned (res) as another sequence of DataValues.

Notice here that generic methods, such a invokeOperation(), have a major drawback in that they circumvent the compile time, data type checking. However, situations arise in which such methods can be useful. For example, the code shown in these examples was derived from work I did that involved the use of a central dispatcher required to dispatch jobs to different servers based upon the operation specified, as well as other aspects, such as queue lengths. Using a generic method such as invokeOperation() allowed the dispatcher to be written such that it needed to know only which operations a server could handle, not the operation's calling conventions.

Now, let's look at how invokeOperation() is implemented. Listing 2 contains the source code for invokeOperation(). The first action taken by this method is to get the current load values for the system. This is accomplished by invoking getLoadAvgs(); see Part I.

Listing 2. invokeOperation()




void
CPULoad_impl::
invokeOperation(const char* operation,
		const Arguments& args,
		Results_out results)
  throw(CORBA::SystemException)
{
  (void)args;

  static const string getOneMinAvg("getOneMinAvg");
  static const string getFiveMinAvg("getFiveMinAvg");
  static const string getTenMinAvg("getTenMinAvg");
  static const string getAllAvgs("getAllAvgs");

  CORBA::Float oneMinAvg, fiveMinAvg, tenMinAvg;

  getLoadAvgs(oneMinAvg, fiveMinAvg, tenMinAvg);

  results = new Results(3);

  if (operation == getOneMinAvg) {
    results->length(1);
    (*results)[0].floatData(oneMinAvg);
  } else if (operation == getFiveMinAvg) {
    results->length(1);
    (*results)[0].floatData(fiveMinAvg);
  } else if (operation == getTenMinAvg) {
    results->length(1);
    (*results)[0].floatData(tenMinAvg);
  } else if (operation == getAllAvgs) {
    results->length(3);
    (*results)[0].floatData(oneMinAvg);
    (*results)[1].floatData(fiveMinAvg);
    (*results)[2].floatData(tenMinAvg);
  } else {
    cerr << "Unknown operation specified: " << operation;
    throw 0;
  }
}


The second action taken is the creation of the Results sequence. The pointer to the new object is assigned to the variable results. The value specified when creating the Results object is the starting maximum length of the sequence. This value provides the constructor for the new object with additional information; therefore, the constructor can allocate efficiently the memory required to support a sequence of that length.

Once the current load has been obtained, invokeOperation() can create and return the proper results, depending on the operation specified. As the code shows, invokeOperation() does this by comparing the operation to a sequence of strings and, depending on which string is specified, taking the proper action. The requested load information is returned to the client in the results argument.

The Result_out data type is a reference to a pointer to an object of type Results. For those readers not familiar with C++ references, a reference basically is a compiled time name that refers to the same data that another name refers to. If a function's parameter is specified as a reference, then that parameter refers to the same data that the argument passed into that function refers to. This is similar to specifying in C that a parameter is a pointer and then dereferencing that pointer within the function. The main difference is there is no need to dereference a reference parameter.

Regardless of which operation was specified (except for the case in which an unknown operation was specified), two actions are taken. The first action sets the actual length of the sequence, while the second action initializes the sequence.

The length of the sequence is set using the sequence's length() method. Once must be careful in setting this value as it indicates the actual length of the sequence, that is, the number of valid entries in the sequence. This number is used by the underlying transport layers to determine the actual number of bytes that need to be transferred when the sequence is passed between objects.

Once the length is specified, the sequence is initialized by using the sequence's [] operator. The syntax used for initializing can be somewhat tricky to understand if one forgets that results actually is a pointer to an object. Thus, before the [] operator can be invoked, we need to dereference the pointer using *. Given the precedence rules, we force the dereferencing by surrounding the *results with parentheses. Once the pointer has been dereferenced, we can invoke the [] operator to select the specific sequence element.

Keep in mind that each element in the sequence is a union object. In order to set the value of a union object, one must activate the field to be set and then provide the value used to set that field. In order to active a field, one sets the object's discriminator using one of the object's discriminator methods. A union object has one such method for each field specified. For example, for the DataValue's floatData field, a floatData() method was created. Invoking this method sets the objects determinator to the correct value, in this case, DT_FLOAT.

The determinator methods are overloaded methods. If they are invoked with a value, then the object's specified field is set to that value. If they are invoked without a value, then the current value is returned. Given that we are setting the object's value, we invoke floatData() with the value we want to set the object to--the current processor load.

Now that we have looked at how invokeOperation() functions, we can look at how the client invokes it. Listing 3 contains the source code for the getLoadData() function that is part of the client application. It is in this function that invokeOperation() is called and the results are processed. The first action this function takes is the creation of two _var variables, args and results. Given that invokeOperation() does not expect any arguments, we are free to pass it a 0-length sequence, which is what we get by creating args using the default Argument constructor.

Listing 3. getLoadData()




void
getLoadData(CPULoad_var& cpuLoad,
	    const string command,
            unsigned int length,
            float averages[])
{
  Arguments_var args = new Arguments;
  Results_var results;

  cpuLoad->invokeOperation(command.c_str(), args.in(),
                           results.out());

  if (results->length() != length) {
    ostringstream message;
    message << "Invalid results: Length != " << length
            << " (length = " << results->length() << ")";
    throw runtime_error(message.str());
  }

  for (unsigned int i = 0; i < length; ++i)
    if ((*results)[i]._d() != DT_FLOAT)
      throw runtime_error("Invalid data type for results");

  for (unsigned int i = 0; i < length; ++i)
    averages[i] = (*results)[i].floatData();
}


When we execute invokeOperation(), we pass to it the string specifying the operation we want executed along with the two sequences, args and results. For those not familiar with the C++ string class, the c_str() method simply returns a const char pointer. We use the in() method with args because we are providing the arguments to invokeOperation(); we use the out() method because we are retrieving the results from invokeOperation().

Calling invokeOperation() as shown causes the desired load information to be stored in the results sequence. In order to be sure the correct action was taken, we check that the results sequence contains the correct type of data. The first check we perform is to make sure the length of the sequence is correct. Next, we check that each element in the sequence is of the right type. Remember, results is a sequence of union objects. Thus, we use the discriminator access method, _d(), to determine the element's type. As you can see, we check to see if the element is of type DT_FLOAT.

Finally, we return the load data to the calling function. We have chosen to copy the values into a float array, as opposed to returning the sequence, in order to demonstrate that it is easy to isolate the CORBA specific components of an application. One easily could wrap all of the CORBA-related functionality within a set of wrapper functions, like we did with getLoadData(), and the remaining application code would never need to know that it was a CORBA-based client.

We conclude our look at some of the advanced CORBA data structures at this point and turn our attention to the world of CORBA services. Readers are encouraged to look further into the CORBA data structures in order to appreciate fully the CORBA data structure model.

______________________

White Paper
Linux Management with Red Hat Satellite: Measuring Business Impact and ROI

Linux has become a key foundation for supporting today's rapidly growing IT environments. Linux is being used to deploy business applications and databases, trading on its reputation as a low-cost operating environment. For many IT organizations, Linux is a mainstay for deploying Web servers and has evolved from handling basic file, print, and utility workloads to running mission-critical applications and databases, physically, virtually, and in the cloud. As Linux grows in importance in terms of value to the business, managing Linux environments to high standards of service quality — availability, security, and performance — becomes an essential requirement for business success.

Learn More

Sponsored by Red Hat

White Paper
Private PaaS for the Agile Enterprise

If you already use virtualized infrastructure, you are well on your way to leveraging the power of the cloud. Virtualization offers the promise of limitless resources, but how do you manage that scalability when your DevOps team doesn’t scale? In today’s hypercompetitive markets, fast results can make a difference between leading the pack vs. obsolescence. Organizations need more benefits from cloud computing than just raw resources. They need agility, flexibility, convenience, ROI, and control.

Stackato private Platform-as-a-Service technology from ActiveState extends your private cloud infrastructure by creating a private PaaS to provide on-demand availability, flexibility, control, and ultimately, faster time-to-market for your enterprise.

Learn More

Sponsored by ActiveState