And then SOM...
Volume Number: 14 (1998)
Issue Number: 2
Column Tag: develop
And then SOM...
by Éric Simenel
IBM System Object Model (SOM), which was introduced on Mac OS with OpenDoc, has become even more important to developers with the introduction of the Contextual Menus in Mac OS 8. It's not restricted to the development of Contextual Menus plug-ins, though. SOM is also available to all developers and solves a lot of common issues which plague real-life project development, management and maintenance. This article will show how SOM can ease your product development and save your precious time.
Since most developers develop in C++ or other object-oriented languages these days, most of you know the obvious reasons why object-oriented programming is better than the previous models. The main strengths (which are also its definitions) of OOP are encapsulation, polymorphism, and inheritance, all of which are conducive to easy reuse.
That's theory. In real life, we deal with different development environments which, even when they're dealing with the same language, are not always compatible because of implementation choices, and are usually even less compatible when dealing with different languages. Even when staying with the same development environment, it changes over time, and it's always trouble to reintegrate old code in new projects, and sometimes, it's even trouble trying to reopen an old project for maintenance with a current development environment.
So, a usual project these days involves many developers who have different tastes in development environments and languages, old legacy source codes written in different languages under different development environments, and sometimes old legacy binary codes whose source codes are no longer available.
Although SOM is not the universal cure to these problems, it does bring a lot of relief, and even if you're a single developer using only one language, SOM can help you manage your project by breaking it into small easily reusable pieces.
Getting SOM
SOM provides developers with the advantages of both object-oriented programming and shared libraries. The main advantage is that whether you're just using a SOM class from a library or inheriting from it, if you later replace this library with a more recent version, you don't have to recompile or rebuild all the client code. When you stop and think about it, this feat is a real breakthrough from what we've been dealing with until now. The second advantage is SOM's language independence, which allows developers to use their favorite environment and still mesh with other people's code. Due to its rather recent emergence in the Mac OS, the preferred (and only) language used with SOM is C++, but it may change in the future.
This article will cover the basics of SOM encapsulation, polymorphism, and inheritance, the use of a SOM library in an application, and in another SOM library, multiple aspects of versioning, dealing with exceptions, etcetera. This article is aimed at giving you a kick start, covering the basics aspects of SOM and how to use it for fun and profit; it will explain in some places the internal works, in case you're interested, but not everywhere. If you're interested in more knowledge about SOM and its internals, then you should read the manuals provided where SOMObjects(tm) for Mac OS is distributed (for example, on the Mac OS SDK CD). This knowledge is not an absolute need, mostly you can just program by example (and a lot of examples are provided in this article). An alternate title for this article could have been "SOM for C++ developers...". When I refer in this article to the Users Guide, it means the Users Guide found in the documentation folder of SOMObjects.
Throughout this article, I'll use the following example to illustrate the different techniques:
Figure 1. SOM Classes of the example.
Where som_Taxes (encapsulation) is a SOM class used by som_Item (usage of a SOM lib within a SOM lib). som_Solid (inheritance) and som_CarWash (Meta Classes) both inherit from som_Item. som_Car (exception handling) inherits from som_Solid, som_Tires (multiple inheritance) multiple inherits from som_Solid and som_Attr, and som_Tapes (versionning) inherits from som_Solid but does interesting things when som_Item v1.1 is present.
Each class is there to illustrate one interesting point at a time (to prevent confusion of the issues). To understand the example, you should note that I have assumed that we're located in a country where products are submitted to sales tax but services are not.
I've been using the Direct-To-SOM capabilities of MetroWerks CodeWarrior MW C/C++ (CodeWarrior Pro release). I verified that MPW MrCpp also has the same capabilities, but I don't cover its usage in this article. I also verified that you can build 68K SOM libraries, but do not cover that in this article either as there are only minor differences in the project settings.
Throughout this article, all the listings have been purged of irrelevant (for this article) lines, such as debugging information, to hilite the more interesting parts. Look at the real code provided with this article to see the complete sources.
Encapsulation
Let's say that you have old legacy source or binary code. Each time you want to use it in a new project it may be a pain to integrate, because things change over time. If it's binary code, it may be 68K code, for which you have to construct UniversalProcPtrs. If it's source code, it may be in a different language than the one you're currently using. Currently, if that code is callable from C/C++, then you can encapsulate it in 1 or more SOM classes distributed in 1 or more SOM shared libraries. In my example, that would be the som_Taxes SOM class.
The main advantage of encapsulation in a SOM wrapper is to enable you to use this old legacy code in all your new projects, with a nice interface. And, eventually, if you decide to rewrite all or part of it, all the projects which have been using this code won't have to be rebuilt to continue to work. The SOM wrapper really isolates the interface from the implementation. Whereas, if you were to keep this code the way it is, with maybe just a nice new set of headers to be able to use it easily in new projects, and if you decide later to modify all or part of it, then you would have to rebuild everything.
The costs involved are not only a development issue but also a sales issue, in the first case, since we're dealing with shared libraries, you just have to ship your customers the new version of the SOM library containing the old legacy code, now updated. In the second case, after having rebuild everything, you have to ship everything.
Creating a SOM library is pretty straightforward. Without the Direct-To-SOM capabilities of the most recent C++ compilers, we would have had to write first an .idl file, then go through MPW somipc to generate the .xh, .xih and .cpp files, and then modify the .cpp file according to what had to be done. With the Direct-To-SOM capabilities, it's much quicker: we simply write a .hh header file (.hh is just a style convention to differentiate them from simple .h header files, but there are no other differences) and the corresponding .cpp file. As a reminder, let's take a look at the .idl we would have written for straight SOM:
Listing 1. som_Taxes.idl
#include <somobj.idl>
module CalcTaxes
{
interface som_Taxes : SOMObject
{
long CalcTheTax(in long value, in short kind);
#ifdef __SOMIDL__
implementation
{
majorversion = 1;
minorversion = 0;
functionprefix = som_Taxes__;
override: somInit;
releaseorder: CalcTheTax;
long tax;
};
#endif //__SOMIDL__
};
};
Compare this with the .hh we are writing for Direct-To-SOM compilation:
Listing 2. d2som_Taxes.hh
#include <somobj.hh>
class CalcTaxes_som_Taxes : public virtual SOMObject {
public:
CalcTaxes_som_Taxes();
virtual ~CalcTaxes_som_Taxes();
virtual long CalcTheTax(INOUT Environment *ev, IN long value, IN short kind);
private:
long fTax;
#if __SOM_ENABLED__
#pragma SOMReleaseOrder (CalcTheTax)
#pragma SOMClassVersion (CalcTaxes_som_Taxes, 1, 0)
#pragma SOMCallStyle IDL
#endif
};
Comparing both .idl and .hh, the meaning of the SOMReleaseOrder and SOMClassVersion pragmas are pretty much obvious. The SOMCallStyle pragma can take 2 arguments, IDL and OIDL. If we want to use the exceptions mechanism (more on that later in this article), it is imperative we use the IDL argument, so it's a good idea to get into this habit. This means that each method must have Environment *ev as its first parameter.
The first difference between a SOM class and a straight C++ class is that the constructor (ie. CalcTaxes_som_Taxes() in this example) for a SOM class can't have any arguments (we'll see later in this articles how to use an equivalent mechanism with Meta Classes). We can also override somInit, which is a method defined in SOMObject, but it can't take any arguments either. And anyway, you can't use an efficient exceptions mechanism with either constructor or somInit, so we'll see in the next example how you should use Initialize and Uninitialize methods to properly set up and unset your objects. In the case of som_Taxes, since it doesn't do much, we simply use a constructor which can't fail.
In this example, there's only one new method, CalcTheTax. CalcTaxes_som_Taxes is inheriting from SOMObject which is the root for SOM classes, and has methods like somGetClass and others which we'll see uses for later. The keyword IN found in the parameter list of CalcTheTax means that the parameter is passed to the method and, even if modified, not returned. As we'll see further below, the keywords OUT and INOUT can also be used, OUT meaning that the parameter doesn't receive an initial value and is returned by the method, INOUT meaning that the parameter is passed to the method with an initial value, and that the method can return another value. In fact, IN , OUT and INOUT are just for reading purposes, since these macros expand to nothing, they're just here as a reminder for SOM's in, out and inout keywords. But the same rules don't apply. A SOM out long value would be transformed in the .cpp as long *value. Here we have to say OUT long *value. In addition, the used SOM classes won't get an extra * (see the note on page 61 of develop 26).
SOM Objects and SOM Classes
SOM is a dynamic environment where classes themselves are instantiated as objects in memory. Those special objects are called class objects to differentiate them from simple objects, but there are objects nonetheless. Notwithstanding its name, SOMObject is a SOM class. SOMClass is also a SOM Class inheriting from SOMObject. To simplify complex matters, let's just say that when the SOM Runtime starts, it instantiates a class object of the class SOMClassMgr, a class object of the class SOMObject, and a class object of the class SOMClass. When you are instantiating an object of the class CalcTaxes_som_Taxes for the first time, SOM actually instantiates a class object for your class, and then, the object you requested. The class object of your class is instantiated only once, whatever the number of objects of this class you are instantiating. If you are interested in more details, please refer to chapter 2.1 of the Users Guide. Throughout this article, I made an effort to distinguish a SOM object from its SOM class, where it made sense, but mostly they're the same. When I'm writing about a method, for example, I may refer to it as the "SOM object method" or "SOM class method", but it doesn't matter much.
The next step is to write the .cpp file. It's in that .cpp file that you are going to either include or link to your old legacy code:
Listing 3. d2som_Texes.cpp
CalcTaxes_som_Taxes::CalcTaxes_som_Taxes()
{
fTax = 8;
}
long CalcTaxes_som_Taxes::CalcTheTax(INOUT Environment *ev, IN long value, IN short kind)
{
if (kind == 0) return value;
else return(value + ((value * fTax) / 100));
}
A big difference between C++ and SOM is that, by definition, all fields of a SOM class are private to that object, and all methods declared in the releaseorder list must be public and virtual. That means that if you want classes, inheriting from your class, accessing your fields, you have to provide accessors for them.
The next step is simply to build the SOM shared library. Using MetroWerks CodeWarrior, we'll need an extra file which contains the only symbol which has to be exported, for CFM (Code Fragment Manager) and SOM to be happy. This symbol is the complete name of your SOM class concatenated at the end with ClassData, ie. CalcTaxes_som_TaxesClassData. The name of the file must end with .exp and may be put in the project, ie. d2som_Taxes.exp. If there are more than one SOM class defined in a particular library, then all the ClassData symbols should be listed in the .exp file. Another way to achieve symbol export is to use the #pragma export directive in your .hh file as in the following example (see the Solid project for more details):
#pragma export on
class MSolid_som_Solid;
#pragma export off
Since the #pragma direct_to_som directive is on, the development environment does the right thing and adds the ClassData extension automatically.
You then create a new project based on the "ANSI C++ Console PPC (DLL)" (Pro 1) or "Std C++ Console PPC (DLL)" (Pro 2) stationery. You add the .cpp source file, the .exp file, and for convenience, the .hh file (duplicate the .h Target preference, and change the extension to .hh) and other source or resource files if you need. You also add the somlib shared library which contains the SOM code, and you modify the following settings:
- PPC Target: you select Shared Library as project type, you type in the name you chose, and you type cfmg as creator, unless you're providing your own bundle.
- PPC Linker: you clear all Entry Points fields
- PPC PEF: you select either the "use the ".exp" file" or "use #pragma" item in the Export Symbols popup menu depending on the way you desire to export symbols.
Depending on the code you either include or link to, you may or may not get rid of a lot of unuseful libraries automatically put in the project stationery. In my case, I only need to keep MSL ShLibRuntime.Lib and you build the library... (1421 bytes).
Figure 2. MetroWerks CodeWarrior SOM project for PowerPC.
In both MPW and MetroWerks environments, the Enums should be int (required for Direct-To-SOM compilation), and it's a good idea to either generate MacsBug symbols (for 68K) or tracebacks (for PowerPC) to ease your debugging (don't forget to turn them off for your distribution release). You turn on the Direct-To-SOM compilation in MPW MrCpp with the -som directive in the command line, and you can turn it on (popup menu) in the C/C++ Language preference panel in MetroWerks CodeWarrior. You can alternatively use the #pragma direct_to_som on, if you prefer.
Using the SOM Shared Library
It's very simple to use the SOM shared library in your application project (or another SOM shared library project, in this case it's GeneralItem_som_Item), you just have to add the TaxesSOMLib to your project window, add the #include "d2som_Taxes.hh" directive in your .cpp source file, allocate the SOM object with a new, deallocated it with a delete, and use it as if it were a C++ object everywhere you need it.
But let's take a closer look at the d2som_Item project first. The .hh is:
Listing 4. d2som_Item.hh
#include "D2som_Taxes.hh"
class GeneralItem_som_Item : public virtual SOMObject {
public:
GeneralItem_som_Item();
virtual ~GeneralItem_som_Item();
virtual void Initialize(INOUT Environment *ev);
virtual void Uninitialize(INOUT Environment *ev);
virtual void SetTheBTPrice(INOUT Environment *ev, IN long thePriceBeforeTax);
virtual void SetProductOrService(INOUT Environment *ev, IN short kind);
virtual long CalcTheATPrice(INOUT Environment *ev);
private:
short fProductOrService;
long fBeforeTaxValue;
CalcTaxes_som_Taxes* fTheTax;
#if __SOM_ENABLED__
#pragma SOMReleaseOrder (CalcTheATPrice, SetTheBTPrice, SetProductOrService,
Initialize, Uninitialize)
#pragma SOMClassVersion (GeneralItem_som_Item, 1, 0)
#pragma SOMCallStyle IDL
#endif
};
Note that you can't call anything which might throw an exception from somInit or somUninit or from the constructor or destructor of the C++ class. That's why it's usually a good idea to add to all your SOM classes the methods Initialize and Uninitialize which will be able to throw exceptions and which will be called just after the allocation and just before the deallocation of the SOM object.
In the example below, we do create the CalcTaxes_som_Taxes object in the Initialize method. The interesting part of d2som_Item.cpp is:
Listing 5. d2som_Item.cpp (extract)
void GeneralItem_som_Item::Initialize(INOUT Environment *ev)
{
fTheTax = new CalcTaxes_som_Taxes;
}
void GeneralItem_som_Item::Uninitialize(INOUT Environment *ev)
{
delete fTheTax;
}
long GeneralItem_som_Item::CalcTheATPrice(INOUT Environment *ev)
{
return(fTheTax->CalcTheTax(ev, fBeforeTaxValue, fProductOrService));
}
After that, we set up our new project file as before, without forgetting to add the TaxesSOMLib, and we do a build Now we can test our new SOM shared library.
To test all the SOM shared libraries in this article, I built one testing application, TestLibraries, which uses console output to hilite only the SOM coding. Its project file contains, of course, somlib and all the SOM shared libraries directly used by the application. For instance, it uses directly the som_Item object, but not the som_Taxes object which is used indirectly via the som_Item object, so only the ItemSOMLib is included in the project window and not the TaxesSOMLib. Of course, when the application is launched, the Process Manager has to find the TaxesSOMLib somewhere (or else you will get the usual Finder message saying that the TaxesSOMLib couldn't be found, and the application won't launch). So for pure ease of development, I set up projects to have all my shared libraries (some through aliases) and the application in the same folder. If you're interested in knowing of others places the shared libraries can go and still be found by the Process Manager, look at the "Mac OS Runtime Architectures" document available on the "Reference Library" Developer CD.
The interesting part of the source code of TestLibraries for the manipulation of GeneralItem_som_Item is:
Listing 6. TestLibraries.cpp (extract)
#include "d2som_Item.hh"
main()
{
long thePrice;
AutoInitEnvironment sev;
Environment *ev = (Environment *)&sev;
GeneralItem_som_Item* theItem = new GeneralItem_som_Item;
theItem->Initialize(ev);
theItem->SetTheBTPrice(ev, 100);
theItem->SetProductOrService(ev, 0);
thePrice = theItem->CalcTheATPrice(ev);
printf("General Item, service, thePrice = %ld\n",
thePrice);
theItem->SetProductOrService(ev, 1);
thePrice = theItem->CalcTheATPrice(ev);
printf("General Item, product, thePrice = %ld\n\n",
thePrice);
theItem->Uninitialize(ev);
delete theItem;
}
And the execution printout is:
General Item, service, thePrice = 100
General Item, product, thePrice = 108
Since we need an Environment* variable to pass to the SOM methods, we simply use the specially-defined-for-Direct-To-SOM-C++-compilation AutoInitEnvironment structure. We could also use Environment* ev = somGetGlobalEnvironment();. When you are writing an application, you care only about one Environment structure, so you might as well be using the one provided by the somlib shared library (somGetGlobalEnvironment). If you are writing threads, then it is imperative to allocate one Environment variable on the stack of each thread (AutoInitEnvironment) or else you will be in trouble.
Inheritance and Polymorphism
Now that we have defined our som_Item base class, let's inherit from it.
All the previous SOM classes inherited from SOMObject, but this time we'll inherit from GeneralItem_som_Item, leading to the following declaration, implementation and usage:
Listing 7. MSolid_som_Solid declaration, implementation and usage
class MSolid_som_Solid : public virtual GeneralItem_som_Item {
public:
virtual void SetQuantity(INOUT Environment *ev, IN short howmany);
virtual void SetUnitPrice(INOUT Environment *ev, IN short howmuch);
// overriding
virtual void Initialize(INOUT Environment *ev);
virtual long CalcTheATPrice(INOUT Environment *ev);
private:
short fQuantity;
short fUnitPrice;
#if __SOM_ENABLED__
#pragma SOMReleaseOrder (SetQuantity, SetUnitPrice)
#pragma SOMClassVersion (MSolid_som_Solid, 1, 0)
#pragma SOMCallStyle IDL
#endif
};
void MSolid_som_Solid::SetQuantity(INOUT Environment *ev, IN short howmany) { fQuantity = howmany;}
void MSolid_som_Solid::SetUnitPrice(INOUT Environment *ev, IN short howmuch) { fUnitPrice = howmuch;}
void MSolid_som_Solid::Initialize(INOUT Environment *ev)
{
this->GeneralItem_som_Item::Initialize(ev);
SetProductOrService(ev, 1);
SetUnitPrice(ev, 15);
}
long MSolid_som_Solid::CalcTheATPrice(INOUT Environment *ev)
{
SetTheBTPrice(ev, fQuantity * fUnitPrice);
return(this->GeneralItem_som_Item::CalcTheATPrice(ev));
}
{
MSolid_som_Solid* theSolid = new MSolid_som_Solid;
theSolid->Initialize(ev);
theSolid->SetQuantity(ev, 10);
thePrice = theSolid->CalcTheATPrice(ev);
printf("theSolid Price = %ld\n\n", thePrice);
theSolid->Uninitialize(ev);
delete theSolid;
}
As you can see, the Initialize method first calls its parent GeneralItem_som_Item::Initialize, and then calls methods which are defined only in the parent (such as SetProductOrService) or in this class (such as SetUnitPrice) in the same way.
Here's the predictable result:
theSolid Price = 162
As we see, the calling application can as easily use methods defined by the class (SetQuantity) as methods defined by the parent (CalcTheATPrice) in the same way. So, basically, you design and use inheritance the same way as you would in C++ or Object Pascal or any other OOL. The only big difference is that the methods reside in different shared libraries (SetQuantity is in the SolidSOMLib shared library, whereas CalcTheATPrice is in the ItemSOMLib shared library), although they're being called the same way. This mechanism is transparent both for the developer and for the user. That also means, as we'll see in more detail further below, that the CalcTheATPrice method can be changed in the parent, without SolidSOMLib being any different, the fragile base class problem is solved.
Under its apparent simplicity, both in design and usage, the main strength of SOM resides in this mechanism, that all the methods of an object don't have to be all in the same shared library. Take five to think about it...
for(time=TickCount();TickCount()<time+300;);
Listing 8. MCarWash_som_CarWash declaration, implementation and usage
class MCarWash_som_CarWash;
class M_MCarWash_som_CarWash : public virtual SOMClass
{
public:
virtual MCarWash_som_CarWash* CarWashCreate(INOUT
Environment *ev, IN short withWax);
#if __SOM_ENABLED__
#pragma SOMReleaseOrder (CarWashCreate)
#pragma SOMClassVersion (M_MCarWash_som_CarWash , 1, 0)
#pragma SOMCallStyle IDL
#endif
};
class MCarWash_som_CarWash : public virtual
GeneralItem_som_Item {
public:
virtual void SetWithWax(INOUT Environment *ev,
IN short yesOrNo);
// overriding
virtual void Initialize(INOUT Environment *ev);
private:
short fwithWax;
#if __SOM_ENABLED__
#pragma SOMReleaseOrder (SetWithWax)
#pragma SOMClassVersion (MCarWash_som_CarWash, 1, 0)
#pragma SOMCallStyle IDL
#pragma SOMMetaClass (MCarWash_som_CarWash,
M_MCarWash_som_CarWash)
#endif
};
MCarWash_som_CarWash*
M_MCarWash_som_CarWash::CarWashCreate(INOUT Environment
*ev, IN short withWax)
{
MCarWash_som_CarWash *obj = new MCarWash_som_CarWash;
if (obj) obj->Initialize(ev);
if (obj) obj->SetWithWax(ev, withWax);
return obj;
}
void MCarWash_som_CarWash::SetWithWax(INOUT Environment
*ev, IN short yesOrNo)
{
fwithWax = yesOrNo;
if (fwithWax) SetTheBTPrice(ev, 200); else
SetTheBTPrice(ev, 50);
}
void MCarWash_som_CarWash::Initialize(INOUT Environment *ev)
{
this->GeneralItem_som_Item::Initialize(ev);
SetProductOrService(ev, 0);
}
{
MCarWash_som_CarWash* theCarWash;
M_MCarWash_som_CarWash* theCarWashMetaClass;
theCarWashMetaClass = new M_MCarWash_som_CarWash;
theCarWash = theCarWashMetaClass->CarWashCreate(ev, 1);
thePrice = theCarWash->CalcTheATPrice(ev);
printf("CarWash with wax Price = %ld\n", thePrice);
theCarWash->Uninitialize(ev);
delete theCarWash;
theCarWash = theCarWashMetaClass->CarWashCreate(ev, 0);
thePrice = theCarWash->CalcTheATPrice(ev);
printf("CarWash without wax Price = %ld\n\n", thePrice);
theCarWash->Uninitialize(ev);
delete theCarWash;
delete theCarWashMetaClass;
}
Meta Classes
A good feature of C++ is that we are able to allocate and initialize (with parameters) a class in one statement such as CRect r(0, 0, 50, 100); or CRect *r = new CRect(0, 0, 50, 100);. Although there is an equivalent mechanism in SOM, it's neither so direct nor so easy, but at least it exists.
We saw in the previous examples that our classes inherit from SOMObject. Well, we can also have a class inherit from SOMClass, which we'll use to allocate and initialize (with parameters) our objects. This class is a Meta Class:
And this leads to the predictable result:
CarWash with wax Price = 200
CarWash without wax Price = 50
Multiple Inheritance
We first create a SOM class (Attributes_som_Attr) the usual way, then we're going to create another SOM class (MTires_som_Tires) which inherits both from Attributes_som_Attr and from MSolid_som_Solid which inherits from GeneralItem_som_Item.
The .hh is not very different. After the usual #include directives, we find:
Listing 9.
MTires_som_Tires declaration and usage
class MTires_som_Tires : public virtual Attributes_som_Attr, public virtual MSolid_som_Solid {
public:
MTires_som_Tires();
virtual ~MTires_som_Tires();
virtual short BrandNew();
#if __SOM_ENABLED__
#pragma SOMReleaseOrder (BrandNew)
#pragma SOMClassVersion (MTires_som_Tires, 1, 0)
#pragma SOMCallStyle IDL
#endif
};
{
MTires_som_Tires* theTires = new MTires_som_Tires;
// from som_Tires
theTires->Initialize(ev); // from som_Solid
theTires->SetQuantity(ev, 4); // from som_Solid
theTires->SetUnitPrice(ev, 500); // from som_Solid
theTires->SetTimeBeforeRotation(ev, 3); // from som_Attr
theTires->SetProvenance(ev, 17); // from som_Attr
thePrice = theTires->CalcTheATPrice(ev); // from som_Solid
printf("theTires Price = %ld\n\n", thePrice);
theTires->Uninitialize(ev); // from som_Item
delete theTires; // from som_Tires
}
Leading to the predictable result:
theTires Price = 2160
The interesting fact to note about the som_Tires object is that although we see it as one object, its code and methods actually reside in 4 different shared libraries: TiresSOMLib, AttrSOMLib, SolidSOMLib and ItemSOMLib, and it's all transparent.
I advise those who are interested in knowing how SOM resolves the classic ambiguities problem when dealing with multiple parents who have different methods with the same name to read chapter 4.2 (Inheritance) of the Users Guide, since I see no point in repeating here the excellent explanations found there. I'll just say that somehow, SOM deals with the ambiguities.
Let's Reap Some Benefits
Now that we have enough classes to play with, let's see how we gain from the use of SOM.
Let's say that suddenly, the state gets greedy (it happens!), and the sales tax on products goes from 8% to 10%.
Well, no big deal. We just reopen the som_Taxes project, change the line ftax = 8; by ftax = 10;, recompile this SOM shared library, and that's it, we're done, we don't have to do any other building of any other library or application using this lib. We just launch our testing application, and the printed results will show that all products (but not the services) are now 2% more expensive:
General Item, service, thePrice = 100
General Item, product, thePrice = 110
Good, but we could have achieved the same result with a simple dynamic shared library based on CFM, so in that case, although SOM didn't hinder, it didn't do anything that we couldn't have done in another way.
Since, when prices go up, consumers tend to buy less. The providers have to react in some way; most of the time they offer discounts...
So we go back to the som_Item class, and we're going to update it to version 1.1, introducing the discount concept (to prevent confusion, I have a separated ItemDiscount project, with new d2som_ItemDiscount.xxx files, but the built shared library is still ItemSOMLib):
As usual, we start at the .hh, which is now:
Listing 10. d2som_Item.hh v.1.1
class GeneralItem_som_Item : public virtual SOMObject {
public:
GeneralItem_som_Item();
virtual ~GeneralItem_som_Item();
virtual void Initialize(INOUT Environment *ev);
virtual void Uninitialize(INOUT Environment *ev);
virtual void SetTheBTPrice(INOUT Environment *ev, IN
long thePriceBeforeTax);
virtual void SetProductOrService(INOUT Environment
*ev, IN short kind);
virtual long CalcTheATPrice(INOUT Environment *ev);
virtual void SetItemDiscount(INOUT Environment *ev, IN
short howmuch);
private:
short fProductOrService;
long fBeforeTaxValue;
short fItemDiscount;
CalcTaxes_som_Taxes* fTheTax;
#if __SOM_ENABLED__
#pragma SOMReleaseOrder (CalcTheATPrice, SetTheBTPrice,
SetProductOrService,
Initialize, Uninitialize,
SetItemDiscount)
#pragma SOMClassVersion (GeneralItem_som_Item, 1, 1)
#pragma SOMCallStyle IDL
#endif
};
As you can see, there are only minor modifications. We introduced a new method SetItemDiscount which we also place in the releaseorder directive, and a new field fItemDiscount, and, very important, we changed the minorversion to 1 (it was 0).
SOM Version Numbers
Since I couldn't have explained it better myself, here's the excerpt from the Users Guide about major and minor version numbers:
"These numbers are checked against the version numbers built into the class library to determine if the class is compatible with the client's expectations. The class is compatible if it has the same major version number and the same or a higher minor version number. If the class is not compatible, an error is raised. Major version numbers usually only change when a significant enhancement or incompatible change is made to a class. Minor version numbers change when minor enhancements or fixes are made. Downward compatibility is usually maintained across changes in the minor version number."
We also modify slightly the .cpp:
Listing 11. d2som_Item.cpp v.1.1 (extract)
void GeneralItem_som_Item::Initialize(INOUT Environment *ev)
{
fTheTax = new CalcTaxes_som_Taxes;
fItemDiscount = 15;
}
long GeneralItem_som_Item::CalcTheATPrice(INOUT
Environment *ev)
{
long theValue = fBeforeTaxValue;
theValue -= (theValue * fItemDiscount) / 100;
return(fTheTax->CalcTheTax(ev, theValue,
fProductOrService));
}
void GeneralItem_som_Item::SetItemDiscount(INOUT
Environment *ev, IN short howmuch)
{
fItemDiscount = howmuch;
}
We build the new library the usual way, and now we reap the SOM benefits: we don't have to rebuild all the libraries containing the classes which inherit from som_Item (ie. som_CarWash, som_Solid, som_Tires, som_Car, som_Tapes), nor do we have to rebuild our testing application. The simple act of rebuilding the new version of som_Item is enough. If we launch the testing application, we will get the following printed result:
General Item, service, thePrice = 85
General Item, product, thePrice = 93
CarWash without wax Price = 43
CarWash with wax Price = 170
theSolid Price = 140
That means that if the whole set of these libraries and testing application was a complete solution installed on your customers' Macintosh, to deal with both the new tax and the discount countermeasure, you just modify and rebuild 2 libraries, and you have only those 2 to send to your customers to update them. This is both a gain in time (we only deal with 2 libraries instead of a whole complex global project), and a gain in cost of goods (we only have to send away 2 libraries instead of the whole application). Whatever ways we could choose for the distribution (floppies, Internet, etc.), it will always be cheaper to send a few small items than one big item.
Versioning
Since we now may have 2 different versions of GeneralItem_som_Item floating in our customers' installed base, let's see how we can deal with it and profit from it.
The more the consumer buys, the greater the discount. Let's say that if a consumer buys 1000 tapes, s/he doesn't expects to pay the price of 1 tape times 1000. Now that we have this neat SetItemDiscount method in our GeneralItem_som_Item class, it would be great to be able to call it from one of its descendants. The problem is that this method doesn't exist in the version 1.0 of GeneralItem_som_Item . So let's create a som_Tapes class dealing with discounts and see how it's done. As usual, the .hh first:
Listing 12. MTapes_som_Tapes declaration and usage
class MTapes_som_Tapes : public virtual MSolid_som_Solid {
public:
MTapes_som_Tapes();
virtual ~MTapes_som_Tapes();
virtual void SetDiscount(INOUT Environment *ev, IN
short howmuch);
#if __SOM_ENABLED__
#pragma SOMReleaseOrder (SetDiscount)
#pragma SOMClassVersion (MTapes_som_Tapes, 1, 0)
#pragma SOMCallStyle IDL
#endif
};
{
MTapes_som_Tapes* theTapes = new MTapes_som_Tapes;
theTapes->Initialize(ev);
theTapes->SetQuantity(ev, 1000);
theTapes->SetUnitPrice(ev, 5);
thePrice = theTapes->CalcTheATPrice(ev);
printf("theTapes Price (no special discount) = %ld\n",
thePrice);
theTapes->SetDiscount(ev, 25);
thePrice = theTapes->CalcTheATPrice(ev);
printf("theTapes Price (discount = 25\%) = %ld\n",
thePrice);
theTapes->SetDiscount(ev, 75);
thePrice = theTapes->CalcTheATPrice(ev);
printf("theTapes Price (discount = 75\%) = %ld\n\n",
thePrice);
theTapes->Uninitialize(ev);
delete theTapes;
}
Let's see how the SetDiscount method is dealt with in the .cpp file.
There are many, many ways to verify at runtime if the version 1.1 of som_Item is present, and thus, if we can use its discount feature.
Just like any good Macintosh developer checks with Gestalt to see if such or such feature is available, instead of checking the system version and then making some assumptions about which feature is there or not (these assumptions, often unjustified, have a way to bite the developer back (or rather her/his customers) at a later date), we can check the availability of any method by just using its name.
Two situations can arise: you have the .hh of GeneralItem_som_Item v.1.1, and this should be the usual situation, or for some strange reason (but strange is real life), you don't have the .hh but at least know the prototype of SetItemDiscount (if you don't, then you're stuck).
Since we use that code in a descendant of GeneralItem_som_Item , and any method of GeneralItem_som_Item is in the scope of the methods of that descendant, we simply write in the first case:
if (somResolveByName(this, "SetItemDiscount") != 0)
this->SetItemDiscount(ev, howmuch);
Or, in the second case:
typedef void (*funcptr)(SOMObject*, Environment*, short);
funcptr callit = (funcptr)somResolveByName(this, "SetItemDiscount");
if (callit) (*callit)(this, ev, howmuch);
And, you're done. Just launching the testing application you'll get, if GeneralItem_som_Item v.1.1 is present:
theTapes Price (no special discount) = 4675
theTapes Price (discount = 25 percent) = 4125
theTapes Price (discount = 75 percent) = 1375
And, if GeneralItem_som_Item v.1.1 is not present:
theTapes Price (no special discount) = 5500
theTapes Price (discount = 25 percent) = 5500
theTapes Price (discount = 75 percent) = 5500
Now you can understand why the consumer would be pleased with the version 1.1 if s/he qualifies for the 75% discount.
Some other ways to check for the availability of SetItemDiscount:
Using the somRespondsTo method defined in the SOMObject class (which is your ultimate ancestor):
char *theMethodName = "SetItemDiscount";
if (this->somRespondsTo(&theMethodName))
/* SetItemDiscount available */; else /* it's not*/;
Using the somSupportsMethod method defined in the SOMClass class (which is an ancestor of your class):
char *theMethodName = "SetItemDiscount";
SOMClass *theClass = (SOMClass *)this->somGetClass();
if (theClass->somSupportsMethod(&theMethodName))
/* SetItemDiscount available */; else /* it's not*/;
theClass->somFree();
If you insist on dealing with version numbers rather than features, then you can test if a direct instantiation of som_Item v.1.1 works:
SOMObject *theObject;
theObject = somNewVersionedObject(GeneralItem_som_Item, 1, 0);
if (theObject != 0) {
/* a majorversion of 1 and a minor version >= 0 has been found */
somReleaseObjectReference(theObject);
} else /* v.1.x unavailable, should never happen... */;
theObject = somNewVersionedObject(GeneralItem_som_Item, 1, 1);
if (theObject != 0) {
/* a majorversion of 1 and a minor version >= 1 has been found */
somReleaseObjectReference(theObject);
} else /* most likely only v.1.0 is there... */;
As we saw in the "Major and Minor Version Numbers" side bar, if you ask for n.0, it will succeed if n.x is present, whatever the value of x. If you ask for n.m, it will succeed if n.x is present, with x >= m. It will fail in all cases if only p.x is present, with p ¬ n (except if you ask for 0.0, then it will succeed if any p.x is present).
In my example, if both v.1.0 and v.1.1 are present in the same folder as the testing application, v.1.1 will be chosen by default, since SOM, unless told otherwise, will always load the higher possible version.
If you insist on dealing with version numbers rather than features, then you may also check a parent version with the following code (remember that in this case, although GeneralItem_som_Item may be v.1.1 or v.1.0, MTapes_som_Tapes is v.1.0, so it doesn't do us a lot of good testing the version of MTapes_som_Tapes):
Listing 13. GetThisParentVersion implementation and usage
short GetThisParentVersion(SOMClass *theClass, char
*className, long *majorVersion, long *minorVersion)
{
short i, result = -1;
SOMClassSequence parents = theClass->somGetParents();
for (i=0; (i<parents._length) && (result == -1); i++)
{
char *parentName = parents._buffer[i]-
>somGetName();
if (strcmp(parentName, className) == 0)
{
parents._buffer[i]->somGetVersionNumbers (majorVersion, minorVersion);
result = 0;
}
else result = GetThisParentVersion(parents._buffer[i], className, majorVersion, minorVersion);
}
if (parents._length) SOMFree(parents._buffer);
return(result);
}
...
long majorVersion, minorVersion;
SOMClass *theClass = (SOMClass *)this->somGetClass();
short ok = GetThisParentVersion(theClass ,
"GeneralItem_som_Item", &majorVersion, &minorVersion);
theClass->somFree();
if (ok == 0) /* the parent was found, so we can test the version numbers */;
else /* the parent was not found, something is really weird here... */;
Since SOM offers multiple inheritance, the somGetParents method returns an array containing all the direct parents of the class. There is also a somGetParent method which returns only the leftest parent, but I strongly advise you to ignore that one, because it might well get you in trouble in the future (ie. some SOM classes, developed by other developers and which you are using, which were single inheriting from one parent begin to inherit from many parents).
Exceptions
Since, according to Murphy's law, if anything can go wrong, it will, and according to some other bright person, Murphy was an optimist... It's really a good idea to check our code against reality. The good news is that we can use C++ native exceptions with Direct-To-SOM compilation, the bad news is that we have to do some effort to make it happen.
You can't throw from a SOM method. It's a compiler limitation. Maybe we'll be able to in future releases of C++ compilers, maybe never. What we can do, though, is to ask the compiler, through the SOMCheckEnvironment pragma, to generate calls to __som_check_ev and __som_check_new, after each call to a SOM method or a SOM object allocation. It's up to you to provide an implementation for those calls. You'll find mine below, but you are welcome to make your own. You get your chance to throw in those routines.
Then, each time we're unhappy with something in a SOM method (not enough memory, disk full, and so on), we just fill the Environment structure with adequate parameters and return. The second parameter (after ev) is the nature of the exception. In your case, it should always be USER_EXCEPTION, then you can pass a string, and a long (in fact a void *). You can use this last parameter to just pass an integer code (that's what I do in this example since I don't have much need for a more complex setup), but you're welcome to allocate whatever structure you want, fill it, and pass its address as this last parameter (you'll have to deallocate it in __som_check_ev later). The exception mechanism you use is your own; you're free to do whatever you want with it.
Then in the calling code, we just try and catch the usual way.
Listing 14. somSetException, __som_check_new, __som_check_ev, trying, throwing and catching
void MCar_som_Car::SetAudioSystem(INOUT Environment *ev,
IN short withOrWithout)
{
if ((withOrWithout != 0) && (withOrWithout != 1))
somSetException(ev, USER_EXCEPTION,
"test somSetException/MCar_som_Car::SetAudioSystem", (void *)1);
else fAudioSystem = withOrWithout;
}
#pragma SOMCheckEnvironment on
extern "C" void __som_check_new(SOMObject*);
extern void __som_check_new(SOMObject *SOMObj)
{ if (SOMObj == 0) { throw(1); }}
extern "C" void __som_check_ev(struct Environment*);
extern void __som_check_ev(struct Environment *ev)
{
if (ev->_major)
{
char *name = ev->exception._exception_name;
printf("%s\n", name);
long trowval = (long)ev->exception._params;
somExceptionFree(ev);
ev->_major = NO_EXCEPTION;
throw(trowval);
}
}
{
try {
printf("calling theCar->SetAudioSystem(ev, 3).\n");
theCar->SetAudioSystem(ev, 3);
}
catch(long x) {
printf("Caught the throw %ld in catch.\n", x);
printf("calling theCar->SetAudioSystem(ev, 1).\n");
theCar->SetAudioSystem(ev, 1);
}
}
Leading to this predictable result:
calling theCar->SetAudioSystem(ev, 3).
test somSetException/MCar_som_Car::SetAudioSystem
Caught the throw 1 in catch.
calling theCar->SetAudioSystem(ev, 1).
Other Interesting SOM Calls
There are many ways in SOM to obtain information on the SOM objects, their names, their parents, their versions, their methods, method names, and method pointers, etc.
Consider the following routines:
Listing 15. PrintParent and PrintInfoAboutSOMObject
void PrintParent(SOMClass *theClass, long level)
{
SOMClassSequence parents;
long i;
parents = theClass->somGetParents();
for (i=0; i<parents._length; i++)
{
char *parentName = parents._buffer[i]-
>somGetName();
printf("parent %ld at level %ld: %s\n", i, level,
parentName);
PrintParent(parents._buffer[i], level+1);
}
if (parents._length) SOMFree(parents._buffer);
}
void PrintInfoAboutSOMObject(SOMObject *theSOMObject)
{
char *theClassName;
theClassName = theSOMObject->somGetClassName();
printf("\nclassName: %s\n", theClassName);
theClassName = theSOMObject->somGetClass()-
>somGetName();
printf("className: %s\n", theClassName);
PrintParent(theSOMObject->somGetClass(), 0);
long nbOfMethods = theSOMObject->somGetClass()- >somGetNumMethods();
printf("number of methods: %ld\n", nbOfMethods);
for(long i = 0; i < nbOfMethods; i++)
{
somKernelId theKernelID;
somId theSomID;
theSOMObject->somGetClass()- >somGetNthMethodInfo(i, &theKernelID);
theSomID = somConvertAndFreeKernelId(theKernelID);
char *theMethodid = somMakeStringFromId(theSomID);
printf("method %ld: %s\n", i, theMethodid);
SOMFree(theSomID);
}
}
These functions provide 2 different way to get the class name of the passed object and other information. When called this way:
theTires = new MTires_som_Tires;
PrintInfoAboutSOMObject(theTires);
delete theTires;
It gives this back this result:
className: MTires_som_Tires
className: MTires_som_Tires
parent 0 at level 0: Attributes_som_Attr
parent 0 at level 1: SOMObject
parent 1 at level 0: MSolid_som_Solid
parent 0 at level 1: GeneralItem_som_Item
parent 0 at level 2: SOMObject
number of methods: 31
method 0: SOMObject::somInit
method 1: SOMObject::somUninit
method 2: SOMObject::somDuplicateReference
method 3: SOMObject::somCompareReference
method 4: SOMObject::somRelease
method 5: SOMObject::somFree
method 6: SOMObject::somCanDelete
method 7: SOMObject::somGetClass
method 8: SOMObject::somGetClassName
method 9: SOMObject::somGetSize
method 10: SOMObject::somIsA
method 11: SOMObject::somRespondsTo
method 12: SOMObject::somIsInstanceOf
method 13: SOMObject::somDispatch
method 14: SOMObject::somClassDispatch
method 15: SOMObject::somCastObj
method 16: SOMObject::somResetObj
method 17: SOMObject::somDumpSelf
method 18: SOMObject::somPrintSelf
method 19: SOMObject::somDumpSelfInt
method 20: Attributes_som_Attr::SetProvenance
method 21: Attributes_som_Attr::SetTimeBeforePerish
method 22: MTires_som_Tires::BrandNew
method 23: GeneralItem_som_Item::CalcTheATPrice
method 24: GeneralItem_som_Item::SetTheBTPrice
method 25: GeneralItem_som_Item::SetProductOrService
method 26: GeneralItem_som_Item::Initialize
method 27: GeneralItem_som_Item::Uninitialize
method 28: GeneralItem_som_Item::SetItemDiscount
method 29: MSolid_som_Solid::SetQuantity
method 30: MSolid_som_Solid::SetUnitPrice
Conclusion
So, if SOM is such a great technology, why isn't Apple using it for every little piece of software distributed to either developers or end users?
There are many good reasons. First, object-oriented programing is not necessary everywhere, it is good for an application or a complete solution, but not always very interesting for system software, where encapsulation, polymorphism or inheritance don't always make sense. And if you're not using object-oriented programing, then you're better off using CFM (which Apple is using for every little piece of software) which provides a good dynamic library architecture. But where OOP does make sense, as in Contextual Menus, then Apple uses SOM.
Depending on what you're writing, you will find yourself in one of those cases:
Table 1
Development Strategy
Runtime
Style Language(s) Architecture
Procedural C, Pascal, etc. CFM
Native Objects C++ SOM
X-Platform or Distributed Java Java
Another reason is that, unfortunately, there is a small price to pay to get the benefits of SOM. Although its dispatch code (when you're calling a method which may or may not be overridden) is quite small (6 PowerPC instructions for a non-overridden method, 12 for an overridden method), it may lead to performance issues if you have too fine a granularity (that means that each method does not do much, so the ratio of useful code and dispatch code is not a good one). Typically, if you define classes where methods do jobs as small as those I've shown in the examples of this article, then if you were to overuse these objects (let's say a billion call of SetQuantity and CalcTheATPrice), you might not get the expected performance, even on the fastest Power Macintosh. That being said, let's not forget that if you're using C++ classes, then you also pay a price for method dispatch which is only marginally smaller than SOM's (1 PowerPC instruction for a non-virtual method, 6 for a virtual method). Furthermore, as my experience showed, when defining utility classes, you define most of them as virtual (so that you can override at your leisure later), although you override a small fraction of them in a particular project. In this case, you get 6 dispatch instructions for both C++ and SOM implementation, and your cost is really 6 additional instructions when you actually do an override in SOM. If your methods (which may contain loops, for instance) are thousands instructions long at runtime, then the cost of using SOM instead of C++ is marginal.
Table 2
C++ versus SOM dispatch code
Case C++ SOM
not overridded 1 instruction 6 instructions
overriddable (virtual) 6 instructions 6 instructions
overridded 6 instructions 12 instructions
So the choice is between non-OOP versus SOM because, if you're going to use OOP, then the advantages of SOM vs. C++ truly more than compensate the very small amount of extra-time spent in the SOM dispatch code vs. the C++ dispatch code.
And, as a reminder, the advantages of SOM are:
- Language and Development Environment independence
- Shared Dynamic Libraries and Object-Oriented Programing
- Keeping the binary compatibility even when:
- Adding new methods,
- Changing the size of an object by adding or deleting
- instance variables,
- Inserting new parent (base) classes above a class in the
- inheritance hierarchy,
- Relocating methods upward in the class hierarchy
- Multi-platform (Mac OS, MS-Windows, OS/2, AIX, and many others...)
Since you, readers, are more likely to develop applications or complete solutions than system software, then you should really think about using SOM to both clean up the past (with encapsulation) and provide for the future.
Thanks to our technical reviewers Deeje Cooley, Pete Gontier, and George Warner.
Thanks to our technical reviewers Brian Arnold, Paul Black, Geoff Clapp, Mike Rossetti, and Jason Yeo.
Éric Simenel is really happy he transferred to Cupertino's DTS from Paris' DTS. Aside from the fact that he received a great welcome from his current colleagues, he's getting much more sun here than there. And, due to his constant location here, he has easier access to comic books conventions, so he has completed many runs: the current mark is at 23,000 and counting.