When I moved my code into a library, what happened to my ATL COM objects?
A caveat: This post discusses details of how ATL7 works. For other version of ATL, YMMV. The general principals apply for all versions, but the details are likely to be different.
My group’s recently been working on reducing the number of DLLs that make up the feature we’re working on (going from somewhere around 8 to 4). As a part of this, I’ve spent the past couple of weeks consolidating a bunch of ATL COM DLL’s.
To do this, I first changed the DLLs to build libraries, and then linked the libraries together with a dummy DllInit routine (which basically just called CComDllModule::DllInit()) to make the DLL.
So far so good. Everything linked, and I got ready to test the new DLL.
For some reason, when I attempted to register the DLL, the registration didn’t actually register the COM objects. At that point, I started kicking my self for forgetting one of the fundamental differences between linking objects together to make an executable and linking libraries together to make an executable.
To explain, I’ve got to go into a bit of how the linker works. When you link an executable (of any kind), the linker loads all the sections in the object files that make up the executable. For each extdef symbol in the object files, it starts looking for a public symbol that matches the symbol.
Once all of the symbols are matched, the linker then makes a second pass combining all the .code sections that have identical contents (this has the effect of collapsing template methods that expand into the same code (this happens a lot with CComPtr)).
Then a third pass is run. The third pass discards all of the sections that have not yet been referenced. Since the sections aren’t referenced, they’re not going to be used in the resulting executable, so to include them would just bloat the executable.
Ok, so why didn’t my ATL based COM objects get registered? Well, it’s time to play detective.
Well, it turns out that you’ve got to dig a bit into the ATL code to figure it out.
The ATL COM registration logic gets picked in the CComModule object. Within that object, there’s a method RegisterClassObjects, which redirects to AtlComModuleRegisterClassObjects. This function walks a list of _ATL_OBJMAP_ENTRY structures and calls the RegisterClassObject on each structure. The list is retrieved from the m_ppAutoObjMapFirst member of the CComModule (ok, it’s really a member of the _ATL_COM_MODULE70, which is a base class for the CComModule). So where did that field come from?
It’s initialized in the constructor of the CAtlComModule, which gets it from the __pobjMapEntryFirst global variable. So where’s __pobjMapEntryFirst field come from?
Well, there are actually two fields of relevance, __pobjMapEntryFirst and __pobjMapEntryLast.
Here’s the definition for the __pobjMapEntryFirst:
__declspec(selectany) __declspec(allocate("ATL$__a")) _ATL_OBJMAP_ENTRY* __pobjMapEntryFirst = NULL;
And here’s the definition for __pobjMapEntryLast:
__declspec(selectany) __declspec(allocate("ATL$__z")) _ATL_OBJMAP_ENTRY* __pobjMapEntryLast = NULL;
Let’s break this one down:
__declspec(selectany): __declspec(selectany) is a directive to the linker to pick any of the similarly named items from the section – in other words, if a __declspec(selectany) item is found in multiple object files, just pick one, don’t complain about it being multiply defined.
__declspec(allocate(“ATL$__a”)) – This one’s the one that makes the magic work. This is a declaration to the compiler, it tells the compiler to put the variable in a section named “ATL$__a” (or “ATL$__z”).
Ok, that’s nice, but how does it work?
Well, to get my ATL based COM object declared, I included the following line in my header file:
OBJECT_ENTRY_AUTO(<my classid>, <my class>)
OBJECT_ENTRY_AUTO expands into:
#define OBJECT_ENTRY_AUTO(clsid, class) \
__declspec(selectany) ATL::_ATL_OBJMAP_ENTRY __objMap_##class = {&clsid, class::UpdateRegistry, class::_ClassFactoryCreatorClass::CreateInstance, class::_CreatorClass::CreateInstance, NULL, 0, class::GetObjectDescription, class::GetCategoryMap, class::ObjectMain }; \
extern "C" __declspec(allocate("ATL$__m")) __declspec(selectany) ATL::_ATL_OBJMAP_ENTRY* const __pobjMap_##class = &__objMap_##class; \
OBJECT_ENTRY_PRAGMA(class)
Notice the declaration of __pobjMap_##class above – there’s that “declspec(allocate(“ATL$__m”))” thingy again. And that’s where the magic lies. When the linker’s laying out the code, it sorts these sections alphabetically – so variables in the ATL$__a section will occur before the variables in the ATL$__z section. So what’s happening under the covers is that ATL’s asking the linker to place all the __pobjMap_<class name> variables in the executable between __pobjMapEntryFirst and __pobjMapEntryLast.
And that’s the crux of the problem. Remember my comment above about how the linker works resolving symbols? It first loads all the items (code and data) from the OBJ files passed in, and resolves all the external definitions for them. But none of the files in the wrapper directory (which are the ones that are explicitly linked) reference any of the code in the DLL (remember, the wrapper doesn’t do much more than simply calling into ATL’s wrapper functions – it doesn’t reference any of the code in the other files.
So how did I fix the problem? Simple. I knew that as soon as the linker pulled in the module that contained my COM class definition, it’d start resolving all the items in that module. Including the __objMap_<class>, which would then be added in the right location so that ATL would be able to pick it up. I put a dummy function call called “ForceLoad<MyClass>” inside the module in the library, and then added a function called “CallForceLoad<MyClass>” to my DLL entry point file (note: I just added the function – I didn’t call it from any code).
And voila, the code was loaded, and the class factories for my COM objects were now auto-registered.
What was even cooler about this was that since no live code called the two dummy functions that were used to pull in the library, pass three of the linker discarded the code!
Comments
- Anonymous
September 27, 2004
One thing I'd like clarified:
You say that in the third pass the linker "discards all of the sections that have not yet been referenced". The AtlComModuleRegisterClassObjects() function walks the list of _ATL_OBJMAP_ENTRY structures,and presumably it references the __pobjMapEntryFirst pointer (which is in seciont "ATL$__a") and __pobjMapEntryLast (which is in section "ATL$__z") to get to the list and know how large the list is.
However, it seems that it doesn't directly reference any of the pointers that are placed into the "ATL$__m" section. So what prevents the linker from discarding section "ATL$__m"?
Thanks,
mikeb - Anonymous
September 27, 2004
One thing I'd like clarified:
You say that in the third pass the linker "discards all of the sections that have not yet been referenced". The AtlComModuleRegisterClassObjects() function walks the list of _ATL_OBJMAP_ENTRY structures,and presumably it references the __pobjMapEntryFirst pointer (which is in seciont "ATL$__a") and __pobjMapEntryLast (which is in section "ATL$__z") to get to the list and know how large the list is.
However, it seems that it doesn't directly reference any of the pointers that are placed into the "ATL$__m" section. So what prevents the linker from discarding section "ATL$__m"?
Thanks,
mikeb - Anonymous
September 27, 2004
Hmm. That's a good question Mike, I'll have to ask the ATL guys. You're right, it should be discarded.
But it may be that by explicitly giving a section, that provides a reference. But this is raw speculation.
I do know that referencing the module fixes the problem, but... - Anonymous
September 28, 2004
In atlbase.h there's a #pragma to merge all ATL* sections into .rdata. This is why ATL$__m isn't discarded.
#pragma comment(linker, "/merge:ATL=.rdata")
Also, OBJECT_ENTRY_PRAGMA is pretty important as it expands to the /include linker option to force a reference to the symbol. I don't know why this didn't force your objects to not be discarded. - Anonymous
September 28, 2004
Ah, I missed that Mutexed, thanks.
The reason was that the modules weren't even considered during the link phase.
Once a symbol from the module was forced to be loaded, then the linker considered all the symbols in the module, which meant that it finally processed the OBJECT_ENTRY_PRAGMA directive. - Anonymous
September 28, 2004
The comment has been removed