本文不会对Com进行非常详细的分析 因为这个技术分析起来难度还是非常大的 要想真正弄懂还是非常困难的 我只会针对d3d中使用到的com技术和comptr技术进行说明 所以看完本文后 可以熟练使用d3d中使用到的相应技术
comptr类似于c++11中的智能指针,用于管理com对象的生命周期,所以我们先会讲解com对象的知识,然后才会讲解comptr的相关内容
为什么微软要推出COM技术?
一言以蔽之 为了彻底解决二进制兼容的问题(即生成的dll文件可以被不同语言和不同编译器生成的可执行文件所加载和调用,并且可以随便升级dll文件而不会影响到原来的老代码) 但是为了解决这个问题实在是有太多技术难点要克服。
所以导致com技术的实现原理现在异常复杂 那么问题就来到了 所谓的二进制兼容问题到底是什么?
假设我们现在使用c++代码编写了一个类 并且生成了dll文件提供给别人使用
我们的头文件mydll.h如下:
class __declspec(dllexport) MyClass
{
public:
MyClass(int i);
int TestFunc();
private:
int member_age_;
};
mydll.cpp源文件如下:
#include"mydll.h"
MyClass::MyClass(int i)
{
member_age_ = i;
}
int MyClass::TestFunc()
{
member_age_++;
return member_age_;
}
我们生成为dll后 给别人使用 在别人的代码里会像这种方式来使用我们的代码:
#include"mydll.h"
int main()
{
MyClass* temp=new MyClass(1);
//做一些事情
delete temp;
return 0;
}
这段代码看似没有任何问题, 但是当我们更新了我们的dll文件过后 如在MyClass类中添加了新的成员变量,这段代码将直接导致问题,因为new的时候是先调用底层的malloc分配内存 而老版本的dll中只有一个int变量,新的则不是 所以malloc分配的内存大小就会出问题.直接就会导致你的内存访问越界
所以!如果我们想让用户能够不重新编译出新的exe 直接替换dll文件就能使用我们新的dll 那么我们就不能够让用户写的代码中来为我们写好的类分配内存,应该让我们自己来分配
所以现在我们改造了我们的头文件,导出了创造对象的函数,让用户的代码中不再能够直接new我们的类
mydll.h
class __declspec(dllexport) MyClass
{
public:
MyClass(int i);
int TestFunc();
private:
int member_age_;
};
__declspec(dllexport) MyClass* create();
mydll.cpp
#include"mydll.h"
MyClass::MyClass(int i)
{
member_age_ = i;
}
int MyClass::TestFunc()
{
member_age_++;
return member_age_;
}
MyClass* create()
{
return new MyClass(1);
}
现在用户使用我们的类的方式如下:
#include"mydll.h"
int main()
{
MyClass* i = create();
i->TestFunc();
}
这样即使我们更新了我们的dll,用户exe文件也不需要重新编译,可以无缝直接使用
但是现在我们又面临一个问题,我们该如何让用户来释放这个由create函数申请的内存呢?
很多小伙伴可能直接就会把代码写成如下的样子
#include"mydll.h"
int main()
{
MyClass* i = create();
i->TestFunc();
delete i;
}
但是这样是没法去真正释放内存的!为什么呢?因为每一家编译器厂商对于malloc的底层实现都不一样 所以delete会导致内存释放错误!delete是先调用析构函数,然后再调用free,问题就在于这个free,如果比较了解malloc的小伙伴就会知道,内存管理是一个比较麻烦的过程,会有很多额外的内存空间来记录当前内存块的状态,大小等东西 一块简化的由malloc申请的内存块如下所示
问题就在于什么呢?不同的c标准库对于该实现不是统一的!也就是说,可能msvc和gcc分配的内存块中他们的内存布局不一致,如果你的dll是使用msvc编译器编译的而用户的编译器是gcc那么调用free将会直接导致内存释放错误!
基于以上原因,我们现在也不能让用户手动的调用delete释放我们的内存了,我们继续改写我们的头文件如下
class __declspec(dllexport) MyClass
{
public:
MyClass(int i);
int TestFunc();
private:
int member_age_;
};
__declspec(dllexport) MyClass* create();
__declspec(dllexport) void release(MyClass* temp);
我们的源文件如下
#include"mydll.h"
MyClass::MyClass(int i)
{
member_age_ = i;
}
int MyClass::TestFunc()
{
member_age_++;
return member_age_;
}
MyClass* create()
{
return new MyClass(1);
}
void release(MyClass* temp)
{
delete temp;
}
用户的使用方式如下:
#include"mydll.h"
int main()
{
MyClass* i = create();
i->TestFunc();
release(i);
}
现在似乎一切都安好了,用户不能直接使用new和delete 我们的dll可以无缝更新了,更新之后exe并不会收到影响,但是我们现在又有一个问题了,如何让用户的不同类中共享多份相同的实例呢?你可能会说,那不是很简单嘛?指针都是共享的,直接复制指针不就在不同类中共享多份实例了嘛?
但是问题就在于,你什么时候应该释放该资源呢?比如我A类有一个实例,B类也有一个实例,A类的析构函数会调用release释放内存,B类的也会,那么如果我们不加上控制,是不是直接就重复释放了呢?
基于这种情况 我们就应该给我们的实例加上引用计数
关于引用计数,详情可参考我的这篇博客,对于引用计数有详细解释,这里就不过多叙述了
本质和智能指针中的引用计数是一样的
是时候来点现代c++了 c++11之超级重要之smart pointer详细解析_std::shared_ptr<std::string> (new std::string());_杀神李的博客-CSDN博客
所以我们应该再在头文件加上如下的接口
class __declspec(dllexport) MyClass
{
public:
MyClass(int i);
int TestFunc();
private:
int member_age_;
};
__declspec(dllexport) MyClass* create();
__declspec(dllexport) void release(MyClass* temp);
__declspec(dllexport) bool addref(MyClass* temp);
当我们想共享实例的时候我们调用addref增加引用计数,我们想释放的时候用release减少引用计数,当引用计数为0,释放资源
目前为止,我们仿佛已经真正的解决二进制兼容的问题了,可以随意更新我们的dll文件
所以我们dll中所有的类是不是都应该有这三个接口呢?是的 微软也是这么觉得的 所以微软做了一个最底层的基类 所有的基于com技术的类都应该继承这个类 名字叫做IUnknown(interface unknown)意思就是接口未确定的 这个类是个纯虚类,只规定了所有类都应该有的方法 IUnknown类一共有三个接口
AddRef():增加引用计数
Release():减少引用计数
QueryInterface():用来检查是否可以做向下转型的,因为让用户直接使用强转是很危险的,所以向下转型应该由dll内部来判断
那么问题来了,微软如何判断是否能够安全的向下转型呢?
微软引用了三个ID,guid,iid,clsid来判断
从上来看,所有的COM类其实都继承了IUnknown。但是,我拿个IUnknown接口有毛用啊,我还是需要把它转为我的具体类才行。假设有汽车类Car,它继承于ICar,像这样:
IUnknown* pUnk = NULL;
CreateCar(&pUnk);//这个函数就是我们上文提到的头文件里的create函数
在d3d中该create函数就为D3D12CreateDevice等函数
ICar* pCar = (ICar*)pUnk;
这样,我们拿到ICar指针才有意义。
但是微软认为,直接由用户来转型是不安全的,比如,你怎么知道pUnk一定可以转成ICar*呢。除此之外,ICar这个类不具有唯一性,我们需要唯一的一个标识符来确定一个类,那么这个标识符就是GUID。类ID就叫作CLSID,接口ID就叫作IID。我们需要一个转型的函数叫QueryInterface。
QueryInterface作为IUnknown中的一个纯虚函数,做的事情其实很简单,判断自己能不能转成某个GUID所指向的类而已。如果不可以,则返回E_NOTIMPL.可以的话返回S_OK,并将转换后的指针作为参数返回,代码类似如下,可以体会一下:
public class Car : IUnknown, ICar { HRESULT QueryInterface(REFIID riid, void **ppvObject) { if (ISEQUAL_IID(riid, IID_ICar)) //riid和ICar的IID相同,说明可以转换成ICar { *ppvObject = static_cast<ICar*>(this); return S_OK; } else if (ISEQUAL_IID(riid, IID_IUnknown)) { *ppvObject = static_cast<IUnknown*>(this); return S_OK; } return E_NOTIMPL; } }
一个真正的QueryInterface要做的事情还要多一点,如增加引用计数等,这里就不多说了。
外部是这样调用:
ICar* pCar = NULL;
pUnk->QueryInterface(IID_ICar, (void**)&pCar);
这样,我们就从pUnk得到了个ICar*。
基于这种情况微软又做了封装,既然我每个类都有对应的ID了 那么我的create函数是否可以统一化呢?
所以微软常见的create函数就被封装为了下面这种形式:
HRESULT CoCreateInstance( [in] REFCLSID rclsid, [in] LPUNKNOWN pUnkOuter, [in] DWORD dwClsContext, [in] REFIID riid, [out] LPVOID *ppv );
第一个参数:待创建组件的CLSID。
第二个参数:用于聚合组件。在d3d中用不上,不用了解,本文不再讨论
第三个参数:dwClsContext的作用是限定所创建的组件的执行上下文。在d3d中用不上,不用了解,本文不再讨论
第四个参数:iid为组件上待使用的接口的iid。
CoCreateInstance 将在最后一个参数中返回此接口的指针。通过将一个IID传给CoCreateInstance,客户将无需在创建组件之后去调用 其QueryInterface函数。
第一个参数就是指定了,你所需要的类的clsid,第四个参数指定了你所需要的接口的iid,但是这时候又有问题了?我怎么知道我需要的clsid和iid是多少啊? chatgpt回答如下,因为本文只涉及到d3d中的com技术,而com技术本身并不需要clsid,仅需要iid,而iid是可以通过msvc关键字__uuidof来获取的
但是! 注意! 对于d3d中的com技术 均有特殊create函数使用,而不是依赖最普通最通用的CoCreateInstance函数,比如D3D12CreateDevice函数,所以我们并不需要去查询clsid,直接使用uuid去获取接口id即可!
到现在我们真正的搞懂了com技术 现在来让我们看看com技术是如何在d3d中运用的吧,看一个小例子:
IUnknown* pDevice;
D3D12CreateDevice(nullptr, D3D_FEATURE_LEVEL_11_0, __uuidof(ID3D12Device), (void**)&pDevice);
ID3D12Device* pDevice1;
pDevice->QueryInterface(__uuidof(ID3D12Device), (void**)&pDevice1);
现在我们就可以拿到pDevice1真正的子类指针去操作了!
恭喜 你现在彻底搞懂了d3d中使用到的com技术!