Object-Oriented Programming Using SOM and DSOM/A Complement to C++

From EDM2
Jump to: navigation, search

If you are asking yourself, "Why SOM instead of C++?", you are not alone. This is one of the most frequently asked questions. In this chapter, we will attempt to answer this question by looking at some of the problems with C++, and how SOM solves these problems. In the process, we will introduce a few key features of SOM and show you how to use them.

The Need To Re-Compile

Dynamic Link Libraries (DLLs) have become the standard way for packaging and distributing software on OS/2 and other systems. DLLs allow library functions to change without requiring the programs that use the DLLs to recompile or to relink, as long as the interface to the library functions remains unchanged This is because the code in the DLL is not linked to the user's program until runtime. Therefore, it is possible to replace the DLL without affecting any user program.

DLLs work very well in maintaining binary compatibility, as long as the libraries are written in a procedure-based language. However, as we will see in the following example , this paradigm fails when libraries are de veloped in an object oriented language like C++. The user of a C++ class has to recompile the source code whenever there are changes to the class header file. The changes can be as simple as adding an instance variable to the class, adding a method to the class or relocating the class in the hierarchy.

Consider the following class interface:

class A
{
  public :
       short val1;
       long val2;

       A(short, long) ;
       void display() ;
};

The implementation for the class is given below:

#include <iostream.h>
#include "A.hpp"

A :: A( short x, long y)
 : val1 (x),
   val2(y)
{}

 void A :: display()
 {
    cout <<"The values are ";
    cout << val1 << •" << vat2 << "\n";
 }

The following client program is dynamically linked to the class.

#include "A.hpp"

main()
 {
     A my0bj(5, 100);

     myObj.display();
 }

When you run the program, you get the following:

 > The values are 5 100

Later, we modify the class slightly to introduce a new instance variable. The new class interface is given below:

class A
{
  public:
      short val1;
      char *val3;
      long val2;
      
      A(short, long);
      void display() ;
};

The interface and implementation for the class constructor and the display method remain unchanged. We replaced the original class DLL with this new DLL and rerun the client program again. This time we get the same output,

> The values are 5 100

but the program crashes!

So why did this happen? Let's look at the memory layout of the object before and after the change. This is shown in Figure 4.1.

With the new object layout, when the client program assigns 100 to va/2, it was assigned to the memory location of val3, which is not expecting a long value. This caused the crash.

To avoid the crash, the client program must be recompiled with the new class header file, even though the change to the class does not require any code change in the client program.

OOP-SOM-Fig-4 1.png

Figure 4.1 Memory layout of the C++ object before and after the change.

Although this example is trivial, and the crash can be avoided with some careful programming, it demonstrates the problem with changing class header files in C++. Imagine yourself purchasing a class library (DLL) that was developed in C++, and you have different applications using this library. Whenever there is a new release of the class library, you need to recompile all of your existing applications, because it is likely that a new release will have new header file changes. In some cases, this might not be possible, as the source code for these applications is not available. And you end up having to manage multiple releases of the same class library.

In the next section, we show how SOM resolves this problem by using the releaseorder modifier.

SOM Releaseorder

A major contribution of SOM is that SOM classes can undergo structural changes without requiring the client programs to recompile, if the structural changes do not require source code changes in the client programs. To accomplish this, the implementor for the class must fill in the releaseorder modifier for the class, following the rules described in Section 2.4.5 on page 17.

We rewrite the class interface for A in IDL. The instance variables vall and val2 are mapped to attributes • in the IDL. The _get and the _set method for each attribute are added to the releaseorder. The IDL for A is shown below:

#include <somobj.idl>
interface A : SOMObject
{ 
    attribute short val1;
    attribute long val2;

    void display();

    #ifdef _SOMIDL_
    implementation
    {
    releaseorder : display,
                   _get_val1, _set_val1,
                   _get_val2, _set_val2;
     };
     #endif
 };

(*Recall that only attributes can be accessed by client programs. If you mapped ua11 and ua12 to instance variables in the implementation section, they will not be accessible to client programs.)

The implementation for the class is shown below:

 #define A_Class_ Source
 #include <a.xih>
 #include <iostream.h>

 SOM_Scope void SOMLINK display(A •somSelf, Environment *ev}
{
    AData •somThis = AGetData(somSelf);
    AMethodDebug("A ", "display'1;
 
    cout << "The values are ";
    cout << somThis->val1 << " • << somThis->val2 << "\n";
}

The class is packaged into a DLL. The details of this process are discussed in Section 4.2 on page 78. The client program is modified to use the SOM class instead of the C++ class:

#include "a.xh"

main(}
{
    Environment *ev;
    A *myObj;

    ev = somGetGiobaiEnvironment();
    myObj = new A;
  
    myObj-> _set_val1 (ev,5};
    myObj-> _set_ val2(ev, 1 00) ;

    myObj ->dlsplay(ev} ;
}

As before, when you run the program, you get:

> The values are 5 100

We now modify the class IDL to introduce a new attribute. The get and set methods for the new attribute are added to the end of the releaseorder list.

#include <somobj.idl>
interface A : SOMObject
{
   attribute short val1;
   attribute string val3;
   attribute long val2;

   void display();

   #ifdef _SOMIDL_
   implementation
   {
       releaseorder : display,
                      _get_val1, _set_val1,
                      _get_val2, _set_val2,
                      _get_val3, _set_val3;
   };
   #endif
};

We recompile the class implementation and replace the original DLL with the new DLL. When we rerun the client program, we get the following:

 > The values are 5 100

We have changed our class definition successfully, without affecting our client program.

So What Is the SOM Magic?

Simply put, the magic is based on levels of indirection. To understand how it works, we need to look at the SOM run-time data structures, and how a method is invoked.

In SOM, every class has a global data structure named <className>ClassData. This data structure is provided in the usage binding of the class. It consists of a pointer to the class object, followed by a series of "method tokens". A method token can be thought of as a value that identifies a method. There is a method token for every method that is introduced by this class. These method tokens are ordered in the same order as the method names in the releaseorder.

For example, the ClassData structure for the original class A looks like the following:

SOMEXTERN struct AClassDataStructure {
   SOMClass *classObject;
   somMToken display;
   somMToken _get_val1;
   somMToken _set_val1;
   somMToken _get_val2;
   somMToken _set_val2;
} AClassData;

During the initialization of a class object (e.g., when new <className> is called), SOM builds the method table for the class and sets up the offset of the method tokens. A method table is a table of pointers to the procedures (body of code) that implements the methods. This is shown in Figure 4.2.

When a method is invoked, e.g., myObj->display(eu), the SOM run-time performs the following steps.

  1. Using the class name and the method name, constructs the method token for the specified method.
  2. Using the method token, obtains a pointer to the procedure that implements the specified method.
  3. Invokes the body of code using this pointer.

The SOM_Resolve macro performs steps one and two. If you look at the usage binding that is generated for a method in a class, you will see that this is exactly what it is doing.

OOP-SOM-Fig-4 2.png

Figure 4.2 Initializing method tokens

Now what happens when we add the attribute val3? Recall that we added its _get and _set methods to the end of the releaseorder. This causes the AClassData to look like the following:

SOMEXTERN struct ACiassDataStructure {
    SOMClass *classObject;
    somMToken display;
    somMToken _get_val1;
    somMToken _set_val1;
    somMToken _get_val2;
    somMToken _set_val2;
    somMToken _get_val3;
    somMToken _set_val3;
} AClassData;

This, in turn, creates the run-time picture shown in Figure 4.3.

OOP-SOM-Fig-4 3.png

Figure 4.3 Method tokens after val3 added with proper releaseorder

Now you can see why the client program still works after we change the class structure. Since we added the methods to the end of the releaseorder, the method tokens for the _set_vall, _set_val2 and the display methods still resolve to the same procedure address. Therefore binary compatibility can be preserved. Consider what will happen if you reorder the releaseorder, to the following:

#ifdef _SOMIDL_
implementation
{
   releaseorder : display ,
                  _get_val2, _set_val2,
                  _get_val1, _set_val1,
                  _get_val3, _set_val3;
};
#end if

The AClassData will look like the following:

SOMEXTERN struct AClassDataStructure {
     SOMCiass *classObject;
     somMToken display;
     somMToken _get_val2;
     somMToken _set_val2;
     somMToken _get_val1;
     somMToken _set_val1;
     somMToken _get_val3;
     somMToken _set_val3;
} AClassData;

which creates the run-time picture shown in Figure 4.4.

Because the client has not recompiled to pick up the new AClassData, when it invokes _set_vall, it will be calling the wrong procedure. It is using mToken3 which resolves to proc5, instead of proc3. In other words, binary compatibility is broken, and the client will need to recompile the source code.

Export Entry in DLLs

To build a DLL on OS/2, you must create a module definition (.DEF) file. If you want a function to be available to programs that call the DLL, you must export the function by listing its name under the EXPORTS keyword in the .DEF file. This can become tedious if your DLL is written in C++.

OOP-SOM-Fig-4 4.png

Figure 4.4 Reorder releaseorder

First, you must list the mangled name of the function. To figure out the mangled name of the function you want to export, you need to run the utility CPPFILT that comes with the C Set++ compiler. This will extract all the names from your object files. You then copy the ones you want to export into your .DEF file. Needless to say, this can be an error-prone endeavour.

Second, the number of export entries for a class may be huge, if the class contains a large number of member functions. Even private or protected member functions will need to be exported if they are used by an exported function.

By contrast, SOM requires no more than three export entries per class, and each export entry follows a naming convention. In the following, we will show you step-by-step how to build a DLL for SOM classes.

Creating a DLL for SOM Classes

In this example, we create the DLL domestic to contain two SOM classes: Cat and Dog.

  1. For each class in the DLL, specify the DLL name in the class's IDL file. The DLL name is specified by setting the dllname modifier in the implementation section of the IDL. The Cat.idl is given below:
#include <somobj.idl>
interface Cat : SOMObject
{
     attribute string name;

     void display() ;      // Print the string : "I am a Cat"

     #ifdef _ SOMIDL_
     implementation
     {
          releaseorder : display,
                       _get_name, _set_name;
          dllname = " domestic.dll";
      };
      #end if
};
  1. Create a initialization function for this DLL. The initialization function is called by SOM whenever it loads a class library. It must follow the following function prototype:
 #ifdef _IBMC_
     #pragma linkage(SOMinitModule,system)
 #endif

 SOMEXTERN void SOMLINK SOMinitModule(long majorVersion,
                                   long minorVersion ,
                                   string className)

The initialization function generally invokes <className>NewClass for each class in the class library to create the class objects. The initialization function (initfunc.cpp) for the Cat and Dog classes in the class library domestic is shown below:

 #include "cat.xh"
 #include "dog.xh "

 #ifdef_IBMC_
        #pragma linkage(SOMinitModule,system)

 #end if

 SOMEXTERN void SOMLINK SOMinitModule(long majorVersion ,
                                      long minorVersion,
                                      string className)
{
         SOM_IgnoreWarning(majorVersion);
         SOM_IgnoreWarning(minorVersion);
         SOM_IgnoreWarning(className);

         CatNewCiass(Cat_MajorVersion , Cat_MinorVersion);
         DogNewCiass(Dog_ MajorVersion , Dog_MinorVersion) ;
 }
  1. Compile all the implementation files for the classes and the initialization function. The C Set++ compiler requires the compiler option /Ge- to be specified when compiling a DLL file. For example, the following command compiles the cat. idl file.
icc /c+ / Ge- /1. cat.idl
  1. Create a .DEF file for the DLL. This can be done in two steps. First, run the de{emitter from the SOM compiler to generate the necessary exported symbols for each class. Second, combine the exported symbols into one .DEF file. For example, the following command generates the cat.def file.
sc -sdef cat.idl

The cat.def file is listed below:

 ; This file was generated by the SOM Compiler.
 ; FileName: cat.def.
 ; Generated using :
 ; SOM Precompiler somipc: somc/smemit.c
 ; SOM Emitter emitdef : somc/smmain .c
 LIBRARY cat INITINSTANCE
 DESCRIPTION 'Cat Class Library '
 PROTMODE
 DATA MULTIPLE NONSHARED LOADONCALL
 EXPORTS
 CatCClassData
 CatCiassData
 CatNewClass

Notice that three export symbols are generated. This is the advantage we described earlier. Symbols <className>ClassData and <dassName>CClassData are external data structures referenced by the SOM bindings. The symbol <className>NewClass is the name of the function used to create the class.

To produce the .DEF file for our domestic DLL, we combine the cat.def and the dog.def files that are generated from the Cat and the Dog IDL. We also need to include the initialization function, SOMinitModule, in the export list. The final domestic.deffile is given below:

LIBRARY domestic INITINSTANCE
DESCRIPTION 'Domestic Animal Class Library'
PROTMODE
DATA MULTIPLE NONSHARED LOADONCALL
EXPORTS
SOMinitModule
CatCClassData
CatClassData
CatNewCiass
DogCClassData
DogClassData
DogNewClass
  1. Link the object files and .DEF file into a DLL. You can use icc to invoke the compiler and the linker. For example, you can use the following command to create domestic.dll.
icc /Fe"domestic.dll" cat.obj dog.obj initfunc.obj domestic.def os2386.1ib somtk.lib

We will demonstrate the use of the DLL domestic in the next example.

Dynamic Class Loading

Suppose you are building a graphical user interface (GUI) builder that supports reuseable parts. You might have a push-button control, a list-box control, or some other user-defined controls. Each of these controls are implemented as objects that exhibit different behavior. A typical user will ask the GUI builder to create controls and use them in the applications. Since not all the controls are predefined at compile time, it will be hard to use C++ to implement such a system because C++ requires the class name to be known at compile time. What we need is a system that supports the dynamic loading of classes that are unknown at compile time.

SOM provides methods that let you dynamically load a class and create a class object when the name of the class is unknown at compile time. This is possible because, in SOM, classes are objects at run-time, as opposed to C++, where classes are types, which are fixed at compile time. The SOMClassMgr class provides two methods for dynamically loading and creating class objects: somFindClslnFile and somFind Class.

The somFindClslnFile Method

The somFindClslnFile method is the more restrictive of the two. It requires the specific name of the DLL in which your class resides. The following shows the syntax for the method in C.

myClass=_somFindCislnFile(SOMCiassMgrObject, //global instance
                          classId,           //somId for the class
                          classMajorVersion, //class major version number
                          classMinorVersion, //class minor version number
                          dllname);          //DLL filename

The parameter SOMClassMgrObject is a pointer to the instance SOMClassMgr. There is only one instance of SOMClassMgr, and it is created during SOM initialization. The parameter classId is a somId that represents the name of the class. It can be obtained by passing the name of the class to the function somldFromString. The parameters classMajorVersion and classMinorVersion are normally set to 0, unless you want to check against the version numbers of the class. The parameter dllname specifies the name of the DLL that contains the class. You must specify the complete pathname for the DLL. The DLL must also be placed in one of the directories specified on the LIBPATH statement in your config.sys file.

The DYNALOAD program shown in Figure 4.5 uses somFindClslnFile to dynamically load the DLL domestic that we built in the previous section. It takes the name of the class as an input parameter. Ifthe class is created, it will invoke the display method on the animal to display its characteristics.

We run the program with different input.

> DYNALOAD Dog
I am a Dog
> DYNALOAD Cat
I am a Cat
> DYNALOAD Bird
Can't load class Bird
#include <iostream.h>
#include <Som.xh>
#include <somcm.xh>

main(int argc, char ·argvQ, char *envpO)
{
  SOMClass *myClass;
  somId classId;
 
  //I Initialize SOM run-time environment
  somEnvironmentNew() ;

  classld = somldFromString( argv[1] );
  myClass = SOMCiassMgrObject->somFindCislnFile(classld,
                             0,0,
                             "C:\\BOOK\\CHAP4\\DYNALOAD\\DOMESTIC.DLL");

  if (myClass)
  {
    SOMObject *myObj;
    Environment *ev;

    ev = somGetGiobaiEnvironment();
    myObJ = myClass->somNew();
    myObj-:>somDispatch( (som Token*)O,
                          somld FromString("display"),
                          myObj,
                          ev) ;
  }
  else
  {
     cout << wcan't load class .<< argv[1] << ''\n";
  }
}

Figure 4.5 The DYNALOAD program

The DYNALOAD program illustrates how one can invoke a method, when the language bindings for a class are not available. Once the class object is created, the som.New method is called to create an instance of the class. The somDispatch method can then be called to invoke a method on the object.

The somFindClass Method

Another method that can be used to create a class object is somFindClass. The somFindClass method is similar to somFindClslnFile, except that you do not have to specify the DLL name. The C syntax for somFindClass is shown below:

myClass = _somFindCiass(SOMClassMgrObject,  //global instance
                        classld,            //somld for the class
                        classMajorVersion,  //class major version number
                        classMinorVersion); //class minor version number

There are a few things to note when using somFindClass. The somFindClass method uses somLocateClassFile to obtain the name of the file that contains the class. The default implementation of somLocateClassFile checks the Interface Repository for the value of the dllname modifier of the class. We discussed the setting of the dllname modifier in Section 4.2.1 on page 79. To populate the Interface Repository, run the SOM compiler with the -u option. The following command shows how you can update the Interface Repository with the Cat and the Dog IDL files.

sc -sir -u cat.idl dog.idl

The Interface Repository is a database that maintains information about classes described in IDL files. It is used by the Distributed, Persistence, and Replication Framework. We will look at some of the programming interfaces in Chapter 8.

Note that if the dllname modifier is not specified, then somLocateClassFile will return the class name as the DLL name. If your class does not reside in a DLL that has the same name, then you will get an error indicating that the class is not found. Therefore, you should always explicitly set the dllname modifier, or follow the convention: the name of the class is the same as the name of the DLL that contains the class.

Run-Time Type Identification

Currently, C++ provides no support for determining the type of an object at runtime. The need for run-time type identification arises in an inheritance hierarchy, when methods can be supported in one derived class, but not in the others. Given a pointer to the base class, it becomes necessary to check the type of the object to make sure that one can safely invoke a particular method.

While run-time type identification can be avoided to a large extent by using virtual functions, there are cases where it is not possible. For example, a user might want to extend the function of a class by subclassing, but cannot add or modify the original class to use virtual functions, because the source code of the original class is not available. This represents a legitimate case of using runtime type identification. Indeed, one of the language extensions that has been accepted by the C++ committee is the run-time type identification proposal by Stroustrup and Lenkov.

SOM provides methods that let you query information about an object at runtime. In particular, the somlsA method lets you determine whether an object is an instance of a given class. You invoke somlsA on a target object, passing the class object that the target object should be tested against, as an input parameter. It returns true, if the object is an instance of the specified class, otherwise it returns false.

How do you get a pointer to a class object that you want to test against? We have already discussed a number of ways. One way is to invoke the <className>NewClass procedure, which creates and returns a pointer to the class object of the specified class. Another way is to use the som.FindClslnFile or the som.FindClass method, if the language bindings for the class are not available at compile time. A third way is to use the ClassData structure that we discussed in Section 4.1.2 on page 75.

Recall that each class has a global data structure, named <className>ClassData. This data structure contains a pointer to the class object, which is built during initialization. Therefore, if you have the usage bindings file for the class, you can obtain a pointer to the class object by specifiying <className>ClassData.classObject.

The following example illustrates the above points. Consider the following interfaces:

interface Employee : SOMObject
{
     short salary();
};
interface Manager : Employee
{
     short bonus ();     // Manager receives bonus on top of base salary
};
interface Programmer : Employee
{
    short overtime ();  // Programmer receives overtime paid
};

A client program, which calculates the salary for an employee, might be written as follows:

calcSalary(Employee ·emp)
{
  SOMClass *mgrCiass;
  Environment •ev = somGetGiobaiEnvironment(};
  /**********************************************
  // Use the ClassData structure to get the pointer to the Programmer class object
  /************************************************
  if (emp->somlsA(ProgrammerCiassData.classObject))
  {
      cout << "Programmer salary: " ;
      cout « (emp->salary(ev) + ((Programmer*)emp)->overtime(ev));
  }

 /*********************************************     
 // Create a Manager class object using ManagerNewClass
 / *****************************************************
  mgrClass = ManagerNewCiass(O,O);
  if ( emp->somlsA(mgrCiass) )
  {
      cout <<"Manager salary: " ;
      cout « (emp->salary(ev) +((Manager·) emp)->bonus(ev));
  }
}

When calcSalary is called with a pointer that points to a Programmer object, the first somisA test will be successful and the overtime method is called on the Programmer object. When calcSalary is called with a pointer that points to a Manager object, the second somlsA will be successful and the bonus method is called.

The preceding example also illustrates two different ways of getting a pointer to a class object. In the :first case, the expression ProgrammerClassData.classObject is used. In the second case, the procedure ManagerNewClass is used.

Summary

Perhaps the problems described in this chapter are not important to you. Perhaps you already have workarounds for them. The intent here is to provide you with additional options if you are trying to solve similar problems.

SOM is not intended to replace existing object-oriented languages such as C++. SOM is not a language. However, it provides additional run-time capability that can be used to supplement C++. It also provides a new technology for packaging class libraries. Your application is likely to have both SOM and C++ objects. This is encouraged as there are capabilities in C++ that are not surfaced in the C++ bindings for SOM. Some of the C++ capabilities that are not available include: passing parameters in the constructor, overloading, and class templates.

Note that the lack of C++ capabilities will be considerably alleviated when the DirectToSOM compiler becomes available. With the DirectToSOM technology, you will be able to compile your C++ objects directly into SOM objects and generate IDL from the corresponding C++ interface. Chapter 10 provides more information on this topic.