Library Zone Articles
External Articles
Byte Size

Discovery Zone Catalogue
Diary
Links
Bookstore
Interactive Zone Ask the Gurus
Discussion Groups
Newsletters
Feedback
Etc Cartoons
Humour
COMpetition
Advertising
Site Builder ASP Web Ring ASP Web Ring

Click here
The Developer's Resource & Community Site
COM XML ASP Java & Misc. NEW: VS.NET
International This Week Forums Author Central Find a Job

Exploring COM Threading and Apartments

Download print article

Exploring COM Threading and Apartments

In this article I plan to discuss COM threading and apartments, a subject that is not without coverage by any means. In fact, you can open up just about any book on COM and find it explained to some extent.

So what makes this text any different?

It's my opinion that no matter how many books are read on a subject, how many magazine articles are consumed, or how many times someone else explains it, nothing can educate like seeing and doing something for yourself. Case in point, the subject covered in this article. Apartments and threads have been discussed at length throughout the ever-growing COM literary world, and yet questions related to the subject continue to be posted to the ATL and DCOM mailing lists. In fact, although I've never bothered to keep track, I would venture a guess that thread and apartment related questions have been the most common postings on the lists since their inception.

Usually the question starts with someone trying their best to explain how their process creates some number of threads that enter different types of apartments, and then in turn those threads create COM objects with a variety of threading models. And to make a long story short, the program won't run correctly and they are having a heck of a time debugging it.

Which brings me nicely into what this article is all about; Figuring it out for yourself. I guarantee you that by the time we are through here, whatever you don't quite fathom about threads and apartments in COM, you will have the tools and knowledge you need to observe the mechanisms involved first hand. And through that observation gain a level of understanding that can rarely be attained through book study alone.

There is one catch though; you have to be willing to write some code and spend a little time in the debugger.

The Basics Revisited

It probably wouldn't be fair to jump right in without first covering the basics. So, here's a very brief explanation. Those of you familiar with COM apartments can feel free to skip this section.

In COM there are effectively two and a half different apartment types: Single-threaded (STA), Multi-threaded (MTA), and the "Main" STA. Each process has at most one MTA and one Main STA, and any number of additional STAs. Any thread that wishes to have access to a COM object through its methods must enter some kind of an apartment. A thread enters an apartment by making a call to CoInitialize(Ex) and specifying the appropriate parameters. The first thread in a process to enter an STA has in effect just created the Main STA. That's really all that makes it "Main".

Apartments differ primarily in how incoming COM function calls are handled. Single-threaded apartments use a Windows message queue to serialize access to COM objects residing within, and the MTA simply lets any thread in at any time. The bottom line here is that MTA resident COM objects assume the responsibility for synchronization; STA residents don't have to worry about it.

Whenever a COM activation request (i.e. CoCreateInstance) or an interface method call results in an interface pointer being moved from one apartment to another (referred to as exporting and importing an interface), a proxy is instantiated in the destination apartment, and a corresponding stub is created in the source apartment. This proxy and stub pair maintains a connection channel between the two apartments on behalf of the interface implementation and the thread accessing that implementation. Parameters used in subsequent method calls (including interfaces) are transmitted between the apartments by COM.

That's the nutshell version of it. For a really deep explanation of the mechanisms behind inter-apartment method calls I would recommend either "Essential COM" by Don Box or Dr. Al Major's excellent book "COM IDL and Interface Design."

Going a little deeper

Ok, so the first question here is "How does one go about observing this whole phenomenon?" Well first we have to take a little closer look at what goes on when an interface pointer gets transmitted between apartments. In order for an interface pointer to be safely accessible from outside of the apartment it originated from, it must first be transformed into some kind of apartment independent representation. COM allows this to be achieved in a number of ways; one method used by in-proc servers is the API call CoMarshalInterThreadInterfaceInStream. Which has to be the longest name in the entire Win32 API. The exact footprint of this function is as follows:


HRESULT CoMarshalInterThreadInterfaceInStream(REFIID rIid, 
                                              IUnknown *pUnk, 
                                              IStream **ppStrm);

Where rIid is a reference to the interface's IID, pUnk is the interface itself, and ppStrm is pointer to an IStream pointer to store the resulting apartment independent representation of the interface.

Once this long-named function has been successfully called, any thread in any apartment in the process can then gain safe access to the interface pointer by somehow obtaining access to the IStream pointer (oft times via a global variable) and making a corresponding call to CoGetInterfaceAndReleaseStream. Which, as the name implies, extracts the original interface and makes a call to IUnknown::Release on the IStream pointer that contained it.

MEOW

Which brings me to the next question: Just what ends up inside that stream? The answer, believe it or not, is something called a MEOW packet. I've heard a couple of guesses at what MEOW stands for, but I don't think anyone knows for sure. As for me, I view the answer as something akin to my wife procuring my checkbook before going shopping; I'm just better off not knowing. But I digress…

Never mind the name, it's what's inside the MEOW packet that counts. And luckily we can find this out from the DCOM wire protocol. Figure 1 shows a MEOW packet's structure.

MEOW

FLAGS

STD FLAGS

CpublicRefs

OXID

OID

IPID

cch

secOffset

Host Address

Security Info

Figure 1 - MEOW Packet Internals

As you can see, there's a bunch of stuff in there. In fact it should rather evident from the contents of the MEOW packet just how location transparency in DCOM is accomplished. Everything one would need to locate the implementation of an interface is in there. The portion of interest to us however starts at the STD FLAGS portion and ends with the inclusion of the IPID. This chunk of data is known as a STDOBJREF. I should point out here that this only applies for standard marshalling. But for the purposes of this article that's all we need to know.

The two parts of most interest within the context of this article are the OXID, and the OID. The OXID is the "apartment identifier" and the OID is the "object identifier". Both of these values are unique for a specific apartment and interface implementation respectively.

So, here's the trick: if we can get a hold of these values then we have the ability to identify the unique apartment and unique object from which an interface originated. And with a little creative coding, we can add that functionality to any COM object we implement. Learning why and when apartments are created and the rules concerning their creation would then just be a matter of coming up with some interesting activation scenarios and then inspecting the OXID and OID values in the debugger. For example, we could have an STA-housed component instigate the activation of a component marked as free threaded in the registry, and observe first hand that they are indeed housed in different apartments. And then perhaps have that MTA-housed component create multiple instances of some COM implementations that are marked as single threaded. And then check out how COM handles such a situation. (The answer, by the way, may surprise you.)

Wire representation is the key

The first step here is to correctly get the OID and OXID out of the STDOBJREF. Which raises a lot of questions. Where are they located? How big are they? And just how to we go from an IStream pointer to a bunch of bytes that we can get at?

Fortunately, the DCOM wire specification is fully documented and openly available. And most of the data mentioned is defined in the IDL file named obase.idl.

Remember also that the MEOW information is stuck inside of an IStream implementation, so easily viewing the contents of the stream in the debugger requires a little handiwork. Fortunately there just so happens to be an API call that will put the contents of an IStream into a chunk of global memory. Not surprisingly it's called GetHGlobalFromStream and the definition is shown below:

Figure 2 - MEOW in memory
Figure 2 - MEOW in memory

As it turns out, both the OXID and the OID are 64 bit integers, so we need to use the appropriate type to store them. Here's an example of how a COM implementation class could fetch it's own OID and OXID after activation has occurred:


HGLOBAL hGlobal = 0;
CComPtr<IStream> pStream;
HRESULT hr(CoMarshalInterThreadInterfaceInStream(IID_IUnknown,
                                                 this, 
                                                 &pStream));

// NOTE: m_uliOXID and m_uliOID are both of type ULARGE_INTEGER
if(SUCCEEDED(hr))
{
	// NOTE: Not really necessary to convert to HGLOBAL
	// in order to read stream, it just makes it easier
	// to look at the data in the debugger.
      hr = GetHGlobalFromStream(pStream, &hGlobal);
if(SUCCEEDED(hr))
	{
		// Gain access to the global memory
		BYTE *pGlobal = (BYTE*)GlobalLock(hGlobal);
			
		// Suck out the OXID from the MEOW packet
		WORD One = MAKEWORD(pGlobal[32], pGlobal[33]);
		WORD Two = MAKEWORD(pGlobal[34], pGlobal[35]);
		m_uliOXID.LowPart = MAKELONG(One, Two);
			
		One = MAKEWORD(pGlobal[36], pGlobal[37]);
		Two = MAKEWORD(pGlobal[38], pGlobal[39]);
		m_uliOXID.HighPart = MAKELONG(One, Two);

		// Suck out the OID from the MEOW packet
		One = MAKEWORD(pGlobal[40], pGlobal[41]);
		Two = MAKEWORD(pGlobal[42], pGlobal[43]);
		m_uliOID.LowPart = MAKELONG(One, Two);
			
		One = MAKEWORD(pGlobal[44], pGlobal[45]);
		Two = MAKEWORD(pGlobal[46], pGlobal[47]);
		m_uliOID.HighPart = MAKELONG(One, Two);

		// Release the global memory
		GlobalUnlock(hGlobal);
	}
}  

After having pulled this off it's now possible to reveal the unique apartment ID and object ID to any caller who wants to know. I should say here that doing this for any reason other than educating yourself or debugging some code is a bad idea. The idea of being able to "turn off" location transparency goes against the philosophy of COM. So using something like this as functional production code may cause the Gods of COM to come down from Mount Olympus and hurl lightning bolts in your general direction.

Educational Hacking

Being able to determine the unique apartment where a COM implementation is being housed helps in understanding the apartment layout of a process. But there's some other low hanging fruit we can use to help understand the nature of COM. Thread Id's can be eye-opening for a COM newbie when inspected at the appropriate time. As a matter of fact, just watching how many threads are in your process at any given time can aide in the understanding of just when the RPC thread pool comes into play. Also paying attention to TLS access can also give a little insight as to how the underlying system code associates threads and apartments.

To illustrate this I've captured all that functionality into an interface, and provided a default implementation as well as an example of how to use it. By reading the code and the accompanying comments, it should be fairly clear just how it works.

IApartmentInfo

The following IDL code describes an interface for revealing interesting COM related information:


//////////////////////////////////////////////////////////////////////
// ApartmentInfo.idl - provides useful debugging and educational info
//////////////////////////////////////////////////////////////////////

import "oaidl.idl";
import "ocidl.idl";

[
	object,
	uuid(90633D20-D63A-11d2-953E-0004AC868400),
	pointer_default(unique)
]
interface IApartmentInfo : IUnknown
{
	HRESULT GetThreadId([out] DWORD *pdwThreadId);
	HRESULT GetApartmentId([out] ULARGE_INTEGER *puliOXID);
	HRESULT GetObjectId([out] ULARGE_INTEGER *puliOID);
	HRESULT GetTLSEntryCount([out] DWORD *pdwCnt);
	HRESULT GetTLSEntries([out] DWORD *pdwCnt, [out] DWORD **ppdwArray);
};

As you can see, there's not much to the interface. It's really just a collection of properties that can be retrieved and read. To help explain it, let's take a look at some functioning code. I've whipped up a default implementation for the interface in a file called "ApartmentInfoImpl.h", so let's go over what that comprises.

IApartmentInfoImpl

First we have the class declaration. It essentially mirrors the interface. With the exception of the Init() function, which is intended to ensure that the object is in a viable state before it is used. ATL's FinalConstruct method is a perfect spot to exercise that code.

Also, the class itself is template parameterized on the intended interface, like the XXXOnSTLImpl classes found in ATL.


/////////////////////////////////////////////////////
// ApartmentInfoImpl.h
// See ApartmentInfo.idl for interface definition
// Provides default implementation of IApartmentInfo
/////////////////////////////////////////////////////

//
// IApartmentInfoImpl
// Provides default implementation of the IApartmentInfo interface
//
template <typename Base>
class IApartmentInfoImpl : public Base
{
	public:

		IApartmentInfoImpl()
		{
			m_uliOXID.HighPart = m_uliOXID.LowPart = 0;
			m_uliOID.HighPart = m_uliOID.LowPart = 0;
		}

		// Implementation specific
		HRESULT Init();

		// IApartmentInfo Methods
		STDMETHOD(GetThreadId)(DWORD *pdwThreadId);
		STDMETHOD(GetApartmentId)(ULARGE_INTEGER *puliOXID);
		STDMETHOD(GetObjectId)(ULARGE_INTEGER *puliOID);
		STDMETHOD(GetTLSEntryCount)(DWORD *pdwCnt);
		STDMETHOD(GetTLSEntries)(DWORD *pdwCnt, DWORD **ppdwArray);

	protected:

		ULARGE_INTEGER m_uliOXID;
		ULARGE_INTEGER m_uliOID;
};

Here is the Init() function itself. Which is the same code seen previously in the explanation of OID and OXID retrieval from an HGLOBAL. Since these values will remain constant throughout the lifetime of this object, we can do the work now and just give out the values after that.


//
// Init()
// Parameters: NONE
//
// Marshalls the objects IUnknown into a stream and then parses it for
// both the OXID and OID. Intended to be called from
// CComObject::FinalConstruct()
// NOTE: The values returned from this method are only valid for in-process
//       implementations.
//
template <typename Base>
HRESULT
IApartmentInfoImpl<Base>::Init()
{
	HGLOBAL hGlobal = 0;
	CComPtr<IStream> pStream;
	HRESULT hr(CoMarshalInterThreadInterfaceInStream(IID_IUnknown,
		this, &pStream));
	if(SUCCEEDED(hr))
	{
	hr = GetHGlobalFromStream(pStream, &hGlobal);
		if(SUCCEEDED(hr))
		{
			// Gain access to the global memory
			BYTE *pGlobal = (BYTE*)GlobalLock(hGlobal);

			// Suck the OXID out of the MEOW packet
			WORD One = MAKEWORD(pGlobal[32], pGlobal[33]);
			WORD Two = MAKEWORD(pGlobal[34], pGlobal[35]);
			m_uliOXID.LowPart = MAKELONG(One, Two);
			
			One = MAKEWORD(pGlobal[36], pGlobal[37]);
			Two = MAKEWORD(pGlobal[38], pGlobal[39]);
			m_uliOXID.HighPart = MAKELONG(One, Two);

			// Suck the OID out of the MEOW packet
			One = MAKEWORD(pGlobal[40], pGlobal[41]);
			Two = MAKEWORD(pGlobal[42], pGlobal[43]);
			m_uliOID.LowPart = MAKELONG(One, Two);
			
			One = MAKEWORD(pGlobal[44], pGlobal[45]);
			Two = MAKEWORD(pGlobal[46], pGlobal[47]);
			m_uliOID.HighPart = MAKELONG(One, Two);

			// Release the global memory
			GlobalUnlock(hGlobal);
		}
	}

	return hr;
}

The function GetThreadID() is intended to reveal to a client if the object is being accessed on the same thread that was used to instigate the method call. Clients can use the API call GetCurrentThreadId() prior to calling this interface method. A subsequent comparison of the two values will reveal whether or not COM handled the call on the clients behalf.


//
// GetThreadID(DWORD *pdwID)
// Parameters: pdwID - The thread id of the active thread.
//
// Returns the active thread id to the caller. Intended to allow
// clients to determine whether their thread is directly
// accessing the underlying interface implementation.
//
template <typename Base>
STDMETHODIMP
IApartmentInfoImpl<Base>::GetThreadId(DWORD *pdwThreadId)
{
	*pdwThreadId = GetCurrentThreadId();
	return S_OK;
}

These two methods simply return the values obtained from the previous initialization.


//
// GetApartmentID(POXID_VAL pOXID)
// Parameters: pOXID - The OXID of the apartment this object resides in.
//
// Returns the unique apartment identifier in which the object is residing.
//
template <typename Base>
STDMETHODIMP
IApartmentInfoImpl<Base>::GetApartmentId(ULARGE_INTEGER *puliOXID)
{
	HRESULT hr(E_POINTER);

	if(puliOXID)
	{
		puliOXID->HighPart = m_uliOXID.HighPart;
		puliOXID->LowPart = m_uliOXID.LowPart;
		hr = S_OK;
	}

	return hr;
}
//
// GetObjectID(POID_VAL pOID)
// Parameters: pOID - The apartment wide identifier of this object.
//
// Returns the apartment wide identifier of the object implementing
// a given interface.
//
template <typename Base>
STDMETHODIMP
IApartmentInfoImpl<Base>::GetObjectId(ULARGE_INTEGER *puliOID)
{
	puliOID->HighPart = m_uliOID.HighPart;
	puliOID->LowPart = m_uliOID.LowPart;

	return S_OK;
}

The next two functions, GetTLSEntryCount and GetTLSEntries, can be used to inspect thread local storage for the thread currently accessing the object.


//
// GetTLSEntryCount(DWORD *pdwCnt)
// Parameters: pdwCnt - The total number of entries in TLS for this thread.
//
// Returns the current number of entries within this thread's storage.
//
// NOTE: TLS_MINIMUM_AVAILABLE is defined as 64 in winnt.h
//
template <typename Base>
STDMETHODIMP
IApartmentInfoImpl<Base>::GetTLSEntryCount(DWORD *pdwCnt)
{
	for(DWORD x = 0; x < TLS_MINIMUM_AVAILABLE; x++)
	{
		if((DWORD)TlsGetValue(x))
			(*pdwCnt)++;
	}

	return *pdwCnt ? S_OK : S_FALSE;
}

//
// GetTLSEntries(DWORD *pdwCnt, DWORD **ppdwArray)
// Parameters: pdwCnt - Address of variable to receive the
// length of the array
//             ppdwArray - Address of pointer to allocate array of DWORDs
//
// Returns an array of the entries within this thread's storage.
//
template <typename Base>
STDMETHODIMP
IApartmentInfoImpl<Base>::GetTLSEntries(DWORD *pdwCnt, DWORD **ppdwArray)
{
	if(*ppdwArray)
		return E_INVALIDARG;

	HRESULT hr = GetTLSEntryCount(pdwCnt);
	if(hr != S_OK)
		return hr;

	*ppdwArray = (DWORD*)CoTaskMemAlloc((*pdwCnt) * sizeof(DWORD));
	if(!*ppdwArray)
		return E_OUTOFMEMORY;

	DWORD dwOutCnt = 0;
	for(DWORD x = 0; x < TLS_MINIMUM_AVAILABLE; x++)
	{
		DWORD dwVal = (DWORD)TlsGetValue(x);
		if(dwVal)
		{
			(*ppdwArray)[dwOutCnt++] = dwVal;
			if(dwOutCnt == *pdwCnt)
				break;
		}
	}

	return S_OK;
}

And finally, here's a pair of ATL-based COM objects that use the preceding code to implement the IApartmentInfo interface.

One that resides in an STA and one that resides in the process wide MTA. Note the call to Init() from FinalConstruct.


/////////////////////////////////////////////////////////////////////////////
// CSTAObj
/////////////////////////////////////////////////////////////////////////////

class ATL_NO_VTABLE CSTAObj :
	public CComObjectRootEx<CComSingleThreadModel>,
	public CComCoClass<CSTAObj, &CLSID_CoSTAObj>,
	public IApartmentInfoImpl<IApartmentInfo>
{
	public:

	CSTAObj() {}
	virtual ~CSTAObj() {}

	DECLARE_REGISTRY(CSTAObj, "STAObj", "STAObj.1", 0,
			THREADFLAGS_APARTMENT)

  HRESULT FinalConstruct() { return Init(); }

	BEGIN_COM_MAP(CSTAObj)
		COM_INTERFACE_ENTRY(IApartmentInfo)
	END_COM_MAP()
};

/////////////////////////////////////////////////////////////////////////////
// CMTAObj
// NOTE: THREADFLAGS_FREE does not normally exist in ATLBASE.H
// It has been added for convenience when not using RGS files via the
// creation of a class CComFreeThreadedModule : public CComModule
/////////////////////////////////////////////////////////////////////////////
class ATL_NO_VTABLE CMTAObj :
	public CComObjectRootEx<CComMultiThreadModel>,
	public CComCoClass<CMTAObj, &CLSID_CoMTAObj>,
	public IApartmentInfoImpl<IApartmentInfo>
{
	public:

		CMTAObj() {}
		virtual ~CMTAObj() {}
		DECLARE_REGISTRY(CMTAObj, "MTAObj", "MTAObj.1", 0,
			THREADFLAGS_FREE)

	HRESULT FinalConstruct() { return Init(); }

	BEGIN_COM_MAP(CMTAObj)
			COM_INTERFACE_ENTRY(IApartmentInfo)
		END_COM_MAP()
};

OK, so having gotten past all that code, let's answer the key question here:
How do we go about using this to learn something about COM apartments and threads?

In order to best exhibit the code usage, I'll run through a scenario and show some of the client code as well.

Using the Code

Ok, so let's cook up a scenario and show some example client code.

How about an STA based client that creates an MTA based object and an STA based object? A straightforward scenario, but it serves the purpose. Inventing more interesting activation scenarios is an exercise left to the reader. (I've always wanted to say that!)

For example, try having an STA client create an MTA object, which in turn creates some N number of STA objects. The apartment(s) where the STA objects end up existing may surprise you.

So the client activation code for our simple scenario would look something like the following:


void CApartmentInfoClient::CreateObjects()
{
	HRESULT hr(CoInitialize(0));
	CComPtr<IClassFactory> pFact;
	hr = CoGetClassObject (CLSID_CoMTAObj,
			CLSCTX_INPROC_SERVER, 0, IID_IClassFactory,
(void**)&pFact);
	if(SUCCEEDED(hr))
	{
		CComPtr<IApartmentInfo> pAI;
		hr = pFact->CreateInstance(0, IID_IApartmentInfo,
			(void**)&pAI);
		if(SUCCEEDED(hr))
		{
			// A nifty trick to check if we are holding a proxy...
			CComPtr<IMultiQI> pPM;
			hr = pAI->QueryInterface(IID_IMultiQI, (void**)&pPM);

		// Who's thread is accessing the object?
			DWORD dwThreadID = GetCurrentThreadId();  // My thread
			DWORD dwObjectThreadID(0);
			hr = pAI->GetThreadId(&dwObjectThreadID);
					// Who's thread?
			_ASSERT(SUCCEEDED(hr));

		// Apartment number please.
			ULARGE_INTEGER uliOXID;
			ZeroMemory(&uliOXID, sizeof(ULARGE_INTEGER));
			hr = pAI->GetApartmentId(&uliOXID);
			_ASSERT(SUCCEEDED(hr));
		}
	}

	// Make sure a call to Release() happens
	pFact = NULL;
// Create an MTA object and see what kind of values we get…
	hr = CoGetClassObject(CLSID_CoMTAObj, CLSCTX_INPROC_SERVER, 0,
		IID_IClassFactory,
(void**)&pFact);
	if(SUCCEEDED(hr))
	{
		CComPtr<IApartmentInfo> pAI;
		hr = pFact->CreateInstance(0, IID_IApartmentInfo,
			(void**)&pAI);
		if(SUCCEEDED(hr))
		{
			// A nify trick to check if we are holding a proxy...
			CComPtr pPM;
			hr = pAI->QueryInterface(IID_IMultiQI, (void**)&pPM);

		// Who's thread is accessing the object?
			DWORD dwThreadID = GetCurrentThreadId();
			DWORD dwObjectThreadID(0);
			hr = pAI->GetThreadId(&dwObjectThreadID);
			_ASSERT(SUCCEEDED(hr));

		// Apartment number please.
			ULARGE_INTEGER uliOXID;
			ZeroMemory(&uliOXID, sizeof(ULARGE_INTEGER));
			hr = pAI->GetApartmentId(&uliOXID);
			_ASSERT(SUCCEEDED(hr));
		}
	}

	// Make sure a call to Release() happens 'cause scope alone
	// won't do it here
	pFact = NULL;
		CoUninitialize();
} 

Running this code under the debugger and inspecting the contents of the variables is all you need to do. Notice the QueryInterface call for IMultiQI? That's an undocumented interface that is exposed on proxies. If the object you are interacting with exposes that interface, you are talking to a proxy.

Well that's about it. Given the code snippets and the explanations you should be able to tinker to your heart's delight with all kinds of activation scenarios. And hopefully learn something new for the effort. If you find yourself wanting more, you might try exploring Thread Environment Blocks (TEB) and Channel Hooks.


What do you think of this article?

Have your say about the article. You can make your point about the article by mailing [email protected] (If you haven't allready joined, you can join by going to onelist.com/community/dev-com).

You can also write a review. We will publish the best ones here on this article. Send your review to [email protected].

Mail a question to the author!!

As part of the IDevResource commitment to Open Publishing, all of our authors are available to answer all of your trickiest questions at Author Central. For information about the authors, or to mail a question, visit them at Author Central.


Power your site with idr newswire

Contribute to IDR:

To contribute an article to IDR, a click here.

To contact us at IDevResource.com, use our feedback form, or email us.

To comment on the site contact our webmaster.

Promoted by CyberSavvy UK - website promotion experts

All content © Copyright 2000 IDevResource.com, Disclaimer notice

Join the Developers Webring

Visit the IDR Bookstore!

WTL Introduction

Visit our NEW WTL Section