Есть подводные камни, поскольку LIB загружаются только в том случае, если на них есть ссылка, в отличие от OBJ, которые включены явно.
Когда я переместил свой код в библиотеку, что случилось с COM-объектами ATL?
Предостережение: в этом посте обсуждаются детали работы ATL7. Для другой версии ATL YMMV. Общие принципы применимы ко всем версиям, но детали могут различаться.
Недавно моя группа работала над сокращением количества DLL, составляющих функцию, над которой мы работаем (с 8 до 4). В рамках этого я провел последние пару недель, объединяя кучу ATL COM DLL.
Для этого я сначала изменил библиотеки DLL для создания библиотек, а затем соединил библиотеки с помощью фиктивной подпрограммы DllInit (которая в основном просто вызывала CComDllModule::DllInit()), чтобы создать DLL.
Все идет нормально. Все связалось, и я приготовился протестировать новую DLL.
По какой-то причине, когда я попытался зарегистрировать DLL, при регистрации фактически не были зарегистрированы COM-объекты. В этот момент я начал корить себя за то, что забыл одно из фундаментальных различий между связыванием объектов вместе для создания исполняемого файла и связыванием библиотек вместе для создания исполняемого файла.
Чтобы объяснить, я должен немного рассказать о том, как работает компоновщик. Когда вы связываете исполняемый файл (любого типа), компоновщик загружает все разделы в объектных файлах, составляющих исполняемый файл. Для каждого символа extdef в объектных файлах он начинает искать общедоступный символ, соответствующий этому символу.
Как только все символы совпадают, компоновщик выполняет второй проход, объединяя все разделы .code с одинаковым содержимым (это приводит к свертыванию методов шаблона, которые расширяются в один и тот же код (это часто происходит с CComPtr)).
Затем выполняется третий проход. Третий проход отбрасывает все разделы, на которые еще не было ссылок. Поскольку на разделы нет ссылок, они не будут использоваться в результирующем исполняемом файле, поэтому их включение просто раздует исполняемый файл.
Итак, почему мои COM-объекты на основе ATL не были зарегистрированы? Что ж, пора поиграть в детектива.
Что ж, оказывается, вам нужно немного покопаться в коде ATL, чтобы понять это.
Логика регистрации ATL COM выбирается в объекте CComModule. Внутри этого объекта есть метод RegisterClassObjects, который перенаправляет на AtlComModuleRegisterClassObjects. Эта функция просматривает список _ATL_OBJMAP_ENTRY структур и вызывает RegisterClassObject для каждой структуры. Список извлекается из m_ppAutoObjMapFirst члена CComModule (хорошо, это действительно член _ATL_COM_MODULE70, который является базовым классом для CComModule). Так откуда взялось это поле?
Он инициализируется в конструкторе CAtlComModule, который получает его из глобальной переменной __pobjMapEntryFirst. Так откуда взялось поле __pobjMapEntryFirst?
На самом деле релевантных полей два: __pobjMapEntryFirst и __pobjMapEntryLast.
Вот определение для __pobjMapEntryFirst:
__declspec(selectany) __declspec(allocate("ATL$__a")) _ATL_OBJMAP_ENTRY* __pobjMapEntryFirst = NULL;
А вот определение для __pobjMapEntryLast:
__declspec(selectany) __declspec(allocate("ATL$__z")) _ATL_OBJMAP_ENTRY* __pobjMapEntryLast = NULL;
Давайте разберем это:
__declspec(selectany): __declspec(selectany) — это директива компоновщику выбрать любой из элементов с одинаковыми именами из раздела, другими словами, если элемент __declspec(selectany) найден в нескольких объектных файлах, просто выберите один, не жалуйтесь на его многократное определение.
__declspec(allocate("ATL$__a")): Это тот, который заставляет волшебство работать. Это объявление для компилятора, оно говорит компилятору поместить переменную в раздел с именем "ATL$__a" (или "ATL$__z").
Хорошо, это хорошо, но как это работает?
Что ж, чтобы объявить мой COM-объект на основе ATL, я включил следующую строку в свой заголовочный файл:
OBJECT_ENTRY_AUTO(<my classid>, <my class>)
OBJECT_ENTRY_AUTO расширяется в:
#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)
Обратите внимание на объявление __pobjMap_##class выше, там снова эта штука declspec(allocate("ATL$__m")). И в этом заключается магия. Когда компоновщик размещает код, он сортирует эти разделы в алфавитном порядке, поэтому переменные в разделе ATL$__a будут стоять перед переменными в разделе ATL$__z. Итак, под прикрытием происходит то, что ATL просит компоновщика разместить все переменные __pobjMap_<class name> в исполняемом файле между __pobjMapEntryFirst и __pobjMapEntryLast.
И в этом суть проблемы. Помните мой комментарий выше о том, как работает компоновщик при разрешении символов? Сначала он загружает все элементы (код и данные) из переданных файлов OBJ и разрешает для них все внешние определения. Но ни один из файлов в каталоге-оболочке (а это те, которые связаны явно) не ссылается на какой-либо код в DLL (помните, что оболочка не делает ничего, кроме простого вызова функций-оболочек ATL, на которые она не ссылается). любой код в других файлах.
Итак, как я решил проблему? Простой. Я знал, что как только компоновщик вытащит модуль, содержащий определение моего COM-класса, он начнет разрешать все элементы в этом модуле. Включая __objMap_<class>, который затем будет добавлен в нужное место, чтобы ATL могла его подобрать. Я поместил фиктивный вызов функции с именем ForceLoad<MyClass> внутри модуля в библиотеке, а затем добавил функцию с именем CallForceLoad<MyClass> в мой файл точки входа DLL (примечание: я просто добавил функцию, которую не вызывал из какого-либо кода).
И вуаля, код был загружен, и теперь фабрики классов для моих COM-объектов автоматически регистрируются.
Что было еще круче, так это то, что, поскольку ни один живой код не вызывал две фиктивные функции, которые использовались для извлечения библиотеки, передайте три компоновщика, который отбросил код!