DevGuy's C++ COM Tips


Self-service advertising system

30 Jul 2004 23:32

Tip Library
C++ Tips
MSXML DOM Tips
Tom's ATL FAQ
MVP VC FAQ

ATL Architecture and Internals

Effective C++

Public COM+ Discussion

Free COM Tools

Automation Considerations and Datatypes

Returning Objects

Returning Strings

L"" is not a BSTR

Use ASSERTs with care

COM Strings

Datatypes

COM Strings

Store all IDL files in one directory

Registering TLBs

Uninitializing COM in Destructors Considered Dangerous

Don't Modify Error Object In Release()

Multiple Dispatch Interfaces on one Object

Common Service Errors

Common DCOM Errors

List interfaces in CoClass in IDL

Release interfaces in FinalRelease

Put Constructor Logic in FinalConstruct

Common Linker Errors

Turn an in-proc DLL into an out-of-proc server

Single-Instance Servers

Free non-retval output parameters

Initialize Output Parameters

MSDN Topics

Naming Conventions

CComBSTR and CComVariant Memory Leaks

Initialize COM

ATL Usage

Type Libraries

Use #import

Export TLB

Import TLB, not IDL Files

Don't #include TLH Files

Using CComVariant Properly

DCOM

Resources

Object Instantiation

Robustness

Threading

Implementing existing interfaces

Global Variables Holding Interface Pointers

ATL Singletons

Error Handling

Beware the Connection Point Implementation Wizard

Need to Automate Your Java, Visual C++, and VS.NET Builds?

Try the Programmers' Canvas Toolkit


Help!

Post your feedback and COM+ questions in the COM+ forum.


Great Books on COM and ATL

Effective COMEssential COMCOM+ Programming
Professional NT ServicesDeveloper's Workshop to COM and ATL 3.0
Beginning ATL 3 COM ProgrammingATL Internals


Introduction

COM (and its successor, COM+) is one of the most successful object technologies in the relatively brief history of the computer industry.  If you hear someone flippantly mention that there's nothing to COM programming, they're either a genius or they have never written a COM object in C++.  COM works, as long as you do your homework first.  Otherwise you're in for a difficult learning experience. 

These pages represent an attempt to chronicle the not-so-obvious facts of COM programming that we have discovered over the years.  All of these points have been proven by experience and verified by experts.  However, if you discover some untruths here, please email us.


Free COM Tools

  • RegAll -- Register OCXs, DLLs, and TLBs Recursively

  1. Install ComUtils

  2. Run the command dgregall for instructions:

    Usage:

    1) The directory to process. Also processes subdirectories.
    2) Optional - Pass in the name of the directory that the files
    must be found in (e.g., Debug). Pass in "." as the first
    parameter to process the current directory.

    Switches:
    [-v] Verbose mode -- display all dialog boxes from
    regsvr32
     

  3. dgreglib.exe registers type libraries and may be used

  4. The source to dgreglib.exe is in the CVS module pctk/reglib.

  5. dgregall.pl is the source for dgregall.exe and is in the CVS module RegAll


MSDN Topics on COM/ATL

Revised on: 2001-04-16


Use ASSERTs with care

Revised on: Feb 13 2001

The ASSERT, ASSERTE, ATLASSERT, etc. macros can easily be overused.  These macros are actually removed from release builds, so they provide no value whatsoever in these types of builds.

Do not use ASSERT macros to test error return codes.  The macros' only purpose is to assert something which should ALWAYS must true and CAN NEVER be false.  Since failures can occur at run-time, ASSERTs should never be used to test for them.  Otherwise release builds will not know when a failure occurred.

An appropriate use of ASSERT is to test input parameters in internal methods or functions.  For example, a method can test that a passed-in pointer is not NULL.  However, it is considered bad form to ASSERT on COM method parameters and properties.  Once you ship your COM object, you have no idea who will call it.  Your customers will experiment with the object and will undoubtedly send bad data to it.  Remember:  ASSERTs are removed from release builds.  Your object should catch bad data before it does something bad with it, such as crash the process.  Crashing a customer's processes is never acceptable, even if they give you bogus inputs (within reason).

Here is how code should test input parameters:

if (!pVal) return E_POINTER;


COM Strings

Revised on: 2000-09-23

See the Entire Page Dedicated to COM Strings

Store all IDL Files in the Same Directory

Revised on: 2000-09-25

  • If IDL files import other IDL files, you should put all the files in the same directory
  • Import statements in IDL files should refer to "naked" IDL file names
  • For an explanation, see here
  • Go to the project settings and choose "all configurations"
  • Click the Resources tab.  In the include directories enter the relative path to the IDL file directory.
  • Click the C+ tab.  Select Preprocessor from the drop-down.
  • Set the "include directories" to include the IDL directory
  • Click on the IDL file in the project settings window
  • Click on the MIDL tab
  • Output the TLB file to IDL file directory
  • Output the header file to the current directory
  • Set the Additional include directories to the IDL file directory


Registering Type Libraries

Revised on: 2000-09-28

When using type-library marshaling, it's an absolute requirement that the type libraries be registered on the client and server machines.  Sometimes you want to register the type library for a server on a client without actually installing the server components.  Here's a free type-library registering program.  Run dgreglib.exe with the path of a TLB file.


Don't Modify Error Object In Release()

Revised on: 2000-10-14

It's a very bad idea to clear or set the error info object in Release().

Corollary:

It's a very bad idea to clear or set the error info object in a destructor.  Note that it's sometimes not so easy to avoid doing this because destructors may call methods on COM objects.  Code that does this should preserve the error object that was present when Release() was called.


Uninitializing COM in Destructors Considered Dangerous

Revised on: 2000-09-23

Helper classes that initialize and uninitialize COM via constructors and destructors can be dangerous with regards to cleaning up local variables of type CComPtr<> and similar smart pointer classes in the thread's "main" function.  In order to avoid uninitializing COM before these local variables are destroyed, put the function's body inside a nested block or put the code in a separate function.

LONG threadEntry(void *p)
{
    // Initialize COM
    ComInit foo;

    {
        // Do stuff here
    }
    return 0;
}


Common Service Errors

Revised on: 2000-12-08

Error 3: The system cannot find the path specified

If this happens when starting a service, it's probably because the system can't find the service's executable as specified in the registry.  Search the registry for the name of the executable and make sure it's right.  Make sure the path is right in the registry.

The service's executable should be on a local drive, not on a network share or a subst'd drive.


Common DCOM Errors

Revised on: 2000-11-27

RPC_S_SERVER_UNAVAILABLE

  • Make sure the DNS suffix on the client and server machines are the same
  • From the client, enter "ping foo.bar.com", in other words make sure you can look up the machine using the machine's full name with the DNS suffix
  • Make sure the proxy/stub DLLs (or the TLBs) are registered

RPC_E_DISCONNECTED_DNE / RPC_E_DISCONNECTED

  • COM clients must ping remote servers every 6 minutes
  • If a remote server doesn't hear from a client after 6 minutes it terminates the connection
  • The service that pings the remote server is called rpcss
  • There is only one rpcss process per machine (it's a service)
  • rpcss can get "stuck" by bad processes that call WaitForSingleObject in a single-threaded apartment (STA), or when other STAs for one reason or another do not process the Windows event queue
  • When rpcss gets stuck no "pings" can be sent out from the ENTIRE MACHINE
  • Therefore this error might not be caused by your code!  The problem might be caused by another process, in which case the resolution is pretty much hopeless.  Microsoft technical support can help you identify the "bad" process.
  • One solution is to move your object to a machine where the "bad" process isn't running
  • Use SOAP  :)

CO_E_SERVER_EXEC_FAILURE

  • The user that's invoking the object doesn't have sufficient rights on the machine.
  • The user can instantiate the object and access its methods but the object is attempting to access resources on the machine that the user doesn't have sufficient rights to.
  • This usually happens when invoking an object via a system service
  • Solution is to supply a network user account when calling CoCreateInstanceEx
  • Another solution is to run DCOM on the server, double click on the object, click Identification, and choose Identity.  Select "this User" and specify the user that has sufficient rights to the needed resources.

Multiple Dispatch Interfaces on a Single Object

Revised on: 2000-09-24

"Dual" interfaces inherit from IDispatch.  When a component needs to implement two "dual" interfaces, which IDispatch pointer should QueryInterface return?

Note that this is only a problem for automation languages such as VBScript.  Languages, for example C++, that can call QueryInterface do not suffer from the multiple IDispatch problem.

There are several solutions to this problem but Victor Pavlov's solution is best.  The component author chooses the default interface.  Each interface contains an additional method QueryDispatch that can only be invoked via IDispatch.  QueryDispatch accepts a string that distinguishes each IDispatch pointer.  All of this work is done by clever macros that are simple to incorporate into existing code.


List Implemented Interfaces in IDL

Revised on: 2000-11-09

The CoClass section of IDL files should list the custom interfaces that an object implements.  This allows OLEVIEW and similar programs to be able to display the interface that a particular object implements.

An exception to this is ISupportErrorInfo, which is usually omitted from the IDL.  But it doesn't hurt.

This is an asinine recommendation.  Only import IDL files when you absolutely have to.  Specifying IDispatch in IDL files works.  Sure, you have to do an extra QueryInterface and the interface isn't self-describing.  But here's what can happen when IDL files import other IDL files:

  • You're likely to run into issues about whether the interface is defined inside or outside the Library block
  • You will have compile-time name collisions and struct vs. class errors
    • v:\source\c\whois\whois\debugu\whois.tlh(171) : error C2374: 'IID_IFoo' : redefinition; multiple initialization
    • Use the exclude #import directive to fix this
  • IDL's import statement doesn't obey the project's include directories.  You have to enter the include directories via the project's MIDL tab
  • Dependencies between IDL files are just as bad a dependencies between header files.  It becomes a tangled mess that you will regret later.
  • Using IDispatch is very flexible.  Some day you might return an object that supports a different interface.  It's similar the flexibility of VARIANTS.  It allows you to incrementally add features without changing interfaces.

Release Interfaces in FinalRelease (ATL)

Revised on: 2000-09-23

Do not rely on smart pointer destructors to call Release on interface pointers that are stored in data members.  Do not call Release() in destructors either.  Instead, override the FinalRelease method and manually release the interfaces.

Failure to do this may result in crashes or memory leaks at shut-down.  This is because DllCanUnloadNow will return true before the interfaces get released.


Put Initialization Logic in FinalConstruct (ATL) or in an Interface Method

Revised on: 2000-09-23

Heavyweight logic should be put in FinalConstruct, not in a constructor.  Better yet, delay any "heavyweight" processing until a "real" method is called.  Methods called Init or Initialize are considered bad form -- the object should be able to know when to initialize itself, not the programmer.


Common Linker Errors

Revised on: 2000-09-23

Missing symbol *_Lib, CLSID_* etc...

Add a file to your project that doesn't include the precompiled headers that contains one line, where foo.tlb is the TLB (or DLL or IDL) where the COM interface, library, object, etc. resides:

#import "foo.tlb" named_guids


Turn an In-Process DLL into an Out-of-Process Server

Revised on: 2000-09-23

If you already have an in-process COM object, it's not necessary to create a separate COM project just because you want to be able to use it across process or machine boundaries.

The following instructions will enable an in-process COM object to be created out-of process as long as CoCreateInstance is called with the CLSCTX_LOCAL_SERVER (that's right, local server!) context.

The Windows surrogate process dllhost.exe hosts the object.  The downside to this approach is that, when you look in Task Manager, all you can see is dllhost.exe.  If some other object might be hosted by dllhost.exe, you have virtually no way of telling which process contains your object.  Some vendors distribute their own copy of dllhost.exe that simply has a different name and set the registry entry DllSurrogate to the full path of the renamed host executable.  Note that most flavors of Windows (98, XP, NT, 2000) have their own version of dllhost.exe, so it's recommended that your installer copy %WINDIR%\system32\dllhost.exe rather than shipping your own version.

  1. Open the Visual C++ project file

  2. Open the RGS file for your COM object

  3. Inside ForceRemove{GUID}{ add:

    val AppID = s '{APPGUID}'

  4. Add the following at the bottom of the file before the last closing paren:

    NoRemove AppID
    {
        ForceRemove {APPGUID}= s 'My Object'
        {
            val DllSurrogate = s ''
        }
    }

  5. Replace the {APPGUID} text above with the CLSID of the object, e.g., {1A9B826E-930F-4CF6-8279-B55C8F8E6108}.  Or, you can generate a new GUID:

    1. Run GUIDGEN.EXE

    2. Click 4-Registry format

    3. Click copy

    4. Go back to the RGS file

    5. Replace the GUID text with the contents of the clipboard via Edit/Paste

If you don't have access to the project's source code, you can alter the registry yourself.  Note:  HKCR is an alias for HKEY_CLASSES_ROOT 

  1. Create HKCR\AppID\{APPID GUID}...

  2. Set the default value to any text you like 

  3. Create a string value DllSurrogate which is empty

  4. Under HKCR\CLSID\{CLSID}...

  5. Create a string value AppID which contains the APPID GUID

As you can see, it's easy to instantiate an object in another process space.  What's not so easy is to implement the singleton design pattern.  Such as, processes on machines A, B, and C all have a pointer to the same instance running on machine D.  Generally this is not a scalable model but let's say you want to that.  The best way to do so is to create an executable that runs as a service.  If you already have an in-process DLL that does all the work, the executable could merely forward method calls on to this object.

Also see here.  There is some information on surrogate sharing here.


Single-Instance Out-of-Process Servers

Revised on: 2001-03-09

By default, out-of-process servers host many objects in a single process.  Consider an example.  Two client processes instantiate two objects from the COM EXE server called MyServ.EXE.  Although there are objects instantiated, there will only be one MyServ.EXE in the process list.

Sometimes it is necessary to have one process per object.  You would do this to isolate processes for security or robustness reasons.  For example, if one instance crashes a process, it won't affect the other instances because their processes will be intact.  Another situation where this is helpful is when the code uses a non thread-safe library and you want multiple instances to be able to use this library simultaneously in separate logical threads.

To do this, you must create a COM EXE server -- you can not use the built-in surrogate (dllhost.exe).  When the class factory is registered using CoRegisterClassObject, use REGCLS_SINGLEUSE for the 4th parameter (flags).

This technique uses many more resources per object so you should only use it when you have a good reason to do so.


Free Non-Retval Output Parameters

Revised on: 2000-12-15

Output parameters that are not marked "retval", particularly VARIANTs, need to be free'd; otherwise memory leaks will result.  VBScript doesn't free memory before calling methods.

For example:

STDMETHODIMP CURLUtilObject::GetNVPairs(BSTR in, 
  VARIANT *names, VARIANT *values)
{
	if (!names) return E_POINTER;
	if (!values) return E_POINTER;

	VariantClear(names);
	VariantClear(values);
}

If you write VBScript that calls GetNVPairs twice in a row with the same variable for "values" or "names", a memory leak will result unless GetNVPairs internally calls VariantClear.


Initialize Output Parameters to NULL

Revised on: 2000-12-15

It's good practice to initialize output parameters (BSTR*, LPVARIANT, LPDISPATCH etc.) immediately when called.  COM does not look at the return code when marshaling output parameters.  Even if a failure scode is returned, all output parameters are passed back to the caller.  If the parameters contain bogus or uninitialized data, COM will crash.

See also "Free Non-Retval Output Parameters" -- sometimes you also need to free the memory, not just set the pointer to NULL.

Do not set output parameters to a value that needs to be deallocated (e.g., a string) until you are certain that the method can not return a failure SCODE.

If you were to, say, set an [out, retval] BSTR * and return E_FAIL, the caller might not deallocate the string.  This is really, really, bad form on the part of the caller, but it can happen.


Naming Conventions

Revised on: 2000-09-23

  1. I always create projects for IDL files.  The project might implement interfaces or it might just be a "stub" project.  Creating project files for IDLs enables you to use the project wizards to add methods and properties.
  2. Do not name a CoClass the same as a library name -- the names need to be different by more than just the case of their letters (e.g., Foo and FOO).  Otherwise you will get weird compiler errors.
  3. Type library names should be unique across all companies -- this is needed for Visual Basic support.
    • A common convention is, when creating a project via the ATL project wizard, to use the naming convention XXLibName where XX is a company-wide two-letter prefix.  Naming the DLL in a special way is not necessary -- you can modify the IDL file directly and provide your own name.
    • It's a good idea for the library name and the project name to be the same, or have Lib attached to it
    • E.g., the project that has the IDL file is DGFoo and builds DGFoo.DLL.  The library name is DGFOOLib.  The ATL Wizard will do this automatically.
  4. Names of enumerated values:  (1) uppercase (2) all names within one IDL file (and included IDL files, if any) must be unique.
  5. Use the following format as the object's description -- or enter in the "Type" field in the ATL wizard:
    <company name> <Class Name>
  6. ProgIDs should have the form (set this in the RGS file):
    <company name>.<object name>
  7. The help string for type libraries should have the form (set this in the IDL file):
    <company name> <library name> Type Library
  8. Beware of name collisions when an IDL file needs to include another IDL file
    • Such as, when implementing an interface that's defined in another IDL file
    • In IDL files, the names of interfaces, coclasses, enumerations, and on and on may clash with other names
    • Bottom line:  in order to be safe, all identifiers should have a prefix

Here is an example RGS file:

HKCR
{
  devguy.SystemInfo.1 = s 'DevGuy SystemInfo Class'
  {
    CLSID = s '{F16F6FFC-129C-4736-9963-8E34F22C195B}'
  }
  devguy.SystemInfo = s 'DevGuy SystemInfo Class'
  {
    CLSID = s '{F16F6FFC-129C-4736-9963-8E34F22C195B}'
  }
  NoRemove CLSID
  {
    ForceRemove {F16F6FFC-129C-4736-9963-8E34F22C195B} = s   'DevGuy SystemInfo Class'
    {
      ProgID = s 'devguy.SystemInfo.1'
      VersionIndependentProgID = s 'devguy.SystemInfo'
      InprocServer32 = s '%MODULE%'
      {
        val ThreadingModel = s 'both'
      }
    }
  }
}

Here is a sample IDL file:

import "oaidl.idl";
import "ocidl.idl";
	[
		object,
		uuid(65D62D03-3F65-487E-BAA9-1F8F6EFD5A6C),
		dual,
		helpstring("IDGStringUtil Interface"),
		pointer_default(unique)
	]
	interface IDGStringUtil : IDispatch
	{
	};

[
	uuid(57B9B864-F875-472A-9887-9BB396234982),
	version(1.0),
	helpstring("StringUtil 1.0 Type Library")
]
library DGStringUtilLib
{
	importlib("stdole32.tlb");
	importlib("stdole2.tlb");

	[
		uuid(22B4BAC9-E321-4674-8E8F-372B6256FBC3),
		helpstring("DevGuy StringUtil Class")
	]
	coclass DGStringUtil
	{
		[default] interface IDGStringUtil;
	};
};

CComBSTR and CComVariant Memory Leaks

Revised on: 2000-09-23

The following code demonstrates a memory leak:

CComBSTR str;
pFoo->get_Bar(&str);
pFoo->get_Baf(&str);

CComBSTR::operator & does not free the CComBSTR's string.  To remedy this, call str.Empty() before calling get_Baf().

The same thing can happen with strings stored in CComVariant.  You must call the Clear() method first.

Note that when creating a CComVariant with a BSTR, you must make sure the BSTR doesn't get deallocated twice.  Use  CComVariant(CComBSTR("hello").Detach()))


ATL

Revised on: 2000-09-23

  • Also see the information regarding implementing interfaces
  • Implement ISupportsErrorInfo
  • All interfaces should be dual
  • Use CComPtr or similar smart pointers to avoid memory leaks especially in situations when MFC throws exceptions.
  • Interfaces and TLBs
    • Multiple interfaces can go into one IDL/TLB file, but minimize this practice - logically distinct interfaces should not appear in the same TLB
    • In order to use the ATL wizard, one project may "own" the IDL so you can add methods/properties easily - this is called the "master project"
    • Projects that implement existing interfaces should import the TLB instead of "#including" the MIDL-generated header files from the master project
    • See MSDN section for a link regarding implementing existing interfaces with ATL
  • ATL Tips:

LIBCMT.lib (crt0.obj) : error LNK2001: unresolved external symbol _main

If you get this error when building the release build, simply click on the C/C++ tab in Project->Settings and select Preprocessor from the Category drop-down box.  Remove the _ALT_MIN_CRT flag in the Preprocessor definitions box.  By default, ATL release builds use a tiny version of the C runtime library (CRT).  However, this tiny CRT does not include certain functions which your ATL project may use.  Therefore, you will get unresolved symbols.  By removing this flag, you will tell the linker to link in full-size version of the CRT.   However, this only increases your DLL by ~30KB.

_ATL_STATIC_REGISTRY and _ATL_DLL

If one uses the VC++ wizard to build an ATL project, then try to build a "MinSIZE" build, you may get the error:

C:\Program Files\Microsoft Visual Studio\VC98\ATL\INCLUDE\atlbase.h(4927) : error C2259: 'CRegObject' : cannot instantiate abstract class due to the following members:
C:\Program Files\Microsoft Visual Studio\VC98\ATL\INCLUDE\statreg.h(233) : see declaration of 'CRegObject' C:\Program Files\Microsoft Visual Studio\VC98\ATL\INCLUDE\atlbase.h(4927) :
warning C4259: 'long __stdcall IUnknown::QueryInterface(const struct _GUID&,void ** )' : pure virtual function was not defined
C:\Program Files\Microsoft Visual Studio\VC98\INCLUDE\unknwn.h(109) : see declaration of 'QueryInterface'

This turns out to be because of the way VC++ links with ATL. There are two ways; one, by compiling in statically all the ATL base functions (QueryInterface, etc.) or by linking dynamically with ATL.DLL The former is invoked by defining _ATL_STATIC_REGISTRY, which is placed by the wizard into StdAfx.h. However, when the MinSize version of the Unicode or non-Unicode release is setup, the wizard also places _ATL_DLL in the preprocessor declarations. _ATL_STATIC_REGISTRY and ATL_DLL are not compatible, hence the error.

So, one can remove (or conditionally ifdef) the _ATL_STATIC_REGISTRY if one wants to save 5 kB (size of the statically linked ATL functions) or remove _ATL_DLL and accept the extra 5 KB. For further info, see MSDN: Article ID: Q166717 .

In the same build (after fixing the former) I also got an odd message: warning LNK4086: entrypoint "_wWinMainCRTStartup" is not __stdcall with 12 bytes of arguments; image may not run.  This turned out to be the fact that "_wWinMainCRTStartup" waas defined. In fact, it doesn't cause a problem, but one gets the warning because wWinMainCRTStartup should only be defined as the entry point for an EXE, not a DLL. This warning only occurs with Release builds, not with Debug builds - whether Unicode or no.... For further info see Article ID: Q166480

IUNKNOWN

If you want to call QI inside an ATL implementation, in the class declaration declare
DECLARE_GET_CONTROLLING_UNKNOWN()
And use
GetControllingUnknown() to get the IUnknown pointer of the object.

E_CLASSNOTREG or E_INTERFACENOTIMPL

Problem:

When I try to CoCreate my object I get E_CLASSNOTREG.  Or, I get E_INTERFACENOTIMPL when I call QueryInterface.

Solution:

Look at the header file for your object, specifically at the BEGIN_COM_MAP section.  Make sure there is a COM_INTERFACE_ENTRY item for the interface in question.  For example, the following is a COM map for an object that implements several interfaces.

BEGIN_COM_MAP(CDispenser)
COM_INTERFACE_ENTRY(IDGDispenser)
COM_INTERFACE_ENTRY(ISupportErrorInfo)
COM_INTERFACE_ENTRY(IConnectionPointContainer)
COM_INTERFACE_ENTRY_AGGREGATE(IID_IMarshal, m_pUnkMarshaler.p)
COM_INTERFACE_ENTRY2(IDispatch, IDGDispenser)
COM_INTERFACE_ENTRY(IXMLAcceptor)
END_COM_MAP()


Use #import

Revised on: 2000-09-23

Use 

#import <TLB filename> named_guids raw_interfaces_only raw_native_types no_implementation

it's convenient, avoids build dependencies, and MIDL-generated files don't have to be checked in if you use it (unless you need to...)

Do not use no_namespace unless you really mean it.  Namespaces are absolutely required when a project implements an interface that's also #import'ed.

Using plain #import causes COM object wrappers to throw exceptions instead of returning an HRESULT indicating the error.  Use raw_interfaces_only to turn it off.  See:   MSDN\vccore.chm::/html/_predir_the_.23.import_directive.htm

When importing non-dual COM objects, don't use raw_interfaces_only.  The importer will create a nice wrapper class around IDispatch (see the generated .tlh file).  Note that you should catch _com_error.

If you get the linker error LNK1179, try removing named_guids.


Import TLB, not IDL

Revised on: 2000-09-23

  • TLB is a binary interface whereas IDL is a text interface that must be compiled.   TLB is the only unambiguous form of an interface.
  • TLB files should be used as the interface between components.  Do not include IDL files to use a COM object.  Use TLB instead.
  • Do not check in TLB files unless they're from third party products -- TLBs are generated by VC++ projects.

Don't #include TLH Files

Revised on: 2000-09-23

The TLB or IDL file for third party products should be checked into source code control.  This makes it easier to maintain machines since it is not always necessary to install an SDK because the TLB files are sufficient.

In some situations the TLB file isn't available or the IDL can't be compiled without having an SDK installed on a machine. This means you must use #import "dllname". After you do that, you might be tempted to use the generated TLH file directly. However, the TLH file is an artifact that is much more obscure than TLB files so they shouldn't be #include'd explicitly.

Instead of checking in TLH files, do one of the following:

  1. Generate the TLB and check it into a 3rd party directory - then #import the TLB

  2. Check in the DLL in a 3rd party directory - then #import the DLL

  3. Make sure the DLL is installed somewhere in the PATH on all machines that need to build the project.  Don't assume all machines install the DLL in the same directory; don't specify the full PATH of the DLL

#1 or #2 is preferred for more "obscure" 3rd party products.


Type Libraries

Revised on: 2000-09-23

A library's help string should follow the form:

<Company Name> <Control Name> Type Library

Such as "ACME Service Type Library".

In general, define interfaces inside the library definition.  Do not do this if you intend to implement the same interface multiple times.  You must define dispinterfaces inside library declarations (at least, that's what I've found from my experience).


Automation Considerations and Datatypes

Revised on: 2002-03-12

This is the most important section of this document.  Failure to understand how scripting languages utilize the myriad COM datatypes will yield unusable interfaces.  Pay special consideration to the "When should VARIANT be used?" section below.

In general, all interfaces should inherit from IDispatch and be dual and should be marked oleautomation

[
object,
uuid(DAC49A7E-239A-42be-BECC-FB22E253FEDA),
oleautomation,
dual,
helpstring(" Interface"),
pointer_default(unique),
oleautomation
]
interface IFoo : IDispatch
{
}

Input Parameters

  • Simple input datatypes: VARIANT, BSTR, long, short, float, double, DATE, VARIANT_BOOL, LPDISPATCH
  • Use VARIANT_BOOL, not BOOL.  The C++ constants for boolean values are VARIANT_TRUE and VARIANT_FALSE.
  • Use long instead of ULONG or DWORD - unsigned 32-bit integers are not compatible with VB 6.0 or earlier.  int is the same as long.
  • [in] VARIANT values should be passed in as VARIANT, not as VARIANT *
  • Even if a method accepts a VARIANT as opposed to a VARIANT *, pointers can be passed indirectly via VT_BYREF|VT_VARIANT.  VBScript passes [in] VARIANTs in this fashion.
  • Optional parameters:  Must be VARIANT or VARIANT * - [in, optional] VARIANT v

Output Parameters

  • Scripting languages generally don't allow multiple output parameters.  A method may only have one output parameter and must be marked "retval."  Using multiple output parameters requires the use of VARIANT arrays (Safearray).
  • Pointer types for [out]: VARIANT*, LPDISPATCH* (VBScript does not support any other types)
  • Use [out] VARIANT*, not [out] VARIANT
  • Methods that accept output VARIANT* values MUST call VariantClear on VARIANT * parameters before changing the value of the VARIANT
  • Pointer types for [out,retval]: VARIANT*, BSTR*, long *, short *, float *, double *, VARIANT *, LPDISPATCH
  • VBScript only supports arrays (SAFEARRAY) that contain VARIANTs (input and output).  VBScript passes [in] VARIANTSs as VT_BYREF|VT_VARIANT and the pvarVal pointer points to a VARIANT of type VT_BYREF|VT_VARIANT|VT_ARRAY which means the SAFEARRAY is in the pparray member.  The members of the array are VT_VARIANT, not BYREF.
  • Array indexes should start with 0

When should VARIANT be used?

  • For any output parameter that is not a retval ([out,retval]) and is not a LPDISPATCH, use VARIANT * -- VBScript and ActivePerl can't handle it otherwise
  • If your method accepts a NULL interface pointer, pass a VARIANT.  JScript can not handle it otherwise.
  • Use VARIANT if you want to accept or return an array (SAFEARRAY) of values
  • Use VARIANT if you want to "overload" a method name so that it can accept a variety of arguments and datatypes
  • Use VARIANT if you want the parameter to have a default value.  Make the parameter optional -- [in, optional] VARIANT v -- When optional parameters are omitted, the variant's type is VT_ERROR.

Creating VB SAFEARRAYs in JScript

You can not pass JScript arrays to methods that take VB SAFEARRAYs (SAFEARRAYs are passed as VARIANTs).

  1. Create a Scripting.Dictionary object

  2. Populate the dictionary by calling Add(key,value) repeatedly where the key value is an incrementing integer

  3. Call the Items() method on the dictionary object which returns a Variant containing a VBArray

Reading VB SAFEARRAYs in JScript

Use the VBArray object

Optional Parameters

An optional parameter is defined as:

[in, optional] VARIANT v

If an optional argument value is not given, the variant's type will be VT_ERROR and its scode member is set to DISP_E_PARAMNOTFOUND

Returning Interfaces

  • All interfaces returned as output parameters must be derived from IDISPATCH
  • Interfaces to objects should be returned via one of the following:

    [out,retval] LPDISPATCH *
    [out]        LPDISPATCH *

    [out]        VARIANT *
    [out,retval] ICustomInterfaceName **
    [out]        ICustomInterfaceName **

Returning Enumerators

There is a standard "enumerator" concept in COM that is used by VB's foreach statement.  This is used when you want to return multiple objects instead of just one.  This is preferable to using VARIANT arrays (SAFEARRAY) in many situations (however, VB's foreach statement supports arrays too).  This requires that the object have some properties available via IDispatch.   See the information on ATL enumerators.

Returning Strings from COM Methods

Revised on: 2000-09-23

Use the Detach or CopyTo methods on CComBSTR

CComBSTR bstr("Hi");
// pbstrOut is a BSTR *
*pbstrOut = bstr.Detach();

or

return m_bstrValue.CopyTo(pbstrOut);


T(""), L"" Is Not a BSTR

Revised on: 2000-09-23

This is probably the most common mistake made by C++ programmers.

When a COM method takes a BSTR, you should send it a BSTR, not a hard-coded string created by the L"" operator or the _T("") macro.

As with all things COM, this rule is deceptive.  Passing a constant like L"foo" to such a method works only when the caller and the COM object live in the same apartment. If you were to call the COM object from a different apartment from which it was created (i.e., your work is split across threads), COM would try to marshal the string across apartments.  Because the string is not a BSTR - its length is not in the 4 bytes before the address of the passed-in string - the method would receive a random substring or superstring of the value, or an exception would be thrown in the COM runtime.

Here is how to create a hard-coded BSTR:

CComBStr(L"foo")

The _bstr_t class can be used as well.


Using CComVariant Properly

Revised on: 2001-04-11

It's very easy to misuse CComVariant, which is a pity since CComVariant is supposed to make your life easier.  The important thing to remember is that CComVariant, like CComPtr, exists to free resources for you when the object is destroyed.  That can be a bad thing if you're not careful.  Like all other "smart pointers", CComVariant can end up freeing the same resource (BSTR, array, interface pointer, etc.) twice, which will result in memory corruption, leading to unpredictable behavior.

Here are the common mistakes made with CComVariant

  1. Assigning a CComBSTR to a CComVariant without using Detach() on the CComBSTR:
    CComVariant v;
    v.vt = VT_BSTR;
    v.bstrVal = CComBSTR(L"Hello"); // error -- call Detach() on the string
  2. Assigning a CComPtr to a CComVariant without using Detach() on the CComPtr:
    CComPtr<IUnknown> pFoo;
    CComVariant v;
    v.vt = VT_DISPATCH;
    v.pdispVal = pFoo; // error -- call Detach() on the pointer


DCOM

Revised on: 2000-09-23

  • Creating COM instances on remote machines
    • use CoCreateInstanceEx which is defined in <objbase.h> may need to define _WIN32_DCOM in order for the method to be properly prototyped
    • Take advantage of the MULTI_QI structure which is one of the parameters for CoCreateInstanceEx; the structure allows a client to specify all of the interfaces wants on the instantiated object which will save any round trips performed in order to do a QueryInterface
    • Always consider performance especially when you feel the urge to marshal interface pointers (LPDISPATCH, LPUNKNOWN) because every time a property/method is accessed by the remote machine on the marshalled interface pointer, a round trip is necessary.  Consider using "by value" semantics such as a VARIANT containing a SAFEARRAY if you need to pass multiple values
  • Security settings
    • Use DCOMCNFG.EXE to enable DCOM on your computer as well as to modify security settings
    • Use CoInitializeSecurity in order for the client application to properly allow the server process identity to call into the client process (ex. QueryInterface);  if CoInitializeSecurity is not called, then the default DCOM security settings will be enforced which often does not allow most server process identities to call back into the client process (see Q158508 for a thorough explanation)

      CSecurityDescriptor sd;

      sd.InitializeFromThreadToken();

      CoInitializeSecurity( sd, 
      -1, 
      NULL, 
      NULL,
      RPC_C_AUTHN_LEVEL_PKT, 
      RPC_C_IMP_LEVEL_IMPERSONATE, 
      NULL, 
      EOAC_NONE, 
      NULL);


Specify the Resource DLL (MFC)

Revised on: 2000-09-23

In order to read resources correctly, generally every COM method should start with

AFX_MANAGE_STATE(AfxGetStaticModuleState())

If the project is MFC-based.  The ATL Wizards insert this code for you automatically.


Initialize COM in New Threads

Revised on: 2000-09-23

All threads should initialize COM by first calling CoInitialize or CoInitializeEx(COINIT_MULTITHREADED) and CoUninitialize() on exit.

This allows us to write utility code that uses COM but doesn't have to fret about CoInit/CoUninit.  Initializing COM when the caller is unaware of that fact can cause really bad performance if COM isn't initialized - imagine calling this utility code in a loop - so that's why we shouldn't do it.  It is a convenient feature for utility code to start up and shut down the COM runtime for you, but it's a bad feature - unexpected but correct results.


Object Instantiation - Don't use CLSIDs

Revised on: 2000-09-23

Instantiate COM objects using the version-independent ProgID, not CLSIDs.  See the function CLSIDFromProgID.  This rule of thumb should not be followed for objects that are created frequently.


Robustness

Revised on: 2000-09-23

  • Check input parameters in Debug and Release builds (in other words, all builds!) and return E_INVALIDARG - don't use ASSERTs

Threading

Revised on: 2000-09-23

  • Know the COM threading models and pick the right one for the job
  • This is particularly an issue with client code - client code should choose the right threading model when initializing COM - performance is highly dependent upon how CoInitialize or CoInitializeEx are called.

Implementing Existing Interfaces With ATL

Revised on: 2000-09-27

Go to the ClassView tab for the project.  Right click on the appropriate object.  Select "implement interface."  After doing this, change the #import statements -- you will usually need to change the paths to the TLB files.

Or, if you want to do it manually...

The ATL new object wizard creates a COM object with its own interface.  If you want to implement an existing interface (instead of creating a new interface), or if you want to add a new interface to an existing object, follow these guidelines.

  1. Add the interface to the IDL file

    import "oaidl.idl";
    import "ocidl.idl";
    import "../../XMLAcceptor/n_xmlacc/n_xmlacc.idl";
    <<...skipping many lines...>>
    coclass IPResolver
    {
    [default] interface IXMLAcceptor;
    interface IIPResolver;
    };

  2. Create a new file that imports the TLB of the implemented interface

    #include "stdafx.h"

    #import "source\tlb\n_xmlacc.tlb" no_namespace named_guids no_implementation raw_interfaces_only

  3. In the .H file, delete the line COM_INTERFACE_ENTRY(IDispatch), add a COM_INTERFACE_ENTRY2 entry for the default interface, and add a COM_INTERFACE_ENTRY for the interface that is not the default interface:

    BEGIN_COM_MAP(CIPResolver)
    COM_INTERFACE_ENTRY2(IDispatch, IXMLAcceptor)
    COM_INTERFACE_ENTRY(IIPResolver)
    END_COM_MAP()

  4. Add the interface's methods to the .H and .CPP files

Problem:

IFoo first seen as struct; now seen as class; struct type redefinition

Solution:

This happens when a project #imports a type library and  #includes the same library's MIDL-generated files.  The #import directive probably uses no_namespace -- otherwise, you should not be seeing this error message.

  • Change stdafx.h to #import the TLB file using no_namespace named_guids raw_interfaces_only raw_native_types
  • Remove all statements that #include the generated header and .C files

Problem:

I'm getting undefined symbols at link time, such as IID_Foo not found

Solution:

Create a file, such as "import.cpp" that #import's the type library that contain the implemented interfaces using no_namespace named_guids raw_interfaces_only raw_native_types

Problem:

I did that and now I'm getting errors like "was seen as 'struct'; now seen as 'class'"

Solution:

This happens when IDL files import other IDL files.  The solution is to have only one #import statement per file.  Create additional .cpp files, one for each type library imported.

Problem:

I'm still getting "was seen as 'struct'; now seen as 'class'" or struct type redefinition

Solution:

You're probably implementing an interface that a .h or .cpp file loads via #import and the #import uses the keyword no_namespace.  I don't know the answer to that one except to use a namespace and use namespace prefixes or "using namespaces."  But then the compiler might complain that LIBID_Foo isn't defined, and I don't have a solution for that.

Also, the IDL file needs to "import" the IDL file that contains the problematic interface, otherwise the compiler will complain that LIBID_Foo isn't defined.

Problem:

The compiler complains about an "import" statement in an IDL file -- the path looks correct.

Solution:

This happens when an IDL file imports an IDL file that in turn imports another IDL file (there are three IDL files in all).  All import statements in the IDL files must either use absolute paths (bad idea), have no path component, or must have the same number of ..\ 's

Lesson:  put all IDL files in the same directory


Global Interface Pointers to In-Process COM Objects:

Implementing shared state with the dispenser paradigm

Revised on: 2002-04-09

Consider the scenario in which a global variable holds an interface pointer to a COM object.  Multiple threads have access to this interface pointer.

Storing interface pointers to in-process COM objects in C++ global variables is problematic because they could be accessed by different types of apartments.  You must use the Global Instance Table (GIT) or CoMarshal to pass interfaces to single-threaded apartment objects across apartment boundaries.  "Raw" pointers to objects that use the "free" (or the mythological "rental") thread model can be accessed by STAs without any problems, as long as the objects are instantiated in-process.

The problem is getting a raw pointer to an interface when an object is created via a single-threaded apartment (STA).  The global interface pointer should only be a "raw" pointer to the object, not a pointer to a proxy.  Otherwise other STAs and MTAs won't be able to use the pointer.

Marking an object as "Both" is one way to do this.  So why not use "Both" and be done with it?  An object marked "Both" is multi-threaded and interface pointers to these objects can be used in different threads -- even in different apartments.  Although this seems like the ideal solution, "Both" doesn't provide the lifetime characteristics needed for global objects.

Consider the following scenario:

  1. A thread living in an STA creates an object
  2. The pointer to the object's interface is stored in a global variable
  3. The thread that created the STA calls CoUninitialize
  4. Some code attempts to use the interface pointer stored in the global variable

This results in the error "the server has terminated" or, if you call QueryInterface on the pointer, "E_NOINTERFACE -- no such interface implemented."  The apartment that created to object was terminated.  Creating objects via an STA and then terminating the apartment doesn't work if you want objects to live beyond the life of the STA.  "Both" doesn't solve this problem.  "Both" causes the object to be instantiated in the STA.  When the STA is terminated, the singleton dies with it.

There is another problem with this approach:  every DLL has its own global interface pointer.  It is often desirable for all objects in an entire process to share the same global state, regardless of whether the objects live in the same DLL.

One way to implement singletons is via ATL's singleton class factory.  However, it isn't wise to implement objects as ATL singletons.  The next topic explains why.  Also refer to COM Singletons - A Dangerous Animal, By Richard Blewett.

A better solution is to first implement all objects as regular objects.  It's a good idea to be able to create two instances of an object if you need to some day.  Then, implement another object to "dispense" interface pointers to these objects, as if they were singletons.  For example, the "dispenser" interface would have a property whose datatype is IDispatch which provides access to a "global" object.

This approach is a good start.  However, it still doesn't deal with the threading issues discussed above.  The result can be strange intermittent problems that apparently occur more often when objects are instantiated by IIS (Microsoft Internet Information Server).

Here is a complete solution for managing global in-process objects:

  1. Declare an interface that "dispenses" global data, such as interface pointers or strings
  2. Implement this interface twice
  3. One implementation implements the interface "for real."  This implementation can use the Both thread model, and must use critical sections to make sure the object's data doesn't become corrupted.
  4. The other implementation delegates to a static (global) interface pointer to the "real" dispenser implementation
    • This implementation uses a global critical section object to ensure that only one instance is created (see example below)
    • This object uses the Free thread model.  Using the Free thread model protects the dispenser from code that calls CoUninitialize.
    • This object ensures the multi-threaded apartment stays alive forever.  It starts a thread that calls CoCreateInstanceEx(COINIT_MULTITHREADED) and waits indefinitely.

When the application needs to access global data, it creates a delegate dispenser object.  The application has no idea that it's not talking to the real dispenser.  When the application is finished with the delegate, it calls Release on the interface pointer.  The real dispenser object creates objects and manages the interface pointers to them.

This solution avoids all marshaling issues without resorting to the free-threaded marshaller, ATL's singleton class factory implementation, CoMarshalInterfaceInStream, or the GIT.  The dispenser can even manage pointers to single-threaded objects without running into any marshalling issues.  Unfortunately, there is a performance penalty to be paid for creating the delegate and destroying it repeatedly.  This is the price applications must pay to share global state.

The delegate never decreases the reference count of the "real" object.  Like ATL singletons, the object will never properly clean itself up when the process dies, as described in the next topic.  This also causes the module (DLL) in which these objects reside to never be unloaded until the process dies, and this can result in remote COM servers never terminating by themselves, even when there are no objects instantiated in the server.  All of these problems can be avoided by adding a Stop method to the interface that calls Release on the global interface pointer.  Knowing exactly when to call Stop is a tricky proposition.

On a related note, it is a good idea to provide a method on the dispenser interface to cause the "real" dispenser object to release its interface pointers and create new objects.  In the real world, long-lived objects may become unusable over time due to bugs or network problems.  Sometimes creating a new object is the only remedy.

The following code is an example of how a delegate object can store a global interface pointer to the "real" dispenser object.  Note that AddRef isn't called, and this solution only works because there is no Stop method which would cause the "real" dispenser object to be released and destroyed while a method is running.  In this example, the progid of the "real" dispenser object is devguy.GlobalsSingleton.

static CComAutoCriticalSection static_cs;
static IGlobals * static_pGlobals = NULL;

IGlobals *GetGlobals()
{
    HRESULT hr;
    for (int i = 0; i < 10; i++)
    {
        if (!static_pGlobals)
        {
            static_cs.Lock();
            try
            {
                if (!static_pGlobals)
	       {
                    hr = CoCreateInstance(L"devguy.GlobalsSingleton", 
                      __uuidof(IGlobals), (void**) &static_pGlobals, CLSCTX_INPROC), 0);
                 if (FAILED(hr)) throw hr;
                 }
            }
            catch(...){
               ATL_TRACE(_T("Unable to create devguy.GlobalsSingleton"))
            }
            static_cs.Unlock();
       }
       else
           break;
    }

    return static_pGlobals;
}

The Problem with Singletons

Revised on: 2002-04-09

There are several problems with singletons ala Design Patterns.  Singletons are inflexible because, by definition, you can't instantiate more than one of them.  You might be convinced that a particular object is a singleton and six months later change your mind.  At that point reimplementing the object may not be an option.  Therefore, COM objects should almost never be implemented as singletons.  Instead, a secondary "dispenser" object should dole out object instances.  Dispensers can be implemented as ATL singletons (DECLARE_CLASS_FACTORY_SINGLETON).

However, ATL singletons can lead to bizarre behavior that is similar to objects that implement the free-threaded marshaler.  Please refer to COM Singletons - A Dangerous Animal, By Richard Blewett.

Singletons have other problems.  Singletons' reference counts are always greater than zero, which means they never get removed from memory.  It's pointless to call Release on a singleton.  There is no way to free a singleton's resources via Release.  Singletons are only cleaned up when the process is terminated.  This can be a problem if the singletons occupy large amounts of memory.  The work-around is to provide a method that cleans up resources, via reference counting or otherwise.  But there never seems to be a good time to release a singleton's resources.

By far, the worst behavior of singletons is that their destructors aren't called until all threads in a process have terminated.  Any threads that the singleton might have created will not be around by the time the singleton's destructor executes.  This can lead to file corruption if the singleton writes to files in background threads because the singleton is not able to wait for the threads to finish pending operations.  Other side-effects can result in even worse consequences.

The destruction behavior of singletons can lead to crashes.  Singletons get removed from memory essentially at random.  Consider the following scenario.  The singleton and the user of the singleton, another COM object, live in different DLLs.  When the process terminates, the singleton DLL gets unloaded first, leaving a dangling interface pointer in the client object.  When the client object gets destroyed, it calls Release on the singleton's interface.  This results in a crash because the singleton's data and code have been removed from the process's address space.

Lesson:

Never call Release on a singleton's interface in a destructor

Corollary:

Class data members should not use smart pointers to hold interface pointers to singletons.  Just use raw pointers to singletons and don't worry about releasing them.

A singleton "dispenser" object provides a false sense of security.  Just because a singleton dispenser holds onto an interface pointer to a dispensed object doesn't mean that the object can't go away.  If the singleton dispenses Apartment-threaded objects, it's possible that the calling code can terminate the apartment via CoUninitialize (or by the thread terminating), causing the object to be "disconnected."  The best way around this problem is to implement the singleton as "Free" and have the singleton manage one thread that keeps the multi-threaded apartment alive.  These two steps insulate your singleton from application code -- it can't unload COM out from under it.  This problem is also described in the above section.

It is not possible to rely on the dispenser's destructor to properly clean up the dispensed objects.  Instead, client logic must control the lifetime of the dispensed singletons.  One way to do this is override Lock and Unlock in CComModule.  When the lock count goes to zero, it's time to tell the dispenser to release all cached interface pointers.  Instead of using AddRef and Release on the dispenser object to accomplish this objective, add new methods like AddRefSingleton and ReleaseSingleton to the dispenser's interface.  Lock calls AddRefSingleton when the reference count is one and Unlock calls ReleaseSingleton when the lock count is zero.  However, this can result in restarting the dispenser too frequently, which negatively affects performance.  There just doesn't seem to be a good time to release global objects unless the executable itself does it.  Which, of course, is not possible if code is running inside IIS, MTS, Microsoft Management Console, or a surrogate process.

Consider using COM exes or NT services the next time you want a singleton.  Although you pay in terms of network overhead and additional installation tasks, there will be fewer development headaches and much less chance of resource misuse that can lead to file corruption and worse problems.

If you want to dedicate one singleton per process, you can accomplish this with a "dispenser" COM singleton object that instantiates objects.  The dispenser is described above and also in the above section.  If you want multiple processes to share the same singleton, use an NT service.  If you want processes on different machines to share the same singleton, use services.  If you want each COM DLL to have its own singleton instance, ATL singletons won't work for you -- you must use C++ global variables, which are very problematic (see above section).


COM's Error Infrastructure

Revised on: 2000-09-23

After building up several years of experience with COM's lousy HRESULT/IErrorInfo scheme, I have come up some rules of thumb:

  • Always support IErrorInfo

  • All methods immediately call GetLastError(NULL, &pIErrInfo) to elminiate any "residue" error objects

  • Assume any COM method you call will clear out the error object, so hold on to error objects if you don't want to lose them

  • Don't bother looking at the Interface pointer, CLSID, PROGID, etc. members of the IErrorInfo object -- treat every IErrorInfo as a valid error object

  • Return E_FAIL unless you're certain the SCODE is "local" for your interface.  Interfaces should not return SCODEs from other Interfaces unless you really know what you're doing

Like I said, these are my rules of thumb.  Here's some more information:

COM's error handling is flaky at best.  There is a very real chance of "ErrorInfo residue" which occurs when:

  1. A method is called on an object that supports IErrorInfo

  2. Method returns and error and sets the ErrorInfo object

  3. Caller fails to call GetErrorInfo

  4. A method is called on an object that doesn't support IErrorInfo

  5. Method returns a failure SCODE

  6. Caller investigates the ErrorInfo object

  7. Result: The ErrorInfo object pertains to the first method call, not the second one, but the caller *thinks* it pertains to the second call

There are some ways around ErrorInfo residue, such as calling GetErrorInfo before calling any methods.  This is undesirable as it has a performance penalty; however, it's the most foolproof method.  All other schemes rely on the caller doing the right thing by calling GetErrorInfo to clear the ErrorInfo object after making a method call.  It's not uncommon to find methods that clear ErrorInfo before performing any work.  This is a good solution but it's not foolproof.  Consider a DCOM scenario in which the client and server have been disconnected.

Error Codes (SCODES)

  • The error code should be greater than or equal to 0x500 and less than 0xFFFF.   Error codes are specific to an interface.
  • Different COM interfaces can use the same error code value - there is no master error code list
  • HRESULTs should be defined only for errors that can be handled.  Otherwise, the methods will just return E_FAIL.
  • Error codes are documented with each interface's main documentation
  • Generate error codes with MAKE_HRESULT(1, FACILITY_ITF, 0x500).  This results in 0x80040500
  • When calling other COM methods, if a failure occurs, E_FAIL should be returned instead of returning the HRESULT from the last call
    • This prevents ambiguity that arises from overlapping HRESULTs codes

Error Info

  • All objects should implement ISupportErrorInfo and return S_TRUE
  • If an error can be handled, clear the error message via GetErrorInfo()
    • If you don't do this, sometimes the error object gets mistaken as the error object for the current error when instead it was set for some other error that occurred earlier
  • An error object should be set on the current thread only when specific events occur.  These events will be determined according to end-user's ability to understand the error message.
    • When an error is encountered and an error object isn't needed, call SetErrorInfo(NULL)
      • This prevents using the error object from previous errors
    • This doesn't include the times when method returns an HRESULT indicating failure
      • Because the method would have already set the ErrorInfo object
    • But it may include times when code calls third-party code that doesn't implement ISupportsErrorInfo

Returning Error Information

  1. Implement the ISupportErrorInfo interface.
  2. To create an instance of the generic error object, call the CreateErrorInfo function.
  3. To set its contents, use the ICreateErrorInfo methods.
  4. To associate the error object with the current logical thread, call the SetErrorInfo function.

The following figure illustrates this procedure.

To retrieve error information

  1. Check whether the returned value represents an error that the object is prepared to handle.
  2. Call QueryInterface to get a pointer to the ISupportErrorInfo interface. Then, call InterfaceSupportsErrorInfo to verify that the error was raised by the object that returned it and that the error object pertains to the current error, and not to a previous call.
  3. To get a pointer to the error object, call the GetErrorInfo function.
  4. To retrieve information from the error object, use the IErrorInfo methods.

The following figure illustrates this procedure.

If the object is not prepared to handle the error, but needs to propagate the error information further down the call chain, it should simply pass the return value to its caller. Because the GetErrorInfo function clears the error information and passes ownership of the error object to the caller, the function should be called only by the object that handles the error.


Beware the Connection Point Implementation Wizard

Revised on: 2001-06-30

It's very easy to add a connection point to an ATL object.  However, there are problems with the generated code, which always seems to be the case with Microsoft's wizards.

  1. If you haven't already done so, create a DISPINTERFACE for your event
    [
    uuid(A6AE2118-A509-4cd0-A48C-D9EB6742AE3D),
    nonextensible,
    helpstring("")
    ]
    dispinterface _DIEvent
    {
    properties:
    methods:

    [id(1), helpstring("")] HRESULT OnEvent([in] long data);
    };
  2. Go to the Class View in the Project Viewer
  3. Right-click on the object name that you want to add the connection point to
  4. Select "implement connection point"
  5. Choose the event interface that the connection point invokes (this is the outgoing interface)
  6. Open the *CP.h file
  7. Comment out the #import of the DLL or #import the TLB instead.  The DLL probably won't exist at build time.
  8. Advise, Unadvise, and Fire_ lock the object's critical section.  Therefore the following scenario will result in a deadlock
    1. Your object has Both or Free model
    2. The event sink has Apartment model
    3. Your object calls Lock()
    4. Your object calls Fire_Foo()
    5. The event sink object calls Unadvise()

    The best way to avoid this is to make sure your object doesn't have its critical section locked when calling Fire.

  9. The Fire_ methods will fail if an event sink calls Unadvise.  Also, the method returns a bogus SCODE if there are no event sinks registered with the object.  Here is an example fix.
    HRESULT Fire_Event(long data)
    {
    	/* Copy the list of event sinks to a local
    	   variable to eliminate issues with
    	   Unadvise being called here */
    	CComDynamicUnkArray vec;
    	T* pT = static_cast<T*>(this);
    	pT->Lock();
    	if (!m_vec.GetSize())
    	{
    		pT->Unlock();
    		return S_OK;
    	}
    	vec = m_vec;
    	pT->Unlock();
    
    	CComVariant varResult;
    	int nConnectionIndex;
    	CComVariant* pvars = new CComVariant[1];
    	int nConnections = vec.GetSize();
    
    	for (	nConnectionIndex = 0; 
    		nConnectionIndex < nConnections; 
    		nConnectionIndex++)
    	{
    		CComPtr<IUnknown> sp;
    		sp = vec.GetAt(nConnectionIndex);
    		IDispatch* pDispatch = 
    			reinterpret_cast<IDispatch*>(sp.p);
    		if (pDispatch != NULL)
    		{
    			VariantClear(&varResult);
    			pvars[0] = data;
    			DISPPARAMS disp = { pvars, NULL, 2, 0 };
    			pDispatch->Invoke(0x1, IID_NULL, 
    			LOCALE_USER_DEFAULT, 
    			DISPATCH_METHOD, 
    			&disp, &varResult, NULL, NULL);
    		}
    		delete[] pvars;
    		return varResult.scode;
    	}
    

(c) 2001 DevGuy.com