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代理
- 简单来说就是编写一个dll,让他运行在另外一个进程
- 直观地来讲,就是启动一个服务进程,并且加载这个dll——这个过程,就叫作dll代理。
- 该操作存在的原因、技术上讲的好处
- 防止客户端进程崩溃,eg:google,会开很多进程,如渲染进程、GPU加速进程,其中一个崩溃了无伤大雅。
- 可以远程调用。例如,利用COM机制,这个dll可以在另外一个机器上被宿主exe加载。这样能更好进行分布式的操作。
- 更加安全。客户端进程外的服务进程,和客户端处于不同地址空间,所以不能拿到客户端的很多信息。
- DLL显示代理
-
LoadLibrary
显示调用dll或者exe的函数,自带封装 -
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);
-
COM对象的创建流程和上面类似。首先,我们必须要加载类所在的DLL,然后我们要找到需要创建的实例的类,获取其IClassFactory接口,接下来再调用类的
IClassFactory::CreateInstance()
。 -
具体方式见本文
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;
}
- 所有的COM对象,必须继承
IUnknown
接口,并且实现它的语义:-
QueryInterface()
: 这个是IUnknown
最核心的一个接口,叫做“变身(误)”。它的第一个参数名字叫做接口ID,接口ID是我们人为给每个接口生成的一个唯一的GUID,它一般命名为IID_接口名。如上面的IFile,它的接口ID为IID_IFile。第二个接口得到对象变身后的值。- QueryInterface担当的其实就是一个转型的工作。
- 创建的对象是派生类对象,通过基类指针的类型转换,决定该指针可以调用哪个函数
MSDN
是微软中国的微博
-
AddRef()
: 表示此对象引用计数加一,比如被外面持有时,需要调用AddRef()。返回之后的引用计数。 -
Release()
: 表示此对象引用计数减一,一旦引用计数为0,实现者必须要释放此对象。 -
上述两个方法很容易实现,例如引用计数声明为ULONG m_ref;那么AddRef()就是
return ++m_ref
,Release
稍微要多做一点事情:--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代码解析(没看懂)
- ATL:一种微软的程序库
COM对象创建的原理及ATL实现
-
COM对象
- COM对象是遵循COM规范编写、以
Win32
动态链接库(DLL
)或可执行文件(EXE
)形式发布的可执行二进制代码,能够满足对组件架构的所有需求。
- COM对象是遵循COM规范编写、以
-
一个
ClassB
这样的COM对象,我们不关心它的实现类ClassB
,而只需要拿它的某一个接口。- 例如,我们只需要在
ClassB
中拿到IRead
或者IWrite
接口,如果我们调用了ClassB
中除了这两个接口之外的函数或者成员并调用了,那么它就和ClassB
的实现耦合了起来,违背了COM对象解决二进制兼容的原则。 - 所以,在创建对象的时候,我们要指明,我们需要拿哪个接口。
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; }
- 例如,我们只需要在
-
新名词
暴露
-
COM对象的创建流程和上面类似。
- 首先,我们必须要加载类所在的
DLL
,然后我们要找到需要创建的实例的类,获取其IClassFactory
接口 - 接下来再调用类的
IClassFactory::CreateInstance()
- 首先,我们必须要加载类所在的
-
COM API将上面的步骤合并成了一步。类和接口一样,也有一个唯一的
ID
,接口叫做IID
,类叫做CLSID
,它们本质上都是GUID
结构。 -
我们的目的是,将一个本地
DLL
,加载到exe
的进程中,所以属于进程内运行的模式。COM API
提供了下面这个API
来获取接口HRESULT CoGetClassObject( REFCLSID rclsid, DWORD dwClsContext, LPVOID pvReserved, REFIID riid, LPVOID *ppv );
rclsid
是指class id
。所有的class id
均保存在注册表的HKCR
处
一般对象的创建方法
-
整体原理是利用ClassFactory来创建并反汇Class实例,但是工厂存在于dll文件中,因此需要暴露一个函数给客户端
-
GetClassFactory
// B.dll IClassFactory* declspec(__dllexport) getReadWriteFactory() { static ReadWriteFactory s_factory; return &s_factory; }
-
-
于是,客户端就可以通过此方法获取类工厂,并且返回类对象,用于后续操作
// 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);
-
可能会存在多个类工厂,可以通过加参数的方式决定返回的工厂类型
// B.dll IClassFactory* declspec(__dllexport) getReadWriteFactory(int which);
-
COM对象的创建方法
-
与一般对象类似,但是会多一点参数
-
类和接口一样,也有一个唯一的ID,接口叫做IID,类叫做CLSID,它们本质上都是GUID结构
-
COM API获取类对象的接口
HRESULT CoGetClassObject( REFCLSID rclsid, DWORD dwClsContext, LPVOID pvReserved, REFIID riid, LPVOID *ppv );
- rclsid以及在注册表中存在的位置
- 第四个参数是需要返回的接口ID,猜测是工厂类的ID
-
当上述参数的CoGetClassObject被调用后,它首先从注册表找到对应的dll,并且加载它,然后运行dll的一个导出函数
HRESULT DllGetClassObject( REFCLSID rclsid, REFIID riid, LPVOID *ppv );
- 这个函数必须由dll提供方,也就是上面我们说的B.dll来实现。
-
B.dll将通过class id创建对应的Factory,并调用QueryInterface将结果写入ppv返回。
-
此时,客户已经拿到了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(); // 不要忘记减引用计数 }
-
可以理解为客户调用接口之后,接口调用dll的自带导出函数,跟随接口一起返回到客户端
-
但是返回的ID可以直接用吗(类的ID和工厂的ID)
-
出于效率的原因,微软将上面的3部操作(CoGetClassObject, CreateInstance, Release)合为了一个函数及它的扩展版本:CoCreateInstance, CoCreateInstanceEx
-
DLL注册和反注册
-
为了使得我们的B.dll能够被CoCreateInstance激活,我们必须要修改注册表。微软提供了一个regsvr32.exe,可以来安装、反安装我们dll的服务。
// 在regsvr32.exe指定安装一个dll服务时,它会调用下面的导出方法: STDAPI DllRegisterServer(void); // 上面这个函数由B.dll实现,做的事情无非就是写注册表,注册CLSID等信息。 // 对应的,在regsvr32.exe指定反安装一个dll服务时,调用下面的方法: STDAPI DllUnregisterServer(void); // 这个方法也由B.dll实现,职责是清除注册时写入注册表的信息。
-
由于写注册表工作非常重复而且繁琐,所以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接口
- 获取Class ID的另外一种方式
- COM提供了2个API,通过ProgID或CLSID来进行相互查询
- CLSIDFromProgID
- ProgIDFromCLSID
动态调用与IDispatch接口
- 获取Class ID的另外一种方式
- COM提供了2个API,通过ProgID或CLSID来进行相互查询
- CLSIDFromProgID
- ProgIDFromCLSID