Initializers and Destructors in SOM

From EDM2
Jump to: navigation, search

by Dr. Scott H. Danforth

An exciting recent development for IBM's System Object Model (SOM) is use of the SOM API to implement C++. The Metaware DirectToSOM C++ compiler is currently in beta testing, and other vendors including IBM are developing similar compilers. These compilers let classes programmed in C++ become SOM classes that are useful to other languages from backwards-compatible binary class libraries.

To support these compilers, IBM has made a number of extensions to the SOM API using an enhanced SOM kernel named ESOM. ESOM includes new methods in SOMObject and SOMClass whose purpose is support for C++-style constructors and destructors. Although ESOM is not an official IBM product, it is available through the SOM Support forums, under the name CSD202 for evaluation and testing. ESOM represents IBM's continuing investment in and development of SOM object technology.

Note: Information about the SOMObjects products and forums is available from Nancy Berlin at 1-512-838-3401 or by calling 1-800-342-6672. Try out these features and provide feedback that might influence future SOM product releases. The ESOM functions are not supported as part of the official IBM SOMObjects product.

Initializers in SOM

Before describing the features of the ESOM, let's review how objects are initialized in the SOMObjects Version 2.0 product.

Object initialization is a separate activity from object creation. In SOM, object creation is the act that enables the execution of methods on an object. In SOM, this means storing a pointer to a method table into a word of memory. This single act converts raw memory into an uninitialized SOM object that starts at the location of the method table pointer. But, the instance variables encapsulated by an object must be brought into some consistent state before general use of a newly-created object makes sense. This is the purpose of initialization, which is supported by an object's methods.

In general, every ancestor of an object's class contributes instance data to an object; therefore, it is appropriate that each of these ancestors contribute to the initialization of the object. In SOM, initializer methods chain parent-initializer calls upwards. This allows initializer method procedures contributed by all ancestors of an object's class to execute. This chaining is not supported in any special way by the SOM API. Parent-method calls simply are one of the possible idioms available to users of object-oriented programming in SOM.

SOM does not constrain initialization to be done in any particular way or require that any particular ordering of ancestor class's method procedures be used. But, SOM does provide an overall framework that class designers easily can tie into to implement default initialization of SOM objects. The somInit method provides this framework. If you want to provide initialization for instance variables introduced by a class, you just override somInit with a method procedure that first makes parent-method calls and then performs the desired initialization.

Because it takes no arguments, somInit serves the purpose of a default initializer. You also can introduce additional non-default initialization methods that take arguments. Or, you can introduce new class methods as object constructors that first create an object--generally using somNewNoInit. Then, invoke a non-default initializer on the new object.

Initializers in ESOM

ESOM makes no major changes to the SOM framework. All code written using the previously-described approach continues to be fully supported and useful within ESOM. ESOM simply adds a few new capabilities to SOM. In the context of initializers (we'll discuss destructors later), ESOM contains 2 important enhancements:

  • ESOM recognizes initializers as a special kind of method and supports a special mechanism for ordering the execution of ancestor initializer method procedures. This ordering is based on information provided in the SOM IDL implementation section for classes.
  • ESOM introduces a new default initializer method that uses this execution mechanism, and lets you define additional initializers. ESOM language bindings provide special support for methods declared as initializers.

To support these enhancements, two new SOM IDL modifiers are supported by ESOM: directinitclasses and init. The directinitclasses modifier controls the execution order of initializer method procedures provided by the different ancestors of the class of an object. The init modifier indicates that a given method is an initializer (that it both uses and supports the new ESOM object initialization protocol).

In ESOM, every class has a somDirectInitClasses attribute (introduced by SOMClass). For any given class, this is the sequence of ancestor classes whose initializer method procedures the given class wants to invoke directly. The default (when the class's IDL doesn't specify directinitclasses) is simply the class's parents - in left-to-right order. Using this information and the actual runtime class hierarchy above them, all classes in ESOM inherit from SOMClass the ability to create a data structure called somInitCtrl. This structure represents a particular visit ordering that reaches each class in the transitive closure of somDirectInitClasses exactly once. The ordering is similar to the ordering in C++. When initializing a given object of some specific class, recursively, for each class whose initializer method procedure should be run, first the initializer method procedures of all of the class's somDirectInitClasses are run (when they have not already been run by other class's initializers), in left-to-right order.

Figure 1 shows this inheritance hierarchy, along with the ordering produced when each class uses its parents as its directinitclasses (when an instance of the class numbered 7 is initialized).

InitDest-Dom-Fig-1.gif

Figure 1. A Default Initializer Ordering

The somInitCtrl data structure mentioned previously is used by the individual initializer method procedures in the ancestor classes to guide their actions, so the required initialization ordering is used. For example, this structure is what tells node 6 in Figure 1 not to run the initializer code of node 3 (because it has already been executed). The code that uses the somInitCtrl structure is not visible to the class implementor. ESOM users can find this code in the implementation bindings if they are curious.

All initializers take a somInitCtrl data structure as an initial parameter. Also, all initializers return void. Here is the IDL for the new ESOM default object initializer.

void somDefaultInit(inout somInitCtrl ctrl);

Implementing Classes with Initializers

You can easily declare and implement new initializers. Classes can have as many initializers as desired, and subclassers can easily invoke whichever of these that they want from their initializer methods. The initializers available for a given class include somDefaultInit, plus any new initializer methods that the class declares using IDL.

Initializers are declared as in the following IDL example:

interface Example1 : ClassA, ClassB {
    void withName(inout somInitCtrl ctrl, in string name);
    void withXY(inout somInitCtrl ctrl, in long x, in long y);
    implementation {
        withName: init;
        withXY: init;
    };
};

When initializers are introduced by a class, as illustrated in the previous example, the ESOM C and C++ implementation template emitters automatically generate an appropriate default implementation for the corresponding initializer method procedures. A default initializer method procedure differs from those provided for other methods. In particular, the body of an initializer method procedure consists of two main sections. The first section includes calls to ancestors of the class to invoke their initializers, and the second section is where you perform the local initializations appropriate to the class being defined.

For example, here is a generated template for the withName method procedure declared in the previous example:

SOM_Scope void SOMLINK withName(Example1 *somSelf,
                                somInitCtrl* ctrl,
                                string name)
{
    Example1_BeginInitializer_withName;
    Example1_Init_ClassA_somDefaultInit(somSelf, ctrl);
    Example1_Init_ClassB_somDefaultInit(somSelf, ctrl);

    /*
     * Local Example1 initialization code added by programmer
     */
}

Initially, the template calls the default ancestor initializers provided by somDefaultInit. But, you can replace any of these calls with a different initializer call (as long as it is to the same ancestor). Macros for this purpose are predefined by the implementation bindings. For example, if ClassA has an initializer named withZork that takes two integers, then the ancestor initializer call to ClassA in the template could be replaced with:

    Example1_Init_ClassA_withZork(somSelf, ctrl, 123, 456);

Using Initializers

If you want to construct an instance of a class supported by a particular initializer, you can use the basic SOM API directly. To do this, first invoke somNewNoInit on the class object to create an instance; then invoke the desired initializer on the new object, passing a NULL control argument in addition to whatever other arguments needed by the initializer. For example, to construct an instance of Example1 using the withName initializer, write:

Example1 *ex = _Example1->somNewNoInit();
ex->withName(0,'a string');

In addition, you always can invoke the general object constructor somNew on a class to create and initialize objects. This creates an object and then invokes somDefaultInit on it.

Usage bindings can hide details associated with initializer use in various ways. For example, the SOMObjects Toolkit C usage bindings already provide a convenience macro Example1New() that first assures existence of the class object, and then calls somNew on it to create and initialize a new object. C macros that invoke non-default initializers could also be provided. For example, Example1New_withString('a string').

The ESOM C++ bindings represent initializers as overloaded constructors. The result is that C++ users would be able to construct an Example1 object using any of the following expressions:

new Example1;
new Example1();
new Example1('a string');
new Example1(123,456);

Destructors

The purpose of a destructor is to uninitialize an object before its storage is freed. This is an important consideration because it allows freeing storage not contained within the body of the object. Destructors in ESOM operate much the same as initializers. They take a control argument (in this case, of type somDestructCtrl) and are supported by ESOM template emitters. Destructors execute in exactly the reverse order of initializers.

You only need a single destructor method, so SOMObject introduces the method that provides this function: somDestruct. As with the default initializer method, if you have nothing special to do during uninititalization, there is no need to use somDestruct. On the other hand, if you do need to perform uninitialization of instance data introduced by your class, you should override somDestruct and place the necessary uninitialization code in the generated template.

Legacy Class Support

ESOM still supports somInit and somUninit. For example, default initialization can still be defined by overriding somInit (instead of somDefaultInit). Likewise, you can use somUninit instead of somDestruct. This is less efficient, so it is not recommended as a programming style, but it is the basis for supporting old code.

Conclusion

ESOM has enhanced the SOM API to provide support for C++-style initialization of SOM objects. This was done to support the requirements of compilers that directly map C++ to the SOM API. These new capabilities are also made available to SOMobjects Toolkit users by appropriate support from language binding emitters. Thus, ESOM programmers can declare initializers and can easily chain initializer calls upwards. For C++ users, initializers simply appear as C++ constructors.

ESOM disables somInit/Uninit parent-call chains and replaces these with initialization/destruction chains that guarantee no class's initialization/destruction code is called more than once. This is an important consideration when multiple inheritance is supported, and SOM's earlier approach to initialization based on parent-method calls required programmers to deal with this problem. Finally, to support legacy classes, these enhancements are provided in a way that lets you continue to use all current and future somInit and somUninit code.

About the Author - Scott Danforth

Scott Danforth received his Ph.D. in Computer Science from the University of North Carolina at Chapel Hill in 1983. His current assignment with IBM is developing language-neutral object-technology for binary-class libraries, and developing system-level frameworks for object-oriented programming. His published work includes journal and conference articles devoted to the theory and practice of object-oriented programming.