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

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 developed 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: The implementation for the class is given below: The following client program is dynamically linked to the class. 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: 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.



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: (*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: 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: 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. 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 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: During the initialization of a class object (e.g., when new  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. 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.
 * 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.



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: This, in turn, creates the run-time picture shown in Figure 4.3.



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: The AClassData will look like the following: 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++.



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. 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) 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:
 * 2) include 

#ifdef _IBMC_ #pragma linkage(SOMinitModule,system) #endif SOMEXTERN void SOMLINK SOMinitModule(long majorVersion,                                   long minorVersion,                                    string className)
 * 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:

The initialization function generally invokes 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) ; }

icc /c+ / Ge- /1. cat.idl
 * 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.

sc -sdef 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.

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 ClassData and CClassData are external data structures referenced by the SOM bindings. The symbol 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 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.
 * 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.

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. If the 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


 * 1) include 
 * 2) include 
 * 3) include 

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 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 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 specifying ClassData.classObject.

The following example illustrates the above points. Consider the following interfaces: A client program, which calculates the salary for an employee, might be written as follows: 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.