如何在C++中使用LoadLibrary来调用COM DLL?

10
首先,COM 对我来说就像黑魔法一样。但是我需要在我正在开发的一个项目中使用 COM dll。
所以,我有一个正在开发的 DLL,我需要一些在单独的 COM DLL 中可用的功能。当我使用 Depends.exe 查看 COM DLL 时,我看到了 DllGetClassObject() 等方法,但没有我感兴趣的函数。
我可以访问 COM DLL(遗留)的源代码,但它很混乱,我更愿意像使用黑盒一样使用二进制 COM DLL,不知道里面发生了什么。
那么,我该如何使用 LoadLibrary 从我的代码中调用 COM DLL 函数呢?这是可能的吗?如果是,请给我一个如何做的示例。
我在这个项目中使用 Visual Studio 6。
非常感谢!
6个回答

23
一般情况下,您应该优先选择使用 CoCreateInstanceCoGetClassObject,而不是直接访问 DllGetClassObject 。但如果您正在处理一个无法或不想注册的 DLL,则下面描述了这些函数在幕后执行的部分操作。
给定 CLSID,DllGetClassObject 允许您获取类对象,从中可以创建实例(通过 IClassFactory 接口,如果我没记错的话)。
步骤摘要(已经有一段时间没有碰过 COM 了,所以请原谅任何明显的错误):
  1. 调用 DllGetClassObject(clsid, IID_IClassFactory, &cf),其中 clsid 是要获取类对象的 CLSID,而 cf 当然是类工厂。
  2. 调用 cf->CreateInstance(0, iid, &obj),其中 iid 是要使用的接口的 IID,obj 当然是对象。
  3. ???
  4. 利润!
CoCreateInstance 执行步骤 1 和 2。 CoGetClassObject 执行步骤 1。如果需要创建多个相同类的实例,则可以使用 CoGetClassObject,以便无需每次重复执行步骤 1。)

2
+1 对于扑克论坛行话渗透到其他极客领域的现象。 - John Dibling
这是使用随机第三方代码进行的危险操作:如果目标 COM 对象使用的线程模型与您自己的线程不同,可能会发生糟糕的事情(多个线程执行仅为一个编写的代码)。如果幸运,它会崩溃;如果不幸,你会得到损坏的数据。CoCreate 在幕后做了额外的工作来匹配线程模型。只有在这是你自己的代码或你绝对确定线程模型时才能这样做。(COM 还负责在适当的时间卸载库;虽然在那种情况下,你可以永远不卸载以避免这个问题。) - BrendanMcK
1
@ChrisJester-Young; 这个问题的关键是不清楚 OP 是否真的想这么做 - 还是只是刚接触 COM,想要使用基于 COM 的 DLL,并且仅提到 LoadLibrary 是因为他们之前只使用过普通的 Win32 DLL。在答案顶部添加一个适当的免责声明(“使用 CoCreate,但如果你不能,例如因为你需要 X,那么...”)通常是最安全的方法,特别是如果答案是某些你自己通常不会采用的东西;否则,通过搜索找到这篇文章的 OP 或随意读者可能会将其误认为是推荐的方法。 - BrendanMcK
1
@BrendanMcK:说得好。我已经添加了这样的免责声明,并解释了推荐函数实际执行的操作(不包括线程模型检查等内容)。 - C. K. Young
1
谢谢,这真的很有帮助,因为我想找到一种方法来调试一个 shell 扩展 (IThumbnailProvider),而不必在每次编译更改后重新注册 dll。 - Andrew Wyatt
显示剩余3条评论

15
通常你会使用CoCreateInstance()从COM DLL中实例化一个对象。当你这样做时,不需要像普通DLL那样先加载DLL并获取过程地址。这是因为Windows“知道”COM DLL实现的类型、它们所在的DLL以及如何实例化它们。(当然假设COM DLL已经注册,这通常是这样的)。
假设你有一个带有IDog接口的COM DLL要使用。在这种情况下,

dog.idl

interface IDog : IUnknown
{
  HRESULT Bark();
};

coclass Dog
{
  [default] Interface IDog;
};

我的代码.cpp

IDog* piDog = 0;
CoCreateInstance(CLSID_DOG, 0,  CLSCTX_INPROC_SERVER, IID_IDOG,  &piDog); // windows will instantiate the IDog object and place the pointer to it in piDog
piDog->Bark();  // do stuff
piDog->Release();  // were done with it now
piDog = 0;  // no need to delete it -- COM objects generally delete themselves

虽然所有这些内存管理的内容都可能变得相当凌乱,不过ATL提供了智能指针使得实例化和管理这些对象的任务稍微容易了一些:

CComPtr<IDog> dog;
dog.CoCreateInstance(CLSID_DOG);
dog->Bark();

编辑:

在我之前所说的:

  

Windows“知道”COM DLL实现的类型[...]以及它们实现在哪个DLL中

...我没有详细说明Windows是如何知道这一点的。虽然这可能看起来有点神秘,但它并不是魔术。

COM库带有类型库,其中列出了库提供的接口和Co类。类型库以文件形式存在于硬盘上--通常直接嵌入到与库本身相同的DLL或EXE中。Windows通过查看Windows注册表来知道类型库和COM库本身的位置。注册表中的条目告诉Windows DLL在硬盘上的位置。

当您调用CoCreateInstance时,Windows查找Windows注册表中的clsid,找到相应的DLL,加载它,并执行实现COM对象的DLL中的正确代码。

这些信息是如何进入Windows注册表中的?当安装COM DLL时,它会被注册。这通常是通过运行regsvr32.exe来完成的,然后将您的DLL加载到内存中并调用名为DllRegisterServer的函数。该函数在您的COM服务器中实现,向注册表添加必要的信息。如果使用ATL或另一个COM框架,则可能在幕后执行此操作,因此您不必直接与注册表交互。只需要在安装时调用一次DllRegisterServer

如果尝试为尚未通过regsvr32/DllRegisterServer过程注册的COM对象调用CoCreateInstance,则CoCreateInstance将失败,并显示错误信息:

  

类未注册

幸运的是,修复此问题的方法是简单地在您的COM服务器上调用regsvr32,然后再次尝试。


+1,这是正确的答案。 COM也可能在幕后执行额外的工作,以确保线程模型匹配:因此,编写使用多个线程的应用程序可以安全地CoCreate假定为单个线程的组件,反之亦然。根据组件的注册方式,COM可能还会执行一些其他操作,但这可能是最重要的要注意的事情。长话短说,只需使用CoCreateInstance;如果您尝试自己实现并且不是COM专家,则很有可能会在某个地方出现微妙的错误。 - BrendanMcK
2
COM DLL的注册不是自动的,如果这是一个自制的DLL,则可能会忽略此步骤。在这种情况下,CoCreateInstance将失败。您应该在答案中涉及一下这个问题。 - Mark Ransom
@Mark:好建议,已编辑。你真的挖出了一个老古董。 :) - John Dibling
我没有挖掘它,一些毫无头绪的新手活动将其带回了活动列表。关于注册DLL的简要概述很不错。 - Mark Ransom

7

在使用COM库时,你不会直接使用LoadLibrary()函数。如果还没有调用该函数,CoCreateInstance()函数将调用它,然后在堆上新建一个你实现的类的实例,并返回指向该对象的原始指针。当然,在此过程中可能会失败,因此需要一些机制来检查状态,例如HRESULT。

为了简化使用,你可以将COM库视为带有以下特点的普通DLL:1)具有一些预定义的入口(main)函数;2)你必须调用一些预定义的函数(如CoCreateInstance())才能进入其中,并接受这种方式是因为必须这样做。


1
我喜欢你对COM的解释。 - Contango

3
如果类型库嵌入在动态链接库中,您可以将其导入到您的项目中:
#import "whatever.dll"

这将自动生成头文件并包含在您的项目中,使您能够使用导出的对象。

2
这里有一段代码,展示了如何获取类工厂并使用它创建COM对象。它使用一个结构体来跟踪模块句柄和DllGetClassObject函数指针。在使用COM对象期间,您应该保留模块句柄。
要使用此功能,您需要分配ComModuleInfo结构的实例,并将szDLL设置为DLL文件名或完整路径名。然后,使用所需的COM对象的类ID和接口ID调用该函数。
typedef struct {
   TCHAR   szDLL[MAX_PATH];
   HMODULE hModule;
   HRESULT (WINAPI *pfnGetFactory)(REFCLSID, REFIID, void**);
   } ComModuleInfo;

HRESULT CreateCOMObject(
   ComModuleInfo & mod,  // [in,out] 
   REFCLSID iidClass,    // [in] CLSID of the COM object to create
   REFIID iidInterface,  // [in] GUID of the interface to get
   LPVOID FAR* ppIface)  // [in] on success, interface to the COM object is returned
{
    HRESULT hr = S_OK;

    *ppIface = NULL; // in case we fail, make sure we return a null interface.

    // init the ComModuleInfo if this is the first time we have seen it.
    //
    if ( ! mod.pfnGetFactory)
    {     
       if ( ! mod.hModule)
       {
          mod.hModule = LoadLibrary(mod.szDLL);
          if ( ! mod.hModule)
             return HRESULT_FROM_WIN32(GetLastError());
       }
       mod.pfnGetFactory = (HRESULT (WINAPI *)(REFCLSID, REFIID, void**))GetProcAddress(mod.hModule, "DllGetClassObject");
       if ( ! mod.pfnGetFactory)
          return HRESULT_FROM_WIN32(GetLastError());
    }

    IClassFactory* pFactory = NULL;
    hr = mod.pfnGetFactory(iidClass, IID_IClassFactory, (void**)&pFactory);
    if (SUCCEEDED(hr))
    {
       hr = pFactory->CreateInstance(NULL, iidInterface, (void**)ppIface);
       pFactory->Release();
    }

    return hr;
}

2
你可能要澄清什么情况下会使用这种方法:似乎使用CoCreateInstance应该是通常的选择,只有作为最后一种手段才会使用这种技术——例如,如果类未注册,并且你确定对象使用与调用者兼容的线程模型。对于COM的新手来说,没有上下文阐明的话,这个答案可能是一个潜在的危险,因为他们可能会错误地认为这种方法通常比使用CoCreateInstance()更可取。 - BrendanMcK

0
如果它是一个COM DLL,你只需要将其添加为项目的引用,然后就可以调用DLL中的函数。
是的,你可以使用低级别的COM函数,比如DLLGetClassObject,但是为什么要这样做呢?

3
手动加载的一个好处是你的 DLL 不必注册就可以使用。 - C. K. Young
为什么会有一个未注册的COM DLL呢?这似乎非常不可能... - Greg
在XP及之后版本中,您可以使用清单文件来访问未注册的COM DLL。 - Nemanja Trifunovic
2
注册的COM对象可以被你的竞争对手使用。很多公司认为这是一件坏事。 - John Knoeller

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接