c++ - 是否可以在静态库中编写COM代码,然后将其链接到DLL?

标签 c++ dll com static-libraries atl

我目前正在一个项目中,该项目具有许多用C++和ATL编写的COM对象。

当前,它们都在直接编译到COM DLL中的.cpp和.idl文件中定义。

为了使单元测试更容易编写,我计划将COM对象的实现移到一个单独的静态库中。然后可以将该库链接到主DLL和单独的单元测试项目中。

我假设ATL生成的代码没有什么特别的,并且在与静态库链接时,它的工作原理与所有其他C++代码一样。但是,我本人对ATL的了解并不多,所以不知道是否确实如此。

我会按预期工作吗?还是我应该注意的陷阱?

最佳答案

因为只有在引用了LIB的情况下才将它们引入,所以存在一些陷阱,这与明确包含的OBJ相反。

Larry Osterman discussed some of the subtleties a few years ago:

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!

关于c++ - 是否可以在静态库中编写COM代码,然后将其链接到DLL?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/7935619/

相关文章:

c# - BadImageFormatException 未处理

c++ - 如何修复架构 x86_64、Boost Asio 1.58 的 undefined symbol

c++ - 如何在 native 回调中使用 Cython cdef 类成员方法

c++ - 参数评估与链式方法之间是否存在有保证的“先发生”关系?

c# - 帮我获取两个同名的分支第三方 dll,以便更好地发挥作用

java - 如何在java中访问C++库(DLL)的方法

com - 微软补丁下载地址

c++ - 操作HNet_ConnectionProperties的C++代码是什么

c++ - 未按描述收到事件通知

c# - 通过 COM Interop 公开 C# 类