CRYSTAL SPACE COM NOTES

Written by Dan Ogles

mailto:dogles@peachtree.com

Last updated: October 22, 1998

Introduction

Over the past couple of months, I have been performing an experiment, shall we say, to see how Microsoft’s Component Object Model (COM) fits in with the Crystal Space project. The overall goal is to allow Crystal Space to act as an ActiveX Automation Server for a scripting language. Using COM as the core of our scripting engine gives us several benefits. Some of these benefits will be outlined in this document.

The experiment was to make all of our rasterization code (all classes formerly derived from csGraphics2D and csGraphics3D) put into separate DLLs and accessed via COM. The advantage of this is that if a person knows the interfaces that Crystal Space exports, s/he can write a new rasterization server without Crystal Space having to know anything about it. Crystal Space loads the server dynamically, and uses it based on an assumed interface.

But all that stuff will be covered below. The key point is that now our graphics servers have an extensible architecture. No recompilation is needed on CS’s part to take advantage of new graphics layers. For instance, if an OpenGL server was written, there would be no need for Crystal Space to be recompiled.

Glossary

Client The program that loads and uses COM components from other sources. The Crystal Space executable is the client in our case.

Server The library that contains a set of components that may be used by other programs. The graphics drivers in Crystal Space are the servers.

In-proc In-process. In-proc servers are sets of components that are contained in a DLL. These components exist in the same process as the server.

Out-of-proc Out-of-process. Out-of-process components either exist in another computer process (EXE), or on another machine. Out-of-process components are currently not used in Crystal Space.

Interface This is the means by which COM components are accessed. It is a binary contract that in C++ is equivalent to a pure abstract class.

Coclass This is a singleton object that is in charge of creating COM components. Also referred to as a class factory.

GUID A Global Unique Identifier. This is a 128-bit number that is guaranteed to be unique in time and space. These numbers are generated through the COM API utility GuidGen.

IID An Interface Identifier. This is a GUID that represents an interface. All COM interfaces have an IID associated with them, usually using the IID_Interface naming convention.

CLSID This is a Class Identifier. This is a GUID that represents a specific coclass. This is used by the CoGetClassObject or CoCreateInstance functions to load a COM server.

ProgID A programmatic ID. This is a human-readable name that is directly related to a specific CLSID. A COM API method exists which allows you to retrieve a CLSID based on a progID. ProgIDs usually have the naming convention library.classname.versionnum.

DLL Also known as a shared library. This is a library that is loaded into the process space during run-time instead of linked into the executable during compile-time.

Why COM?

To be perfectly blunt, COM at first glance seems like a bad solution to anything that is supposed to be platform-independent. Up until recently, the only programs that used COM were MS Windows apps. That’s because the COM API only existed on MS Windows platforms. Now the COM API is ported to several different flavors of Unix, and I believe it exists for the Mac (otherwise Internet Explorer wouldn’t work…). But what about the platforms that we support that don’t support the COM API? We certainly don’t want to stop supporting those platforms just because they don’t choose to implement an API standard developed by Microsoft.

Well, there is a distinction between what is COM and what is the COM API. The COM architecture is just that – an architecture. It only specifies a standard that COM objects adhere to. To adhere to the standard, you only need to organize your object in a specific manner. No system calls are absolutely necessary to support COM. This is very important and should not be forgotten.

The COM API is a set of functions that are implemented by the operating system. The vast majority of these functions aren’t necessary – the bulk of the COM API exists to support out-of-process and remote activation (Distributed COM). The API functions that are needed are easy to emulate. The csCom library, written specifically for Crystal Space, does this.

So what are the advantages of COM? That’s a very good question. And here’s the answer.

Not sold yet? Well it gets better. First of all, components are very lightweight. The amount of code and data needed for an object to support COM is negligible. This usually makes the compiled size of components very small. Secondly, COM is fast. The amount of overhead involved in accessing a COM object is equivalent to the amount of overhead involved in a virtual function call in C++. That amounts to a pointer de-reference or two. Note: this only applies to in-proc (DLL) activation, which is the only type of activation Crystal Space uses. Out-of-proc activation is done through the ORPC transport, which is COM API specific. The degree of overhead is much larger when accessing out-of-proc components, making it undesired for our purposes.

The Crystal Space COM Library

The csCom Library is a library designed to emulate certain standard COM API functions. csCom may be compiled in three different ways:

By default, the COM library is compiled assuming that the OS supports the COM API. If the API does not exist, then the directive NO_COM_SUPPORT should be defined. When this directive is defined, csCom defaults to COM emulation through dynamic linking. The platform should support some means to load a dynamic linked library (also known as a shared library) into the current process, and retrieve a pointer to a function based on the string function name. If the target OS does not support this, then the directive CS_STATIC_LINKED should be defined. This is for OS’s that don’t support dynamic linking of any sort. All component servers are compiled as static libraries and linked at compile time with the Crystal Space executable.

The following is a list of COM API functions that csCom emulates if necessary:

All these methods are prefixed with cs, to make them distinct from the COM API functions if they exist on the system. In addition, the COM emulation library has two helper functions, csRegisterServer and csUnregisterServer. These help register and unregister COM servers from the component registry. They are called by (under any OS that supports the COM API) RegSvr32 or (by Crystal Space’s utility) csRegSvr.

The csCom library also contains macros to simplify the writing of components, as well as standard typedefs and macros that are used by the COM API. All of these are written to be as platform-independent as possible.

Probably the most important typedef is the HRESULT. This is a 32-bit value that is used to return success/failure codes for all COM methods. A value of S_OK (which evaluates to 0) indicates success. There are several different standard HRESULT values that the COM API uses to indicate failure. These should be used in favor of custom HRESULT values whenever possible. There is a particular way custom HRESULT values should be calculated.

There are two important standard COM macros:

In addition, csCom has several helper macros that make the implementation of components easier. Some of these macros are based on macros written by Don Box in Essential COM:

The csCOM library also has a set of macros that make the implementation of IUnknown, specifically QueryInterface, easier to write.

Direct-to-COM

When the csCom library is compiled without either NO_COM_SUPPORT or CS_STATIC_LINKED, it defaults to Direct-to-COM support. This simply forwards all calls to the COM API methods, performing ANSI-to-Unicode conversions for string parameters if necessary. Use this when the COM API exists on the target platform.

COM Emulation through Dynamic Linking

When the csCom library is compiled with NO_COM_SUPPORT, it uses the dynamic-linking version of the emulation library. The majority of this code involves the access to the Crystal Space Component Registry. The registry is within the file cryst.reg. This file forms the abstraction layer over the location of COM components. All component servers are registered into this file using the method csRegisterServer. To use this emulation mode, you must implement three functions:

// used to dynamically load a library.

extern CS_HLIBRARY csLoadLibrary( char* szLibName );

// returns the address of a procedure

PROC csGetProcAddress( CS_HLIBRARY hLib, char* szProcName );

// free all libraries loaded into this process.

void csFreeAllLibraries();

The CS_HLIBRARY type is simply a 32-bit value that is used as a handle onto a loaded library. You must keep track of all loaded libraries, so that you are able to unload them on response to csFreeAllLibraries(). The csLoadLibrary() function loads a dynamic library into the process address space based on the library name. It should call the Dll entry point DllMain. This is absolutely essential to proper run-time behavior. On success, it returns the handle to the library. On failure it returns null. The csGetProcAddress() function returns the address of a function based on it’s exported name. It returns a void pointer to the function.

These functions are platform-specific, so they should be placed within the appropriate directory. They should be compiled into the csCom library.

COM Emulation through Static Linking

When the csCom library is compiled with both NO_COM_SUPPORT and CS_STATIC_LINKED, it uses the static linking emulation mode. This is the least powerful mode and is only meant to be used by legacy operating systems that don’t support dynamic linking in any form. All COM servers are linked at compile-time with the Crystal Space executable, making the plug-in architecture useless. All activation is done through a global array, gb_aiidStaticLinkIDs, which is an array mapping CLSIDs and progIDs to coclass object instances. It is ended with an empty entry. Whenever a COM server is added to Crystal Space that may be used by operating systems that don’t support dynamic linking, an entry should be added to this array.

This emulation mode is currenlty untested, as of October 25th, 1998.

Crystal Space COM Specifics

The Crystal Space project has been split up into several different projects. The file listings are up to date as of October 22nd, 1998.

Files: ICamera.cxx ILightMap.cxx IPolygon.cxx ITexture.cxx IWorld.cxx csspr2d.cpp csobject.cpp csobjvec.cpp csrect.cpp csstrvec.cpp csvector.cpp polyset.cpp dynlight.cpp light.cpp lightmap.cpp cssprite.cpp thing.cpp thingtpl.cpp polygon.cpp polyplan.cpp polytext.cpp portal.cpp camera.cpp inv_cmap.cpp library.cpp msectobj.cpp physics.cpp sector.cpp stats.cpp textrans.cpp texture.cpp wirefrm.cpp world.cpp csloader.cpp command.cpp csview.cpp bsp.cpp math2d.cpp math3d.cpp polyclip.cpp csscript.cpp intscri.cpp primscri.cpp syssound.cpp timing.cpp csendian.cpp fonts.cpp system.cpp tcache.cpp tcache16.cpp csevent.cpp cseventq.cpp csinput.cpp gifimage.cpp image.cpp imageldr.cpp pcx.cpp pngimage.cpp tgaimage.cpp archive.cpp inifile.cpp memheap.cpp memory.cpp parser.cpp sparse3d.cpp token.cpp util.cpp channel.cpp cssound.cpp module.cpp wave.cpp build.cpp collide.cpp collider.cpp collp.cpp overlap.cpp rapid.cpp…and the files implementing SysSystemDriver and your platform-dependent keyboard and mouse drivers.

Files: Driver2d.cpp Fonts.cpp Graph2d.cpp IGraph2d.cxx…and the files implementing your system-dependent csGraphics2d-derived class (and any supporting files).

Files: SoftwareRender.cpp Graph3d.cpp Scan.cpp Scan16.cpp.

Files: D3dCache.cpp Direct3DRender.cpp g3d_d3d.cpp hicache.cpp

Files: Undetermined.

Files: Undetermined.

Currently, each graphics driver exists in it’s own DLL. In addition the associated 2d graphics driver usually resides in a different DLL from the 3d graphics driver. This allows two different 3d graphics drivers to use the same 2d graphics driver without replicating code.

The COM architecture puts an abstraction layer over the names of DLLs through the registry and CLSIDs. The Crystal Space executable never loads a COM server by name. It instead loads it by CLSID, which results a look-up in the registry for the server location. To activate a COM object, a call is made to csCoCreateInstance or csCoGetClassObject. The function csSystemDriver::InitGraphics() method is in charge of creating the 3d graphics driver based on the progID specified in cryst.cfg using csCoGetClassObject. Internally, this loads the associated DLL and calls the 3d graphics driver’s DLL function DllGetClassObject. This in turn retrieves a pointer to the 3d driver’s class object. A COM class object is a singleton COM object that is in charge of creating objects. A call to IGraphicsContextFactory::CreateInstance() actually creates the instance of the 3d driver in memory.

All DLLs must export the following functions to be true COM servers. Driver2D, SoftwareRender, Direct3DRender, GlideRender, and OpenGLRender all export the following symbols from their respective DLLs:

    1. And these DLL functions are required for COM to operate successfully.

Making Your Platform Work – A Mini How-To

Since all 3D drivers are ported or soon-to-be ported, all that’s required to get your platform up and running is to make the 2d graphics drivers COMpatible. All 2D graphics drivers are derived from csGraphics2D, which takes care of a large portion of the COM stuff for you. The COM interface IGraphics2D is implemented through composition, so that you can retain your method signatures while still supporting COM. What you’ll need to do is add the following inside the brackets of your class declaration:

DECLARE_IUNKNOWN()

DECLARE_INTERFACE_TABLE(classname)

These macros are defined in COM.H.

DECLARE_IUNKNOWN() makes declarations for all the IUnknown methods. DECLARE_INTERFACE_TABLE() is used to declare a table that is used by the default implementation of QueryInterface().

You’ll also need to add the following to your C++ file:

BEGIN_INTERFACE_TABLE(classname)

IMPLEMENTS_COMPOSITE_INTERFACE_EX( IGraphics2D, XGraphics2D )

IMPLEMENTS_COMPOSITE_INTERFACE_EX( IGraphicsInfo, XGraphicsInfo )

END_INTERFACE_TABLE()

IMPLEMENT_UNKNOWN_NODELETE(classname)

This gives you a standard implementation of IUnknown (one that doesn’t delete when the last reference is released) and defines an interface table that is used by QueryInterface to determine what interfaces your object supports. The two entries in the interface table indicate that the class implements IGraphics2D and IGraphicsInfo through COM composition using the classes IXGraphics2D and IXGraphicsInfo.

After this is completed, you have a standard implementation for IUnknown, including a working version of QueryInterface.

The next part is the harder part. You’ll have to move your 2d graphics driver our of the Crystal Space EXE and into it’s own DLL. To do this, you’ll have to make sure that the 2d graphics driver doesn’t have any dependencies on the Crystal Space executable. In other words, you can’t access any Crystal Space classes directly from within the 2d graphics DLL. To access Crystal Space, you’ll have to use one of the interfaces used by Crystal Space:

These will provide you with most of what you need. But you might need more than this. For instance, your csSystemDriver-derived class might have specific information that your 2d graphics driver needs to operate. To expose additional functionality, you’ll have to create a system specific interface for your system driver. An example name might be ISystemLinuxInternal. This interface should be exposed by your SysSystemDriver implementation. The interface might look something like this:

interface ISystemLinuxInternal : public IUnknown

{

STDMETHOD(GetSomething)(int& retval) = 0;

STDMETHOD(MakeSomethingHappen)() = 0;

};

Notice that this is a pure abstract class. This defines the binary contract for objects that implement the ISystemLinuxInternal interface. Next, you’ll need to implement ISystemLinuxInternal within your system driver. The most straightforward way is through C++ inheritance:

class SysSystemDriver: public csSystemDriver, public ISystemLinuxInternal

{

. . .

DECLARE_IUNKNOWN()

DECLARE_INTERFACE_TABLE(SysSystemDriver)

};

// in the cpp file:

BEGIN_INTERFACE_TABLE(SysSystemDriver)

IMPLEMENTS_COMPOSITE_INTERFACE(System)

IMPLEMENTS_INTERFACE(ISystemLinuxInternal)

END_INTERFACE_TABLE()

IMPLEMENT_UNKNOWN_NODELETE(SysSystemDriver)

This makes SysSystemDriver a class that supports COM. It will support the interfaces ISystem and ISystemLinuxInternal through the IUnknown method QueryInterface(). You’ll want to make sure to provide an implementation for all methods within ISystemLinuxInternal within your system driver.

Now you’ll want to modify your 2D graphics driver to take in an ISystem* in it’s constructor instead:

SysGraphics2D::SysGraphics2D(ISystem* piSystem)

You can use this ISystem* to retrieve an ISystemLinuxInternal pointer using QueryInterface:

ISystemLinuxInternal* piLinuxSystem = NULL;

piSystem->QueryInterface(IID_ISystemLinuxInternal, void**)&piLinuxSystem);

After that you can use the piLinuxSystem interface pointer to access any system-specific variables you need. Once you are done using interface pointers, or right before they go out of scope, you must make a call to Release:

FINAL_RELEASE(piLinuxSystem)

FINAL_RELEASE(piSystem)

Notice that although the two pointers point to the same underlying object (SysSystemDriver), Release is called twice. That is because AddRef and Release are called on a per-interface basis. Here are the rules for when to call AddRef and Release:

Making AddRef and Release calls when appropriate is essential to having a stable COM architecture. At some point in the future, template smart-pointers will be used to call AddRef and Release when appropriate in most circumstances.

After all dependencies are ripped out (make sure you don’t include any header files from Crystal Space either! only interface include files from Crystal Space), you’ll want to write the DLL functions. The DLL functions required are outlined above, in Crystal Space COM Specifics. A good example implementation exists in SoftwareRender.cpp, or the Windows platform’s Driver2d.cpp.

After all dependencies with Crystal Space are ripped out, you should be able to compile successfully. If you get unresolved external errors during the link process, it probably means that you still have dependencies with Crystal Space.

After the DLL is compiled, you will want to register it using the utility csRegSvr.