COM 介绍 Part2

Introduction to COM Part II - Behind the Scenes of a COM Server

本文章翻译自如下链接:
http://www.codeproject.com/Articles/901/Introduction-to-COM-Part-II-Behind-the-Scenes-of-a

这篇文章的目的

我在 上一篇文章 中已经说明过,写这篇文章的目的是为了帮助那些刚刚入门 COM 编程的程序员更好地理解 COM 基本概念。上一篇文章介绍了 COM 的基本概念,这篇文章主要介绍 COM Server 相关的细节,及如何编写自己的 COM 接口和 COM Server, 并且介绍了 COM library 调用时 COM Server 中实际进行的操作有哪些。

介绍

如果你已经读过我的 上一篇文章 , 你应该已经了解了如何从客户端的角度来使用 COM Server。现在,该学习 COM 的另外一端 COM Server 本身了。我将用纯 C++ 代码展示如何编写一个 COM Server, 不会调用到任何其它类库。通过这种方式,你能更好地理解 server 中所发生的事情。

这篇文章假定你熟悉 C++ 并且理解了上一篇文章说说的若干概念。本文包含如下几个章节:

  • ** 简要介绍 COM Server ** 描述一个 COM Server 所需要具备的基本功能
  • ** Server 的生命周期管理 ** 描述 COM Server 如何控制它加载的时间
  • ** 从 IUnknown 开始,实现接口 ** 展示如何用 C++ class 来实现接口,并介绍 IUnknown 接口各函数的作用
  • ** 深入 CoCreateInstance() ** 简要介绍当你调用 CoCreateInstance() 时内部所发生的操作
  • ** COM Server 的注册 ** 介绍注册 COM Server 时所需要设置的注册表键值
  • ** 创建 COM 对象 - Class Factory ** 介绍为客户端程序创建 COM 对象的流程
  • ** 一个简单的自定义接口 ** 用一个简单的例子说明之前的概念
  • ** 使用我们的 COM 对象 ** 写一个简单的客户端程序来测试我们的 COM Server
  • ** 其它细节 ** 在源码和调试时需要注意的地方

简要介绍 COM Server

在本文中,我们将看到一个最简单的 COM Server,一个 in-process server(进程内服务), "In-process" 意味着服务将被加载到客户端的进程地址空间中。通常它们是 DLL,并且必须与客户端程序同处于一台机器上。

一个 in-proc server 能够被 COM library 使用,必须满足两条准则:

  1. 它必须被正确地注册到 HKEY_CLASSES_ROOT\CLSID 键值中;
  2. 它必须导出一个函数叫做 DllGetClassObject();

对应上述准则,为了让 in-proc server 正常工作,你至少要做以下工作:将 server 的 GUID 注册到 HKEY_CLASSES_ROOT\CLSID 键值下,并且这个键值必须包含一堆值标识 Server 的路径及它的线程模型。 DllGetClassObject() 函数将在 CoCreateInstance() 中由 COM library 调用。

通常还有其它几个接口需要被导出:

  • DllCanUnloadNow() : 由 COM library 调用从而判断 server 能否从内存中卸载;
  • DllRegisterServer() : 由安装工具如 RegSvr32 调用从而让 server 将自己注册到注册表中;
  • DllUnregisterServer() : 由下载工具调用从而让 server 将自己从注册表键值中移除;

当然,仅仅导出正确的函数是不够的,这些函数必须符合 COM 规范才能给 COM library 和 客户端程序使用。

Server 的生命周期管理

对于 DLL Server,有一点容易忽视的是它其实能够控制自己的加载时间。一般的 DLL 是“被动的”,它们只能通过程序的控制来加载或卸载。通常情况下, DLL Server 也是“被动的” ,毕竟它也是个 DLL。但 COM library 提供了一种机制使得 server 能够告知 COM 它可以被卸载。这种机制是通过 DllCanUnloadNow() 接口来实现的。它的原型如下:

HRESULT DllCanUnloadNow();

客户端程序可以在进程空闲时调用 COM API CoFreeUnusedLibraries(),这个函数会调用所有 DLL Server 的这个函数,判断是否可以将这个 server 卸载。如果该函数返回 S_FALSE 表示不能被卸载,返回 S_OK 表示可以。

Server 本身判断自己能否被卸载可以使用引用计数的方法,以下是一个简单的实现:

extern UINT g_uDllRefCount;

HRESULT DllCanUnloadNow()
{
    return (g_uDllRefCount >0) ? S_FALSE : S_OK;
}

下一章节会讨论引用计数的细节,并给出简单的代码示例。

从 IUnknown 开始,实现接口

再说一遍,所有的接口都继承自 IUnknown. 这是因为 IUnknown 包含了 COM 对象的两个基本特性——引用计数和接口查询。当你编写一个 coclass 的时候,你同时也编写了一个满足你要求的 IUnknown 接口实现。接下来展示一段 coclass 代码,它仅仅实现了 IUnknown 接口。

class CUnknownImpl : public IUnknown
{
public:
    // 构造和析构函数
    CUnknownImpl();
    virtual CUnknownImpl();

    // IUnknown 函数
    ULONG AddRef();
    ULONG Release();
    HRESULT QueryInterface(REFIID riid, void** ppv);

protected:
    // 对象的引用计数
    UINT m_uRefCount;
}

构造和析构函数

这里我们在构造函数里管理 server 的引用计数:

CUnknownImpl::CUnknownImpl()
{
    m_uRefCount = 0;
    g_uDllRefCount ++;
}

CUnknownImpl::~CUnknownImpl()
{
    g_uDllRefCount --;
}

当新的 COM 对象被创建时,构造函数将被调用,因此需要在这时 递增 g_uDllRefCount,保证 Server 不被卸载。同时,在这时将 COM 对象的 m_uRefCount 设定为 0。 m_uRefCount 维护的是 COM 对象本身的引用计数。

而在 COM 对象被销毁时,析构函数将被调用,在这时递减 g_uDllRefCount.

AddRef() 和 Release()

这两个函数是用来控制 COM 对象的生命周期的:

ULONG CUnknownImpl::AddRef()
{
    return ++m_uRefCount;
}

AddRef() 简单地递增了引用计数,并返回递增后的引用计数。

ULONG CUnknownImpl::Release()
{
    ULONG uRet = --m_uRefCount;
    if ( 0 == m_uRefCount )
        delete this;
    
    return uRet;
}

Release() 中,除了递减引用计数之外,当它检测到引用计数为 0 时,将销毁 COM 对象。Release() 调用完之后会返回新的引用计数。注意,以上的实现假定 COM 对象是在堆中被创建的。如果你在栈上或者全局域上创建了 COM 对象,那么 delete this 操作将发生错误。

现在你应该能理解为啥在 客户端程序中正确调用 AddRef() 和 Release() 这么重要。如果你没有正确调用它们,COM 对象可能会过早被销毁,或者一直没销毁。如果销毁过早的话,COM Server 可能会提前从内存中卸载,导致下次调用接口时程序崩溃。

如果你要编写的是多线程程序,你可能会担心使用 ++,-- 而不是 InterlockedIncrement(), InterlockedDecrement() 而导致的线程安全问题。但是,如果你的 COM Server 是一个 single-thread server(单线程 Server), 那么 ++,-- 就不会有问题。即使客户端程序是多线程的,并且在不同的线程中调用了 COM Server 的接口,COM library 也会以序列的方式调用 server 接口。也就是说,一个函数开始调用后,下一个视图调用的函数会被暂时锁住,直到第一个函数返回后才能执行。COM library 保证了我们的 server 任何时候都不会有多于一个线程同时进入访问。

QueryInterface()

客户端程序通过 QueryInterface() 从 COM 对象中请求不同的接口。因为我们的示例代码只实现了一个接口,所以它很简单。QueryInterface() 接受两个参数,接口的 IID、用于接收接口指针的缓冲区地址指针。

HRESULT CUnknownImpl::QueryInterface( REFIID riid, void** ppv)
{
    HRESULT hrRet = S_OK;
    *ppv = NULL;

    if ( IsEqualIID( riid, IID_IUnknown) )
    {
        *ppv = (IUnknown*) this;
    }
    else
    {
        hrRet = E_NOINTERFACE;
    }

    if ( S_OK == hrRet )
    {
        ((IUnknown*) *ppv)->AddRef();
    }

    return hrRet;
}

上面代码做了三件事儿:

  1. 将传入的指针初始化为 NULL; [*ppv = NULL;]
  2. 检查 coclass 是否实现了 riid 所要求的接口;[if ( IsEqualIID( riid, IID_IUnknown) )]
  3. 如果返回了接口指针,增加 COM 对象的引用计数;[((IUnknown*) *ppv)->AddRef();]

注意, AddRef() 很重要,这一行代码:

*ppv = (IUnknown*) this;

创建了 COM 对象的一个新的引用,因此要调用 AddRef() 来告知 COM 对象这个新引用的存在。将 ppv 转换为 IUnknown 然后再调用 AddRef() 看起来很奇怪。但在实际的代码中, ppv 可能不只是一个 IUnknown,转换后再去掉用 AddRef() 是一个好的习惯。

现在我们已经了解了 DLL Server 的一些内部细节,接下来我们退回去看看 CoCreateInstance() 里是如何使用 server 的。

深入 CoCreateInstance()

在上一篇文章中,我们已经介绍过 CoCreateInstance(),当客户端程序调用它时它将创建一个所需要的 COM 对象。从客户端的角度来看, CoCreateInstance() 是一个黑盒,只要给他传入正确的函数,你就能得到一个 COM 对象。这里面可没什么黑魔法,它有一套经过完善定义的流程去处理 COM Server 的载入、COM 对象的创建、接口的返回。

以下是这个流程的概览,有一些我们之前没讲过,接下来的章节里会详细讨论:

  1. 客户端程序调用 CoCreateInstance(), 传入 coclass 的 CLSID 和所需要的接口的 IID;
  2. COM library 在注册表 HKEY_CLASSES_ROOT\CLSID 里查询 server 的 CLSID。这个键值存储了 server 的注册表信息;
  3. COM library 读取 server DLL 的全路径并将 DLL 载入客户端程序的进程地址空间;
  4. COM library 调用 server 的 DllGetClassObject() 来获取 coclass 的类工厂 class factory;
  5. server 创建一个类工厂,并通过 DllGetClassObject() 返回;
  6. COM library 调用类工厂的 CreateInstance() 函数来创建 COM 对象;
  7. CoCreateInstance() 返回 COM 对象的接口指针;

COM Server 的注册

在一切开始之前,COM Server 必须被正确地注册到 Windows 注册表中。如果你去看一下 HKEY_CLASSES_ROOT\CLSID 键,你会发现它下面有一堆子键。HKCR\CLSID 存储了当前电脑中所有可用的 COM Server 信息。当一个 COM Server 被注册时(通常通过 DllRegisterServer()), 它会在 CLSID 键下面创建一个以 server 的 GUID 为名称的子键。例如下面这种:

{067DF822-EAB6-11cf-B56E-00A0244D5087} = CLSID_UnknownImpl
    |
    |__> InprocServer32
            |
            |__> default = C:\UnknownImpl.dll
            |
            |__> ThreadingModel = Apartment

大括号和连字符都是必要的,但字母大小写都可以;

这个 key 的默认 value 是一个可读的 coclass 名称,VC 的 OLE/COM Object Viewer 工具可以看到这个名称;

详细信息存储在这个 key 的子键中,这个子键名称如何依赖于你的 COM Server 是什么类型。对于我们这个简单程序来说,设定为 InProcServer32 就好了。

InProcServer32 子键中又有两个键值,分别是默认值,它表示 server DLL 的全路径;以及一个 ThreadingModel 值,它表示 server 所使用的线程模型。线程模型超出了本文的范围,我们这儿直接把他设定为 Apartment 就好了。

创建 COM 对象 - Class Factory

从客户端的角度去看 COM,我之前已经说过 COM 对创建和销毁对象有它一套自己的语言无关的机制。客户端调用 CoCreateInstance() 来创建 COM 对象,现在我们从 Server 的角度来看看它是怎么工作的。

每当你实现了一个 coclass, 同时你还需要写一个 伙伴 coclass 来专门负责创建第一个 coclass 实例。这个伙伴 coclass 也被叫做 class factory. 它的核心功能就是创建 COM 对象。之所以要编写 class factory 的原因就是为了之前说的“语言无关性”,COM 本身不能去创建对象,因为对象的创建是语言相关的。

当客户端试图创建 COM 对象时, COM library 会先从 COM Server 里请求对应的 class factory. 得到 class factory 之后,COM 利用它来创建对象然后返回给 客户端。这一机制通过 DllGetClassObject() 来实现。

class factory 和 object factory 实际上表示的是同一个东西。object factory 更清楚地表述了 class factory 的功能,因为 factory 创建的是 COM object, 而不是 COM class. 用 object factory 来表述可能更清楚一些(实际上, MFC 就是这么干的,它的 class factory 的实现就叫做 COleObjectFactory)。然而,官方明确确实是叫 class factory, 所以本文也沿用这个名称。

当 COM library 调用 DllGetClassObject() 的时候,它会传入客户端所请求的 CLSID。由 Server 负责创建 CLSID 对应的 class factory 并返回。class factory 本身是一个实现了 IClassFactory 的 coclass. 如果 DllGetClassObject() 执行成功,它将返回一个 IClassFactory 接口给 COM library, 它将使用这个接口来创建 COM 对象并返回给 客户端程序。

IClassFactory 接口大概长这样儿:

struct IClassFactory : public IUnknown
{
    HRESULT CreateInstance( IUnknown* pUnkOuter, REFIID riid, void** ppvObject);
    HRESUTL LockServer( BOOL bLock );
}

CreateInstance() 函数用于创建 COM 对象; LockServer() 函数可以让 COM library 在必要的时候递增或递减 server 的引用计数。

一个简单的自定义接口

下面介绍一个 DLL Server 的代码例子,它定义了一个 ISimpleMsgBox 接口, 并用一个叫做 CSimpleMsgBoxImpl 的 coclass 实现了这个接口。

接口定义

我们的新接口叫做 ISimpleMsgBox,像其它接口一样,它必须继承自 IUnknown. 这个接口里只有一个函数 DoSimpleMsgBox(), 注意它返回标准类型 HRESULT。你写得所有函数都应该以这个类型作为返回值,如果需要返回其它内容,必须通过指针参数来返回。

struct ISimpleMsgBox : public IUnknown
{
    ULONG AddRef();
    ULONG Release();
    HRESULT QueryInterface( REFIID riid, void** ppv );

    HRESULT DoSimpleMsgBox( HWND hwndParent, BSTR bsMessageText );
};

struct __declspec(uuid("{7D51904D-1645-4a8c-BDE0-0F4A44FC38C4}"))
                ISimpleMsgBox;

上面代码中的 __declspec 操作符将一个 GUID 复制给了 ISimpleMsgBox 符号,这个 GUID 随后可以用 __uuidof 操作符来获取到。__declspec__uuidof 这两个操作符都是 微软 C++ 扩展 中的操作符。

DoSimpleMsgBox() 的第二个参数是一个 BSTR 类型的值。BSTR 代表 "binary string", 这是 COM 里表示定长字节序列的一个类型。BSTR 主要用在脚本型客户端像是 Visual Basic, Windows Scripting Host 里面。

定义完了接口,接下来用一个叫做 CSimpleMsgBoxImpl 的 C++ 类来实现这个接口:

class CSimpleMsgBoxImpl : ISimpleMsgBox
{
public:
    CSimpleMsgBoxImpl();
    virtual ~CSimpleMsgBoxImpl();

    ULONG AddRef();
    ULONG Release();
    HRESULT QueryInterface( REFIID riid, void** ppv );

    HRESULT DoSimpleMsgBox( HWND hwndParent, BSTR bsMessageText );

protected:
    ULONG m_uRefCount;
}

class __declspec(uuid("{7D51904E-1645-4a8c-BDE0-0F4A44FC38C4}"))
                CSimpleMsgBoxImpl;

客户端可以用如下方式来创建一个 SimpleMsgBox COM 对象:

ISimpleMsgBox* pIMsgBox;
HRESULT hr;

hr = CoCreateInstance( __uuidof(CSimpleMsgBoxImpl),  // coclass 的 CLSID
                        NULL,
                        CLSCTX_INPROC_SERVER,
                        __uuidof(ISimpleMsgBox),    // interface 的 IID
                        (void**) &pIMsgBox );

The Class Factory

** 我们的 class factory 实现 **

SimpleMsgBox 类的类工厂也用 C++ 类来实现,叫做 CSimpleMsgBoxClassFactory :

class CSimpleMsgBoxClassFactory : public IClassFactory
{
public:
    CSimpleMsgBoxClassFactory();
    virtual ~CSimpleMsgBoxClassFactory();
    
    ULONG AddRef();
    ULONG Release();
    HRESULT QueryInterface(REFIID riid, void** ppv);

    HRESULT CreateInstance( IUnknown* pUnkOuter, REFIID riid, void** ppv);
    HRESULT LockServer( BOOL bLock );

protected:
    ULONG m_uRefCount;
}

构造、析构函数、 IUnknown 接口函数都像之前的例子那样写就可以了。跟之前不同的是我们要实现 IClassFactory 接口中的函数:

HRESULT CSimpleMsgBoxClassFactory::CreateInstance( IUnknown* pUnkOuter, REFIID riid, void** ppv)
{
    // 不支持聚集,因此 pUnkOuter 必须是 NULL
    if ( NULL != pUnkOuter )   
        return CLASS_E_NOAGGREGATION;
    
    // ppv 必须是指向 void* 的指针
    if ( IsBadWritePtr ( ppv, sizeof(void*) ) )  
        return E_POINTER;

    *ppv = NULL;

    // 创建 COM 对象
    CSimpleMsgBoxImpl* pMsgBox;
    pMsgBox = new CSimpleMsgBoxImpl;

    if ( NULL == pMsgBox )
        return E_OUTOFMEMORY;
    
    HRESULT hrRet;
    // 查询客户端所请求的接口,如果失败,说明对象不可用,要删除掉
    hrRet = pMsgBox->QuetyInterface( riid, ppv );

    if ( FAILED(hrRet) )
    {
        delete pMsgBox;
    }
    
    return hrRet;
}

** DllGetClassObject() **

接下来看看 DllGetClassObject() 的内部,它的函数原型是:

HRESULT DllGetClassObject( REFCLSID rclsid, REFIID riid, void** ppv);

rclsid 是客户端请求的 coclass 的 CLSID。函数将返回该 CLSID 所指的 coclass 的 class factory。

riid 和 ppv 跟 QueryInterface() 里的参数功能类似。在这里, riid 是 COM library 所请求的 class factory 接口的 IID,通常写为 IID_IClassFactory.

因为 DllGetClassObject() 创建了一个新的 COM 对象(class factory), 因此代码和 CreateInstance() 类似:

HRESULT DllGetClassObject( REFCLSID rclsid, REFIID riid, void** ppv)
{
    // 比较传入的 rclsid 是否是 CSimpleMsgBoxImpl 的 CLSID
    if ( !InlineIsEqualGUID( rclsid __uuidof(CSimpleMsgBoxImpl) ) )
        return CLASS_E_CLASSNOTAVAILABLE;

    // ppv 必须是指向 void* 的指针
    if ( IsBadWritePtr ( ppv, sizeof(void*) )
        return E_POINTER;
    
    *ppv = NULL;

    // 创建 class factory 对象
    CSimpleMsgBoxClassFactory* pFactory;
    pFactory = new CSimpleMsgBoxClassFactory;

    if ( NULL == pFactory )
        return E_OUTOFMEMORY;
    
    // 我们要使用 pFactory 的 QueryInterface 接口,所以要 AddRef
    pFactory->AddRef();

    HRESULT hrRet;
    hrRet = pFactory->QueryInterface( riid, ppv );
    
    // 接口用完了,所以要 Release;
    pFactory->Release();

    return hrRet;
}

上面的 AddRef() 和 Release() 看起来有点儿奇怪,在 CreateInstance() 的例子里并没有这么调用。其实这是两种不同的写法,功能都是一样的,也就是在 QueryInterface() 失败的时候,删除 pFactory 对象。第一次调用 AddRef() 引用计数为 1;QueryInterface() 成功后,引用计数为 2, 失败则仍然是 1;之后再调用 Release(),那么若 QueryInterface() 成功,这时引用计数减为 1,失败时则减为 0,pFactory 自动销毁自己。COM library 使用完 pFactory 之后,会再次调用 Release() 这时就能删除掉 pFactory 了。

** 再看 QueryInterface() **

之前我已经展示过一个 QueryInterface() 的代码例子,但 class factory 的 QueryInterface() 接口要复杂一些。因为它不仅仅要实现 IUnknown 接口还得进行一系列检查:

HRESULT CSimpleMsgBoxClassFactory::QueryInterface( REFIID riid, void** ppv)
{
    HRESULT hrRet = S_OK;

    if ( IsBadWritePtr( ppv, sizeof(void*) ) 
        return E_POINTER;

    *ppv = NULL;

    if ( InlineIsEqualGUID( riid, IID_IUnknown ))
    {
        *ppv = (IUnknown*) this;
    }
    else if ( InlineIsEqualGUID ( riid, IID_IClassFactory) )
    {
        *ppv = (IClassFactory*) this
    }
    else
    {
        hrRet = E_NOINTERFACE;
    }

    if ( S_OK == hrRet )
    {
        ((IUnknown*) *ppv)->AddRef();
    }
    
    return hrRet;
}

** ISimpleMsgBox 实现 **

最后,看看 ISimpleMsgBox 的实现,它就只有一个函数 DoSimpleMsgBox(). 我们使用 微软扩展类 _bstr_t 来将 bsMessageText 转换成 TCHAR 类型字符串,然后用 MessageBox 展示它:

HRESULT CSimpleMsgBoxImpl::DoSimpleMsgBox( HWND hwndParent, BSTR bsMessageText )
{
    _bstr_t bsMsg = bsMessageText;
    LPCSTR szMsg = (TCHAR*) bsMsg;

    MessageBox(hwndParent, szMsg, _T("Simple Message Box"), MB_OK);

    return S_OK;
}

使用我们的 COM 对象

我们的 COM Server 已经搞好了,怎么用呢?现阶段我们的 接口还是一个 custom interface, 这意味着它只能被 C\C++ 客户端使用。(如果我们的 coclass 实现了 IDispatch 接口,我们就能在任何语言的客户端上使用它,这个就不在本文里探讨了)。

客户端代码很简单,使用 CoCreateInstance() 创建 COM 对象,然后调用它的 DoSimpleMsgBox() 函数弹出一个 MessageBox:

void DoMsgBoxTest(HWND hMainWnd)
{
    ISimpleMsgBox* pIMsgBox;
    HRESULT hr;

    hr = CoCreateInstance(__uuidof(CSimpleMsgBoxImpl),
                        NULL,
                        CLSCTX_INPROC_SERVER,
                        __uuidof(ISimpleMsgBox),
                        (void**) &pIMsgBox);
    
    if ( FAILED(hr) )
        return;
    
    pIMsgBox->DoSimpleMsgBox(hMainWnd, _bstr_t("Hello COM!"));
    pIMsgBox->Release();
}

其它细节

COM 宏

COM 里有一些 C\C++ 宏隐藏了具体实现,我在文章里没有用这些宏,但例子代码里用了,因此在这儿解释一下这些宏怎么用,以下是 ISimpleMsgBox 的声明:

struct ISimpleMsgBox : public IUnknown
{
    STDMETHOD_(ULONG AddRef)() PURE;
    STDMETHOD_(ULONG Release)() PURE;
    STDMETHOD(QueryIntreface)(REFIID riid, void** ppv) PURE;

    STDMETHOD(DoSimpleMsgBox)(HWND hwndParent, BSTR bsMessageText) PURE;
};

STDMETHOD() 包含了 virtual 关键字、HRESULT 返回值,_stdcall 调用约定;
STDMETHOD
() 跟上面一样,只是你可以指定一个不是 HRESULT 的返回值类型。
PURE 也就是 C++ 里的 "=0", 表示这个函数是纯虚函数;

STDMETHOD() 和 STDMETHOD_() 都有对应的宏,用在函数实现上,分别是 STDMETHODIMP 和 STDMETHODIMP_, 例如下面是 DoSimpleMsgBox() 函数的实现:

STDMETHODIMP CSimpleMsgBox::DoSimpleMsgBox(HWND hwndParent, BSTR bsMessageText)
{
    // ...
}

最后,标准导出函数可以使用 STDAPI 宏,如下:

STDAPI DllRegisterServer()

STDAPI 宏 包含了返回类型和调用约定。另外如果你使用了 STDAPI ,你不能再用 _declspec(dllexport) 来描述。你必须用 .DEF 文件来导出。

server 的注册和反注册

server 要实现 DllRegisterServer() 和 DllUnregisterServer() 函数。这俩函数用来注册和反注册 server。这种操作很枯燥,我这儿就不写了。他创建的注册表键值类似下面这样:

{067DF822-EAB6-11cf-B56E-00A0244D5087}
    |
    |__> InprocServer32
            |
            |__> default = [path to dll]
            |
            |__> ThreadingModel = Apartment

在 server 中设置断点

如果你想在 server 里设置断点,有两种方法:

  1. 将 server 所在工程设定为启动工程,然后开始调试, VS 会让你输入一个可执行程序的路径来加载 dll。输入后就可以进行调试了。
  2. 将 客户端程序所在工程设定为启动工程,然后设置项目依赖项。这样也可以调试 server。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,293评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,604评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,958评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,729评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,719评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,630评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,000评论 3 397
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,665评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,909评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,646评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,726评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,400评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,986评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,959评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,996评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,481评论 2 342

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,594评论 18 139
  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,567评论 18 399
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,426评论 25 707
  • 生活中的习惯在物理上即为惯性,习惯有着根深蒂固的力量,这种力量就像地球引力将我们固定在地球上一样,使我们难以超越,...
    白卉阅读 307评论 0 4
  • 这段时间过得比较匆忙,也过得比较没心没肺,一个字:忙,但不充盈……内心所需要的感觉并不是这种。 想安安静...
    麦子火了阅读 382评论 1 1