This topic on the Active Template Library (ATL) cannot cover all the topics that a developer working with ATL should know to be effective, but it will serve as an introduction to ATL. ATL helps you implement COM objects and saves typing, but it does not excuse you from knowing C++ and how to develop COM objects.
ATL is the recommended framework for implementing COM objects. The ATL code can be combined with Microsoft Foundation Class Library (MFC) code, which provides more support for writing applications. An alternative to MFC is the Windows Template Library (WTL), which is based on the ATL template methodology and provides many wrappers for window classes and other application support for ATL. WTL was originally written by Microsoft and is now available as open source; at the time of writing, version 8.0 is the latest and can be used with Visual Studio 2005. [ATL download]
In this topic
- ATL in Brief
- ATL and Direct To COM
- ATL and Attributes
- Handling Errors in ATL
- Debugging ATL Code
- Boolean Types
- String Types
- Implementing private COM classes (noncreatable)
- Other features in Visual Studio
- ATL References
ATL in Brief
-
ATL Project - Used to initialize an ATL C++ project.
-
ATL Simple Object - Used to create COM objects. In case the Attributed option of the project has been enabled the COM class will be created with the [coclass] attribute which will provide the IDL code at compile time.
-
Add Variable - Used to add properties to interfaces.
-
Add Function - Used to add methods to interfaces; both the Property and Method Wizards require you to know some IDL syntax.
-
Implement Interface - Used to implement stub functions for existing interfaces.
-
Add Connection Point - Used to implement outbound events' interfaces.
Typically, these are accessed by a right-click on a project, class, or interface in Visual Studio Workspace/Class view.
ATL provides base classes for implementing COM objects as well as implementations for some of the common COM interfaces, including IUnknown, IDispatch, and IClassFactory. There are also classes that provide support for ActiveX controls and their containers.
ATL provides the required services for exposing ATL-based COM objects including registration, server lifetime, and class objects.
These template classes build a hierarchy that sandwiches your class. These inheritances are shown below. The CComxxxThreadModel class supports thread-safe access to global, instance, and static data. The CComObjectRootEx class provides the behavior for the IUnknown methods. The interfaces at the second level represent the interfaces that the class will implement; these come in two varieties. The IXxxImpl interface contains ATL-supplied interfaces that also include an implementation; the other interfaces have pure virtual functions that must be fully implemented within your class. The CComObject class inherits your class; this class provides the implementation of the IUnknown methods along with the object instantiation and lifetime control.
The hierarchical layers of ATL
This layer structure allows changes to be made that affect the interaction of the Object and COM, with only minimal changes to your source files. Only the inherited classes must change.
ATL and DTC
For a more detailed discussion on Direct-To-COM (DTC), see section Direct-To-COM Smart Types. Along with smart types, DTC provides some useful compiler extensions you can use when creating ATL-based objects. The functions __declspec and __uuidof are two such functions, but the most useful is the #import command.
COM interfaces are defined in IDL, then compiled by the Microsoft IDL compiler (MIDL.exe). This results in the creation of a type library and header files. The project uses these files automatically when compiling software that references these interfaces. This approach is limited in that, when working with interfaces, you must have access to the IDL files. As a developer of ArcGIS, you only have access to the ArcGIS type library information contained in .olb and .ocx files. While it is possible to engineer a header file from a type library, it is a tedious process. The #import command automates the creation of the necessary files required by the compiler. Since the command was developed to support DTC, when using it to import ArcGIS type libraries, there are a number of parameters that must be passed so the correct import takes place. See section Importing ArcGIS type libraries.
ATL and Attributes
C++ attributes are a feature introduced in MS Visual Studio 2003 (Version 7) which are used to generate C++ code through attribute providers. Attribute providers also generate code for COM classes, and the code is injected by the C++ compiler at compile time. This has the effect of reducing the amount of code that you need to write. In addition, with C++ attributes you no longer need to maintain separate IDL and RGS files, which makes project management simpler. [reference MSDN]
Why using Attributes in ATL projects
The main reason to use attributes in ATL projects is to make implementing COM components easier. In ATL 3 there are three files used to describe a COM component.
-
IDL file - contains the interface description
-
RGS file - for additional registration information
-
the ATL class itself - specifies implemented interfaces through interface map
This distribution of information to different files is prone to errors. C++ attributes provide a more complete and coherent description of components by implementing the component information in one place but also generating code.
How to use Attributes
An attribute is applied to a class or function by declaring it before the code, using square brackets. See table below for a comparison of a coclass definition in ATL 3 and the attributed alternative in ATL 8:
[VCPP] // ATL 3
class ATL_NO_VTABLE CManager: public CComObjectRootEx < CComSingleThreadModel >
, public CComCoClass < CManager, &CLSID_Manager > , public IDispatchImpl <
IEmployee, &IID_IEmployee >
{
public:
DECLARE_REGISTRY_RESOURCEID(IDR_MANAGER)BEGIN_COM_MAP(CManager)
COM_INTERFACE_ENTRY(IEmployee)COM_INTERFACE_ENTRY(IDispatch)END_COM_MAP()
STDMETHOD(DoWork)(BSTR bstrTask);
};
[VCPP] // ATL 8
// in manager.h
[coclass]class ATL_NO_VTABLE CManager: public IEmployee
{
public:
STDMETHODIMP DoWork(BSTR bstrTask);
};
The [coclass] attribute replaces the coclass statement in IDL. Thus the information about a coclass called CManager in the server DLL has been moved from the IDL file to the implementation file. All the information that makes a C++ class a COM class such as:
-
inheritance from CComObjectRootEx, CComCoClass, IDispatchImpl
-
binding an RGS script through the macro DECLARE_REGISTRY_RESOURCEID()
-
information about implemented interfaces through BEGIN_COM_MAP and END_COM_MAP
is provided by the code the [coclass] attribute creates at compile time in ATL 8. For an example of how to implement a COM class using the [coclass] attribute see Walkthrough. For a detailed description about the build process with attributes please read [reference MSDN].
Attributes and IDL
The COM C++ attributes replicate most of the IDL attributes, and there are also attributes that replace what would be statements in IDL. For instance the [coclass] attribute replaces the coclass statement in IDL. For a complete list of attributes that replace IDL attributes and statements see [IDL Attributes].
Attributes and Registration
UUIDs
For registration UUIDs need to be specified for interfaces and coclasses. In ATL 8 this can be done with the [uuid()] attribute which replaces the __declspec(uuid()) in ATL 3. In addition the [module()] attribute takes a uuid parameter. If no UUIDs are specified the compiler will generate the UUIDs for you.
Module
The [module()] attribute is responsible for creating module entry points. With this attribute you can specify if a COM module will be an EXE COM server or a DLL COM server. It also generates the code for the DllRegisterServer() and DllUnregisterServer() methods. The table below shows a list of code generated by the [module()] parameter.
dll |
exe |
service and service_name | |
Entry point |
DllMain |
_tWinMain |
_tWinMain |
_Module |
CAtlDllModuleT<> |
CAtlExeModule<> |
CAtlServiceModuleT<> |
Other exported functions |
DllGetClassObject, DllRegisterServer, DllUnregisterServer, DllCanUnloadNow |
/ |
/ |
In order to implement your own entry point in the module class you need to overwrite the main method that gets injected by the [module] attribute. For an EXE COM Server the code should look like this:
[VCPP] // The module attribute causes WinMain to be automatically implemented for you
[module(exe, name="exe module class")]class CMyClass
{
public:
int WINAPI WinMain(int nShowCmd)
{
//todo: add your code here
return true;
}
};
For a DLL COM Server the code should look like this:
[VCPP] [module(dll, name="dll module class")]
// module attribute now applies to this class
class CMyClass
{
public:
BOOL WINAPI DllMain(DWORD dwReason, LPVOID lpReserved)
{
// add your own code here
return true;
}
};
RGS Files
As well as IDL files, RGS files can be replaced by attributes in ATL 8. To add custom values to the registry you can use the [rdx()] and [registration_script()] attributes. The [registration_script()] attribute can also be used to bind an existing RGS file to the COM class (see example). This attribute overrides the values that would have been added to the registry by default for a class that uses [coclass()].
[VCPP] [coclass, registration_script(script="Counted.rgs")]class CCounted: public
ICount
{
/* other code */
};
Component Categories
While in ATL 3 COM coclasses have been registered into a component category by either:
- placing the GUIDs for component categories beneath an Implemented Categories key in an RGS file
or
- using ATL macros in an objects header file (BEGIN_CATEGORY_MAP, IMPLEMENTED_CATEGORY, END_CATEGORY_MAP)
in ATL 8 you can use the [implements_category()] and [requires_category()] attributes. For example:
[VCPP] [coclass, implements_category("CATID_Insertable")]class CCounted: public ICount
{
/* other code */
};
The list below shows attributes that can be used for registration of COM classes. For an example of attributes used for registration see the Walkthrough.
- [uuid()] - Can be applied to a class with the UUID of the class that is passed in as a parameter in parenthesis.
[coclass, uuid("53A583E1-18B7-4194-A969-8C1BFA7A3F03")]
- [vi_progid()] - Coclasses are associated with version independent ProgIDs using the attribute [vi_progid()].
[coclass, vi_progid("Walkthrough1VC7.ZoomIn")]
- [progid()] - Coclasses are associated with versioned ProgIDs using the attribute [progid()].
[coclass, progid("Walkthrough1VC7.ZoomIn.1")]
- [noncreatable] - This attribute overrides the registration behaviour of the [coclass()] attribute by do ing the following steps:
- Adding an empty UpdateRegistry() method to the class.
- It does not derive the class from CComCoClass<>.
- Adding the coclass statement with the [noncreatable] attribute in the modules's type library.
- [registration_script()] - This attribute can be used to add custom values to the registry or specifies an RGS file that will be used for custom registry values.
[coclass, registration_script(script="Counted.rgs")]
- [rdx()] - This attribute can be used to retrieve, delete or write data to registry values at runtime.
- [implements_category()] - This attribute can be used to specify the category a class implements.
[coclass, implements_category("CATID_Insertable")]
- [requires_category()] - This attribute can be used to specify the category a container requires to be able to contain the class.
Conclusion
There are two reasons to use C++ attributes. The first is that attributes are applied to the implementation of a class, which means that the information about a class is held in just one place rather than being distributed between several files. The second reason is that C++ attributes reduce the amount of code that you have to write. You can choose if you want to use C++ attributes because when used in an ATL project, the ATL attribute provider will only generate ATL code if it does not already exist in your class. This means that you can use and extend your existing ATL 3 code with C++ attributes.
For a more detailed explanation of attributes in ATL 8 see [reference MSDN].
Handling Errors in ATL
It is possible to just return an E_FAIL HRESULT code to indicate the failure within a method; however, this does not give the caller any indication of the nature of the failure. There are a number of Windows-standard HRESULTs available, for example, E_INVALIDARG (one or more arguments are invalid) and E_POINTER (invalid pointer). These error codes are listed in the Windows header file winerror.h. Not all development environments have comprehensive support for HRESULT; Visual Basic clients often see error results as "Automation Error - Unspecified Error". ATL provides a simple mechanism for working with the COM error information object that can provide an error string description, as well as an error code.
When creating an ATL object, the Object wizard has an option to support ISupportErrorInfo. If you toggle the option on, when the wizard completes, your object will implement the interface ISupportErrorInfo, and a method will be added that looks something like this:
[VCPP] STDMETHODIMP MyClass::InterfaceSupportsErrorInfo(REFIID riid)
{
static const IID *arr[]=
{
&IID_IMyClass,
};
for (int i=0; i < sizeof(arr) / sizeof(arr[0]); i++)
{
if (InlineIsEqualGUID(*arr[i], riid))
return S_OK;
}
return S_FALSE;
}
It is now possible to return rich error messages by calling one of the ATL error functions. These functions even work with resource files to ensure easy internationalization of the message strings.
[VCPP] // Return a simple string
AtlReportError(CLSID_MyClass, _T("No connection to Database."), IID_IMyClass,
E_FAIL);
// Get the Error Text from a resource string
AtlReportError(CLSID_MyClass, IDS_DBERROR, IID_IMyClass, E_FAIL,
_Module.m_hInstResource);
To extract an error string from a failed method, use the Windows function GetErrorInfo. This is used to retrieve the last IErrorInfo object on the current thread and clears the current error state.
Although Visual C++ does support an exception mechanism (try ... catch), it is not recommended to mix this with COM code. If an exception unwinds out of a COM interface, there is no guarantee the client will be able to catch this, and the most likely result is a crash.
Debugging ATL Code
In addition to the standard Visual Studio facilities, ATL provides a number of debugging options with specific support for debugging COM objects. The output of these debugging options is displayed in the Visual C++ Output window. The QueryInterface call can be debugged by setting the symbols _ATL_DEBUG_QI, AddRef, and Release calls, with the symbol _ATL_DEBUG_INTERFACES, and leaked objects can be traced by monitoring the list of leaked interfaces at termination time when the _ATL_DEBUG_INTERFACES symbol is defined. The leaked interfaces list has entries like the following:
INTERFACE LEAK: RefCount=1, MaxRefCount=3, {Allocation=10}
On its own, this does not tell you much apart from the fact that one of your objects is leaking because an interface pointer has not been released. However, the Allocation number allows you to automatically break when that interface is obtained by setting the m_nIndexBreakAt member of the CComModule at server startup. This in turn calls the function DebugBreak() to force the execution of the code to stop at the relevant place in the debugger. For this to work, the program flow must be the same.
[VCPP] extern "C"BOOL WINAPI DllMain(HINSTANCE hInstance, DWORD dwReason, LPVOID
/*lpReserved*/)
{
if (dwReason == DLL_PROCESS_ATTACH)
{
_Module.Init(ObjectMap, hInstance, &LIBID_HISTORYLib);
DisableThreadLibraryCalls(hInstance);
_Module.m_nIndexBreakAt=10;
}
else if (dwReason == DLL_PROCESS_DETACH)
{
_Module.Term();
}
return TRUE;
}
There are two ways to debug code that has been injected by attributes. You can view injected code in the Disassembly window of Visual C++ or you can use the /Fx compiler option in order to create a merged source file that contains original and injected code. For further information see the document "Debugging Injected Code" in the MSDN.
Boolean Types
Historically, ANSI C did not have a Boolean data type and used int value instead, where 0 represents false and nonzero represents true. However, the bool data type has now become part of ANSI C++. COM APIs are language independent and define a different Boolean type, VARIANT_BOOL. In addition, Win32 API uses a different bool type. It is important to use the correct type at the appropriate time. The following table summarizes their usage:
Type |
True Value |
False Value |
Where Defined |
When to Use |
bool |
true (1) |
false (0) |
Defined by compiler |
This is an intrinsic compiler type so there is more potential for the compiler to optimize its use. This type can also be promoted to an int value. Expressions (e.g., i!=0) return a type of Bool. Typically used for class member variables and local variables. |
BOOL (int) |
TRUE (1) |
FALSE (0) |
Windows Data Type (defined in windef.h) |
Used with windows API functions, often as a return value to indicate success or failure. |
VARIANT_BOOL (16bit short) |
VARIANT_TRUE (-1) |
VARIANT_FALSE (0) |
COM Boolean values (wtypes.h) |
Used in COM APIs for Boolean values. Also used within VARIANT types; if the VARIANT type is VT_BOOL, then the VARIANT value (boolVal) is populated with a VARIANT_BOOL. Take care to convert a bool class member variable to the correct VARIANT_BOOL value. Often the conditional test "hook - colon" operator is used. For example where bRes is defined as a bool, then to set a result type:
*pVal=bRes ? VARIANT_TRUE : VARIANT_FALSE; |
String Types
Considering that strings (sequences of text characters) are a simple concept, they have unfortunately become a complex and confusing topic in C++. The two main reasons for this confusion are the lack of C++ support for variable length strings combined with the requirement to support ANSI and Unicode character sets within the same code. As ArcGIS is only available on Unicode platforms, it may simplify development to remove the ANSI requirements.
The C++ convention for strings is an array of characters terminated with a 0. This is not always good for performance when calculating lengths of large strings. To support variable length strings, the character arrays can be dynamically allocated and released on the heap, typically using malloc and free or new and delete. Consequently, a number of wrapper classes provide this support; CString defined in MFC and WTL is the most widely used. In addition, for COM usage the BSTR type is defined and the ATL wrapper class CComBSTR is available.
To allow for international character sets, Microsoft Windows migrated from an 8-bit ANSI character string (8-bit character) representation (found on Windows 95, Windows 98, and Windows Me platforms) to a 16-bit Unicode character string (16-bit unsigned short). Unicode is synonymous with wide characters (wchar_t). In COM APIs, OLECHAR is the type used and is defined to be wchar_t on Windows. Windows operating systems, such as Windows NT, Windows 2000, and Windows XP, natively support Unicode characters. To allow the same C++ code to be compiled for ANSI and Unicode platforms, compiler switches are used to change Windows API functions (for example, SetWindowText) to resolve to an ANSI version (SetWindowTextA) or a Unicode version (SetWindowTextW). In addition, character-independent types (TCHAR defined in tchar.h) were introduced to represent a character; on an ANSI build this is defined to be a char, and on a Unicode build this is a wchar_t, a typedef defined as unsigned short. To perform standard C string manipulation, there are typically three different definitions of the same function; for example, for a case-insensitive comparison, strcmp provides the ANSI version, wcscmp provides the Unicode version, and _tcscmp provides the TCHAR version. There is also a fourth version _mbscmp, which is a variation of the 8-bit ANSI version that will interpret multibyte character sequences (MBCS) within the 8-bit string.
[VCPP] // Initialize some fixed length strings
char *pNameANSI="Bill"; // 5 bytes (4 characters plus a terminator)
wchar_t *pNameUNICODE=L "Bill";
// 10 bytes (4 16-bit characters plus a 16-bit terminator)
TCHAR *pNameTCHAR=_T("Bill"); // either 5 or 10 depending on compiler settings
COM APIs represent variable length strings with a BSTR type; this is a pointer to a sequence of OLECHAR characters, which is defined as Unicode characters and is the same as a wchar_t. A BSTR must be allocated and released with the SysAllocString and SysFreeString Windows functions. Unlike C strings, they can contain embedded zero characters, although this is unusual. The BSTR also has a count value, which is stored four bytes before the BSTR pointer address. The CComBSTR wrappers are often used to manage the lifetime of a string.
Beware! Do not pass a pointer to a C style array of Unicode characters (OLECHAR or wchar_t) to a function expecting a BSTR. The compiler will not raise an error as the types are identical. However, the function receiving the BSTR can behave incorrectly or crash when accessing the string length, which will be random memory values.
ipFoo->put_WindowTitle(L"Hello"); // This is bad!
ipFoo->put_WindowTitle(CComBSTR(L"Hello")); // This correctly initializes and passes a BSTR
ipFoo->put_WindowTitle(L"Hello"); // This is bad!
ipFoo->put_WindowTitle(CComBSTR(L"Hello")); // This correctly initializes and passes a BSTR
ATL provides classes for converting strings between ANSI (A), TCHAR (T), Unicode (W), and OLECHAR (OLE). In addition, the types can have a const modifier (C). These classes use the abbreviations shown in brackets with a "C" in front and a "2" between them. For example, to convert between OLECHAR (such as an input BSTR) to const TCHAR (for use in a Windows function), use the COLE2CT conversion class. To convert ANSI to Unicode, use CA2W. For more details see the MSDN document "ATL and MFC string conversion macros".
[VCPP] STDMETHODIMP CFoo::put_WindowTitle(BSTR bstrTitle)
{
USES_CONVERSION;
if (::SysStringLen(bstrTitle) == 0)
return E_INVALIDARG;
::SetWindowText(m_hWnd, OLE2CT(bstrTitle));
return S_OK;
}
Beware! To check if two CComBSTR strings are different, do not use the not equal ("!=") operator. The "==" operator performs a case- sensitive comparison of the string contents; however, "!=" will compare pointer values and not the string contents, typically returning false.
Implementing private COM classes (noncreatable)
Noncreatable classes are COM objects that cannot be created by CoCreateInstance. Instead, the object is created within a method call of a different object, and an interface pointer to the noncreatable class is returned. This type of object is found in abundance in the geodatabase model. For example, FeatureClass is noncreatable and can only be obtained by calling one of a number of methods; one example is the IFeatureWorkspace::OpenFeatureClass method.
One advantage of a noncreatable class is that it can be initialized with private data using method calls that are not exposed in a COM API. Below is a simplified example of returning a noncreatable object:
[VCPP] // Foo is a CoCreateable object
IFooPtr ipFoo;
HRESULT hr=ipFoo.CreateInstance(CLSID_Foo);
// Bar is a noncreatable object, cannot use ipBar.CreateInstance(CLSID_Bar)
IBarPtr ipBar;
// Use a method on Foo to create a new Bar object
hr=ipFoo->CreateBar(&ipBar);
ipBar->DoSomething();
The steps required to change a cocreatable ATL class into a noncreatable class are shown below. Note that it is much simpler to change a COM class to noncreatable if you are working with attributes:
- noncreatable COM class with attributes
Add the [noncreatable] attribute to the header file of the COM class:
// in manager.h
[coclass, noncreatable]class ATL_NO_VTABLE CBar
{
public:
STDMETHODIMP DoSomething(BSTR bstrTask);
};
- noncreatable COM class without attributes
- Add "noncreatable" to the IDL file's coclass attributes.
[uuid(DCB87952 - 0716-4873-852B - F56AE8F9BC42), noncreatable] coclass Bar
{
[default] interface IUnknown;
interface IBar;
};
- Change the class factory implementation to fail any cocreate instance of the noncreatable class. This happens via ATL's object map in the main DLL module.
BEGIN_OBJECT_MAP(ObjectMap)OBJECT_ENTRY(CLSID_Foo, CFoo) // Creatable object
OBJECT_ENTRY_NON_CREATEABLE(CLSID_Bar, CBar) // Noncreatable object
END_OBJECT_MAP()
- Optionally, the registry entries can be removed. First, remove the registry script for the object from the resources (Bar.rgs in this example). Then change the class definition DECLARE_REGISTRY_RESOURCEID(IDR_BAR) to DECLARE_NO_REGISTRY().
To create the noncreatable object inside a method, use the CComObject template to supply the implementation of CreateInstance.
[VCPP] // Get NonCreatable object Bar (implementing IBar) from COM object Foo
STDMETHODIMP CFoo::CreateBar(IBar **pVal)
{
if (pVal == 0)
return E_POINTER;
// Smart pointer to non-creatable object Bar
IBarPtr ipBar=0;
// C++ Pointer to Bar, with ATL template to supply CreateInstance implementation
CComObject < CBar > *pBar=0;
HRESULT hr=CComObject < CBar > ::CreateInstance(&pBar);
if (SUCCEEDED(hr))
{
// Increment the ref count from 0 to 1 to protect the object
// from being released in any initialization code.
pBar->AddRef();
// Call C++ methods (not exposed to COM) to initialize the Bar object
pBar->InitialiseBar(10);
// QI to IBar and hold a smart pointer reference to the object Bar
hr=pBar->QueryInterface(IID_IBar, (void **) &ipBar);
pBar->Release();
}
// return IBar pointer to the caller
*pVal=ipBar.Detach();
return S_OK;
}
Other features in VS8
This sections lists some further features of VS8:
Build Configurations: There are two default build configurations in VS8; these are Debug- and Release builds based on UNICODE character set which ArcGIS is based on.
Safe array support: This is available with CComSafeArray and CComSafeArrayBound classes.
Module level global: The module-level global CComModule _module defined in ATL 3 has been split into a number of related classes, for example, CAtlComModule and CAtlWinModule. To retrieve the resource module instance, use the following code: _AtlBaseModule.GetResourceInstance();, which is declared in atlcore.h. For further information read the MSDN document ATL Module Classes.
String support: General variable length string support is available through CString in ATL. This is defined in the header files atlstr.h and cstringt.h. If ATL is combined with MFC, this defaults to MFC's CString implementation.
Filepath handling: A collection of related functions for processing the components of filepaths is available through the CPath class defined in atlpath.h.
ATLServer: This is a selection of ATL classes designed for writing Web applications, XML Web services, and other server applications.
#import issues: When using #import, a few modifications are required. For example, the #import of esriSystem requires an exclude or rename of GetObject, and the #import of esriGeometry requires an exclude or rename of ISegment.
ATL References
The MSDN provides a wealth of documentation, articles, and samples that are installed with Visual Studio products. ATL reference documentation for Visual Studio is under ATL article overview.
Additional information is also available on the MSDN Web site at http://www.msdn.microsoft.com/.
You may also find the following books to be useful:
-
Grimes, Richard. ATL COM Programmer's Reference. Chicago: Wrox Press Inc., 1988.
-
Grimes, Richard. Professional ATL COM Programming. Chicago: Wrox Press Inc., 1988.
-
Grimes, Richard, Reilly Stockton, Alex Stockton, and Julian Templeman. Beginning ATL 3 COM Programming. Chicago: Wrox Press Inc., 1999.
-
King, Brad, and George Shepherd. Inside ATL. Redmond, WA: Microsoft Press, 1999.
-
Rector, Brent, Chris Sells, and Jim Springfield. ATL Internals. Reading, MA: Addison-Wesley, 1999.