This section describes the Component Object Model as used in MMLite and general rules as how to program with it.
The COM used in MMLite is a light-weight mechanism and doesn't mean the entire OLE or DCOM enchilada.
The following COM properties are TRUE in MMLite:
· All interfaces derive from IUnknown.
· IUnknown implement lifetime control through reference counting [AddRef, Release methods] and type casting with version checking through Unique Identifiers (IID) [QueryInterface method].
· Interfaces are compatible with C++ pure abstract classes.
· Each object has at least one v-table.
· There may be multiple implementations for each interface and it is transparent to the caller, which implementation is used.
The following are FALSE and are NOT supported on MMLite.
· GUIDS for naming.
· CoCreateinstance. In MMLite we instead use constructors that supply arguments instead of a two-step creation. An explicit factory interface is often required.
· Variants, Monikers
· Backward compatibility of every possible interface. No, we retire them instead.
MMLite components are ideally black boxes that know nothing about the internals of another object. Components gain access to each other through a name space. The name space is used for all naming purposes.
Once a component get access to an object, it can call methods on it.
Most methods return a status code [SCODE]. The macro FAILED(sc) can be used to determine whether the method was successful or not. If a method was succesful, then all other return parameters are initialized and whatever operation the method was supposed to do was carried out.
Example:
sc = Foo->DrawBlob(100, 100, &Size, &pBlob);
if (FAILED(sc)) {
· The values of Size and pBlob are undefined.
· No reference was taken.
· The blob was not drawn.
· sc indicates the reason of failure.
} else {
· Size and pBlob are initialized.
· pBlob carries an object pointer with one reference. When the caller is done with pBlob it should call pBlob->Release().
· A Blob was drawn.
· Different success code might indicate degrees of success, e.g. whether the blob was obstructed and remains invisible.
}
AddRef and Release return the resulting reference count [UINT]. They cannot fail so there is no question about failure.
Some methods return other values. For example IHeap::Alloc() returns a pointer [void *]. If the return value is NULL the operation failed, otherwise it succeeded. In this kind of cases GetLastError() retrieves the status code. A method implementation that fails but whose return value is not SCODE must use SetLastError() to set the error code. After a successfull method call the value returned by GetLastError() is undefined.
Method IN arguments are not reference counted. Their lifetime is the lifetime of the method activation. If the callee intends to keep an object pointer past the time the current method activation returns, for example by storing it into a variable, it must call the object's AddRef method. It is the callee's responsibility to guarantee that Release is eventually called in such a case. In other words the basic caller assumption is that the reference count stayed the same and no references were taken. Anything else is handled by the callee.
Returned pointers come with a reference. The callee is responsible for calling Release when disposing of the pointer. Exception: CurrentNameSpace(), CurrentProcess(), CurrentThread(), CurrentHeap(), and other CurrentBlah() run-time library functions do NOT return a reference with the object pointer. Instead the lifetime is controlled by the caller's environment. If a pointer of this kind is given to outside of the current thread (in the case of CurrentThread) or outside of the current process (in the case of CurrentProcess), an explicit Addref must be done first. The value of CurrentThread() stops being valid once the current thread exits and all external references have been released. Similarly the values returned by ProcessImage() and ProcessArgs() are valid until the current process exits.
It is important that no references are ever leaked as this will result in memory leakage and sometimes lack of termination. It is even more important not to call Release without a valid reference (or too many times). Bad things will happen. One way of keeping track of things is to maintain the correct reference count at all times, minding error conditions.
Example. Note how all preceding operations are undone upon failure.
sc = GetFoo(&pFoo);
if (FAILED(sc))
goto Error1;
sc = GetBar(&pBar);
if (FAILED(sc))
goto Error2;
sc = GetBaz(&pBaz);
if (FAILED(sc))
goto Error3;
return S_OK;
Error3:
pBar->Release();
Error2:
pFoo->Release();
Error1:
return sc;
Another thing to do is to take a reference every time an object pointer is stored and releasing every time it is removed from storage.
Example:
pFoo->AddRef();
pStaticVariable = pFoo;
...
pStaticVariable->Release();
pStaticVariable = NULL;
Writing a NULL into the released variable may or may not be necessary but makes the code more understandable and will catch any accesses to the invalid pointer.
There are three kind of native executables in MMLite.
1. .EXE is a traditional program that has a main() function and a new thread is created to execute it. When main() returns, the thread is terminated. When all threads running in the module (including those created by the initial thread and the initial thread itself) have exited, the module is terminated and the program is unloaded. An exe must be careful if it exports any objects to other modules through the name space or otherwise so that the module is not prematurely unloaded. An explicit reference needs to be taken and released when no longer needed.
2. .DLL is a traditional shared library. The entry point [DllMain] is called when the library is loaded. No new thread is created, instead the thread that called IProcess::LoadImage is used. When DllMain returns, the module is unloaded iff the return value was FALSE. In this case ::LoadImage fails. If DllMain returns TRUE, the module will stay in memory, and :LoadImage returns a reference to the module. Once that reference is released, as well as potential other refs, unloading happens. Again, no atuomatic handling of exported refs is provided.
3. .COB = Component Object Binaries. The preferred way of writing system components. The entry point is CobMain(). CobMain() returns an object pointer [PIUNKNOWN]. That object is cached by the Cob Manager. The lifetime of the initial returned object pointer controls the lifetime of the Module. Iff CobMain returns NULL, the module is immediately terminated. A cached COB is visible in the COB name space and can be accessed from there. Typically the object returned would be a Factory object that can be used to create other objects. It could also be a name space. Using INameSpace::Bind as a constructor has the limitation that no arguments can be passed (besides the URN string) and thus only argument-less constructors can be done this way. Otherwise a special IBlahFactory interface needs to be created. There are examples on writing COBs and clients for them in src/samples.
New interfaces are declared in a header file that usually goes into MMLite/inc. The header should preferably be both in C and C++. XXX NOW IN XML in MMLite/conf.
A new IID needs to be generated (this also applies if the interface merely changed). uuidgen -s generates an IID. Edit and put that into a new file in MMLite/src/iidlib. Uuidgen, put it into the XML file.
Constructors should usually return an IUnknown. The caller can then use QueryInterface to cast it into the desired type. Going through this extra step is beneficial in that it goes through version checking. If the interface changed, the QueryInterface call will notice it by failing. The component that had gotten out of sync should then be recompiled to reflect the new interface. The IID thus only serves to do version checking.