COM编程以及DLL代理

文章详细介绍了COM编程中的关键概念,包括AppID、DLL代理的实现,以及如何通过命名管道进行进程间通信。COM对象的创建流程和IUnknown接口的重要性被强调,同时也讨论了DLL显式调用和接口的转型。此外,文章还提到了ATL库在实现IUnknown接口时的角色,以及COM对象的注册和反注册过程。
摘要由CSDN通过智能技术生成

COM编程以及DLL代理


COM编程攻略(十九 AppID、Dll代理)
COM编程总结

  • chatgpt 32位dll代理
#include <iostream>
#include <Windows.h>

int main()
{
    HANDLE hPipe;
    DWORD dwRead;
    CHAR buffer[1024];

    // 创建命名管道
    hPipe = CreateNamedPipe(TEXT("\\\\.\\pipe\\MyNamedPipe"),
        PIPE_ACCESS_DUPLEX,
        PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE | PIPE_WAIT,
        PIPE_UNLIMITED_INSTANCES,
        1024, 1024, 0, NULL);

    if (hPipe == INVALID_HANDLE_VALUE)
    {
        std::cout << "创建命名管道失败,错误码: " << GetLastError() << std::endl;
        return 1;
    }

    std::cout << "等待连接..." << std::endl;

    // 等待客户端连接
    if (ConnectNamedPipe(hPipe, NULL))
    {
        std::cout << "客户端已连接" << std::endl;

        // 读取来自客户端的数据
        if (ReadFile(hPipe, buffer, sizeof(buffer), &dwRead, NULL))
        {
            std::cout << "从客户端接收到的数据: " << buffer << std::endl;
        }

        // 向客户端发送数据
        const char* sendData = "Hello from 32-bit proxy!";
        DWORD dwWritten;
        if (WriteFile(hPipe, sendData, strlen(sendData) + 1, &dwWritten, NULL))
        {
            std::cout << "向客户端发送的数据: " << sendData << std::endl;
        }
        else
        {
            std::cout << "向客户端发送数据失败,错误码: " << GetLastError() << std::endl;
        }

        // 关闭管道
        CloseHandle(hPipe);
    }
    else
    {
        std::cout << "等待客户端连接失败,错误码: " << GetLastError() << std::endl;
        return 1;
    }

    return 0;
}

COM编程


DLL代理

  1. 简单来说就是编写一个dll,让他运行在另外一个进程
    • 直观地来讲,就是启动一个服务进程,并且加载这个dll——这个过程,就叫作dll代理。
  2. 该操作存在的原因、技术上讲的好处
    • 防止客户端进程崩溃,eg:google,会开很多进程,如渲染进程、GPU加速进程,其中一个崩溃了无伤大雅。
    • 可以远程调用。例如,利用COM机制,这个dll可以在另外一个机器上被宿主exe加载。这样能更好进行分布式的操作。
    • 更加安全。客户端进程外的服务进程,和客户端处于不同地址空间,所以不能拿到客户端的很多信息。
  3. DLL显示代理
    1. LoadLibrary显示调用dll或者exe的函数,自带封装

    2. GetProcAddress返回函数地址给句柄(句柄,“函数名”)

      // B.dll
      IClassFactory* declspec(__dllexport) getReadWriteFactory(int asd)
      {
          if (asd) -------
      static ReadWriteFactory s_factory;
      return &s_factory;
      }
      
      
      // A.exe
      // dll的显式调用
      HMODULE hLib = LoadLibrary("B.dll");
      typedef IClassFactory* (*GetReadWriteFactoryProc)();
      GetReadWriteFactoryProc getReadWriteFactory = (GetReadWriteFactoryProc)GetProcAddress(hLib, "getReadWriteFactory");
      IClassFactory* factory = getReadWriteFactory();
      IRead* reader;
      factory->CreateInstance(NULL, IID_IREAD, (void**)&reader);
      
    3. COM对象的创建流程和上面类似。首先,我们必须要加载类所在的DLL,然后我们要找到需要创建的实例的类,获取其IClassFactory接口,接下来再调用类的IClassFactory::CreateInstance()

    4. 具体方式见本文COM对象的创建方法


AppID


用户接口

  • 用户只能拿到抽象类的接口,而派生类对用户不可见
  • 用户无法看到派生类是如何实现函数的,也不知道具体调用的是哪个派生类
  • 成员变量对用户也是不可见
  • 基类调用派生类的变量应该是不能直接调用的,可能需要函数来访问

COM底层接口IUnknown

struct IUnknown
{
    virtual HRESULT STDMETHODCALLTYPE QueryInterface( 
        /* [in] */ REFIID riid,
        /* [iid_is][out] */ _COM_Outptr_ void __RPC_FAR *__RPC_FAR *ppvObject) = 0;

    virtual ULONG STDMETHODCALLTYPE AddRef( void) = 0;

    virtual ULONG STDMETHODCALLTYPE Release( void) = 0;
}
  1. 所有的COM对象,必须继承IUnknown接口,并且实现它的语义:
    • QueryInterface(): 这个是IUnknown最核心的一个接口,叫做“变身(误)”。它的第一个参数名字叫做接口ID,接口ID是我们人为给每个接口生成的一个唯一的GUID,它一般命名为IID_接口名。如上面的IFile,它的接口ID为IID_IFile。第二个接口得到对象变身后的值。

      • QueryInterface担当的其实就是一个转型的工作。
      • 创建的对象是派生类对象,通过基类指针的类型转换,决定该指针可以调用哪个函数
      • MSDN是微软中国的微博
    • AddRef(): 表示此对象引用计数加一,比如被外面持有时,需要调用AddRef()。返回之后的引用计数。

    • Release(): 表示此对象引用计数减一,一旦引用计数为0,实现者必须要释放此对象。

    • 上述两个方法很容易实现,例如引用计数声明为ULONG m_ref;那么AddRef()就是return ++m_refRelease稍微要多做一点事情:

          --m_ref;
          if (m_ref == 0)
              delete this;
          return m_ref;
      
    • 由于m_ref类型为ULONG,如果它从0往下减,那么会得到一个非常大的值(0xFFFFFFFF...FFFF),这样我们几乎永远都不能释放这个对象了。我们之后可以在ATL中看到它们的默认实现来处理这个问题。

      struct IRead : public IUnknown
      {
      virtual byte* read() = 0;
      };
      struct IWrite: public IUnknown
      {
      virtual void write(byte) = 0;
      }
      
      class ReadWrite : public IRead, public IWrite
      {
      ULONG AddRef();
      ULONG Release();
      HRESULT QueryInterface(REFIID iid, void** ppvObject);
      byte* read();
      void write(byte);
      };
      
    • struct 是接口,但是为什么这样还没弄明白,只是都喜欢用struct来写接口

      • 有一个回答是struct默认是public,不用在class中定义public了?

IUnknown对象实现模型(没看懂)

  • COM重用模型聚合重用模型
  • 我的理解是
    • 继承模型的接口只有一个,其余的派生类的接口在基类内部进行操作,用户能看到的只有最外层的一个接口
    • 聚合模型的封装与继承模型不同,他的接口是直接面向用户,也就是用户可以看到所有的接口

1. 继承模型

2. 聚合模型


硬核ATL实现IUnknown代码解析(没看懂)

  1. ATL:一种微软的程序库

COM对象创建的原理及ATL实现

  1. COM对象

    1. COM对象是遵循COM规范编写、以Win32动态链接库(DLL)或可执行文件(EXE)形式发布的可执行二进制代码,能够满足对组件架构的所有需求。
  2. 一个ClassB这样的COM对象,我们不关心它的实现类ClassB,而只需要拿它的某一个接口。

    1. 例如,我们只需要在ClassB中拿到IRead或者IWrite接口,如果我们调用了ClassB中除了这两个接口之外的函数或者成员并调用了,那么它就和ClassB的实现耦合了起来,违背了COM对象解决二进制兼容的原则。
    2. 所以,在创建对象的时候,我们要指明,我们需要拿哪个接口。
    void* CreateInstance(REFIID iid)
        {
        ClassB* b = new ClassB();
        if (iid是IID_IWrite)
            return static_cast<IWrite*>(b);
        if (iid是IID_IRead)
            return static_cast<IRead*>(b);
        return nullptr;
        }
    
  3. 新名词 暴露

  4. COM对象的创建流程和上面类似。

    1. 首先,我们必须要加载类所在的DLL,然后我们要找到需要创建的实例的类,获取其IClassFactory接口
    2. 接下来再调用类的IClassFactory::CreateInstance()
  5. COM API将上面的步骤合并成了一步。类和接口一样,也有一个唯一的ID,接口叫做IID,类叫做CLSID,它们本质上都是GUID结构。

  6. 我们的目的是,将一个本地DLL,加载到exe的进程中,所以属于进程内运行的模式。COM API提供了下面这个API来获取接口

    HRESULT CoGetClassObject(
    REFCLSID rclsid,
    DWORD    dwClsContext,
    LPVOID   pvReserved,
    REFIID   riid,
    LPVOID   *ppv
    );
    
    1. rclsid是指class id。所有的class id均保存在注册表的HKCR

一般对象的创建方法

  1. 整体原理是利用ClassFactory来创建并反汇Class实例,但是工厂存在于dll文件中,因此需要暴露一个函数给客户端

    1. GetClassFactory

      // B.dll
      IClassFactory* declspec(__dllexport) getReadWriteFactory()
      {
        static ReadWriteFactory s_factory;
        return &s_factory;
      }
      
  2. 于是,客户端就可以通过此方法获取类工厂,并且返回类对象,用于后续操作

    // A.exe
    HMODULE hLib = LoadLibrary("B.dll");
    typedef IClassFactory* (*GetReadWriteFactoryProc)();
    GetReadWriteFactoryProc getReadWriteFactory = (GetReadWriteFactoryProc)GetProcAddress(hLib, "getReadWriteFactory");
    IClassFactory* factory = getReadWriteFactory();
    IRead* reader;
    factory->CreateInstance(NULL, IID_IREAD, (void**)&reader);
    
    1. 可能会存在多个类工厂,可以通过加参数的方式决定返回的工厂类型

      // B.dll
      IClassFactory* declspec(__dllexport) getReadWriteFactory(int which);
      

COM对象的创建方法

  1. 与一般对象类似,但是会多一点参数

  2. 类和接口一样,也有一个唯一的ID,接口叫做IID,类叫做CLSID,它们本质上都是GUID结构

  3. COM API获取类对象的接口

    HRESULT CoGetClassObject(
      REFCLSID rclsid,
      DWORD    dwClsContext,
      LPVOID   pvReserved,
      REFIID   riid,
      LPVOID   *ppv
    );
    
    1. rclsid以及在注册表中存在的位置
    2. 第四个参数是需要返回的接口ID,猜测是工厂类的ID
  4. 当上述参数的CoGetClassObject被调用后,它首先从注册表找到对应的dll,并且加载它,然后运行dll的一个导出函数

    HRESULT DllGetClassObject(
      REFCLSID rclsid,
      REFIID   riid,
      LPVOID   *ppv
    );
    
    1. 这个函数必须由dll提供方,也就是上面我们说的B.dll来实现。
  5. B.dll将通过class id创建对应的Factory,并调用QueryInterface将结果写入ppv返回。

    1. 此时,客户已经拿到了IClassFactory了,于是可以通过CreateInstance创建实例了。整个过程看起来就是这样

      // A.exe
      IRead* reader;
      IClassFactory* pcf = NULL;
      HRESULT hr = CoGeClassObject(CLSID_ReadWriteFactory,
        CLSCTX_INPROC_SERVER,
        NULL,
        IID_IClassFactory,
        (void**)&pcf);
      if (SUCCEEDED(hr))
      {
        hr = pcf->CreateInstance(NULL, IID_IRead, (void**)&ireader);
        pcf->Release(); // 不要忘记减引用计数
      }
      
    2. 可以理解为客户调用接口之后,接口调用dll的自带导出函数,跟随接口一起返回到客户端

    3. 但是返回的ID可以直接用吗(类的ID和工厂的ID)

    4. 出于效率的原因,微软将上面的3部操作(CoGetClassObject, CreateInstance, Release)合为了一个函数及它的扩展版本:CoCreateInstance, CoCreateInstanceEx

DLL注册和反注册

  1. 为了使得我们的B.dll能够被CoCreateInstance激活,我们必须要修改注册表。微软提供了一个regsvr32.exe,可以来安装、反安装我们dll的服务。

    // 在regsvr32.exe指定安装一个dll服务时,它会调用下面的导出方法:
    STDAPI DllRegisterServer(void);
    // 上面这个函数由B.dll实现,做的事情无非就是写注册表,注册CLSID等信息。
    
    // 对应的,在regsvr32.exe指定反安装一个dll服务时,调用下面的方法:
    STDAPI DllUnregisterServer(void);
    // 这个方法也由B.dll实现,职责是清除注册时写入注册表的信息。
    
  2. 由于写注册表工作非常重复而且繁琐,所以ATL提供了一个类,来专门完成注册表写入和删除相关事情:

    class CATLReadWriteModule : public ATL::CAtlDllModuleT< CATLReadWriteModule >
    {
    ...
    };
    CATLReadWriteModule _AtlModule;
    STDAPI DllRegisterServer(void)
    {
        // 注册对象、类型库和类型库中的所有接口
        HRESULT hr = _AtlModule.DllRegisterServer();
        return hr;
    }
    
    // DllUnregisterServer - 移除系统注册表中的项。
    _Use_decl_annotations_
    STDAPI DllUnregisterServer(void)
    {
        HRESULT hr = _AtlModule.DllUnregisterServer();
        return hr;
    }
    

动态调用与IDispatch接口

  1. 获取Class ID的另外一种方式
    1. COM提供了2个API,通过ProgID或CLSID来进行相互查询
    2. CLSIDFromProgID
    3. ProgIDFromCLSID

动态调用与IDispatch接口

  1. 获取Class ID的另外一种方式
    1. COM提供了2个API,通过ProgID或CLSID来进行相互查询
    2. CLSIDFromProgID
    3. ProgIDFromCLSID
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值