COM编程攻略(一 传统C++编程局限性)

发布于 2020-04-13  143 次阅读


众所周知,Windows所支持的动态链接(dll),对程序提供了极大的便利。dll把程序分成若干个模块,某个模块更新的时候,只需要更新对应的dll即可。编译器按照一定调用约定,调用dll中的函数,并返回值。函数、成员的地址,是通过编译时的环境所决定的。例如:

struct Foo
{
  int bar;
} foo;

对于Foo结构,它的大小是4(int按4个字节来算)。Foo和它的下一个变量有可能在内存中紧密地排列在一起,那么,对于一个Foo数组,取下一个Foo的代码可能就是:

lea rax, [foo]
add rax, 4

以上只是举一个例子,说明代码的二进制结构,在编译时已经决定。加入某一个可执行文件exe,它加载了一个dll,它们则会按照上面的约定进行调用,即:通过地址+4来获取下一个Foo。

此时,假设dll进行了更新,Foo结构中新增了一个变量:

struct Foo
{
  int bar;
  int foobar;
} foo;

在dll中,Foo的大小变成了8个字节,在其内部可正常运作,但是可执行文件并不知道这个dll的布局发生了变化,仍然按照4个字节的大小来访问Foo,那么foo[1].a,其实访问到的是dll中的foo[0].b,这样会造成非常严重的后果。

之所以造成这样的原因,是因为dll模块的Foo结构,直接暴露给了应用程序,紧接着Foo结构改变,造成了二进制层面的不兼容。为了解决这个问题,我们必须要想出一种方法,使得暴露给客户的代码,在二进制层面上不会再改变。

下面来看另外一种情况。假设某个dll是以二进制的形式分发,而不是以源文件的形式分发,那么二进制的内容与dll厂商所使用的编译器有密切的关系。举个例子:

厂商提供的dll中有这样一个类:


struct A { }; struct B { }; struct C : public A, public B { }; A* create();

客户代码(exe):

A* a = create(); // 假设dll的create一定会返回一个C的实例
C* c = static_cast<C*>(a);

上述代码,正常运行建立在了这样一个事实上:双方编译器,对于多重继承的对象模型是一致的。也就是说,static_cast中的运行结果,必须要兼容于厂商所使用的编译器。然而,很多情况下,编译器可以选择自己的实现方式,只要能达到static_cast的效果就可以了,并不能保证各个编译器下的对象模型都是一致的,类似的还有dynamic_cast,规范中并没有对实现进行约束,那么出现跨越调用边界(exe和dll相互调用)的情况,由于编译器的差异,也有可能会出现问题。

C语言之所以不会出现这样的问题,是因为它的模型简单得多——清晰的调用约定,清晰的函数名匹配方式。C++则会复杂很多,如构造函数、析构函数、虚基类、虚表,C++规范只在行为上进行了约束,并没有在实现上或二进制上进行约束。为了能跨越编译器起到一致的效果,我们必须要隔离编译器相关的实现部分,不得出现跨边界的编译器相关行为。为了解决这个问题,我们可以简单地在A里面暴露一个方法:

//.h
struct C;
struct A
{
  C* cast();
}

//.cpp
C* A::cast()
{
  return static_cast<C*>(this);
}

由于cast函数是在dll中实现,因此static_cast这段汇编代码一定是和dll兼容的,那么exe中调用C* c = a->cast();不会有任何问题。

基于以上2个原因(当然还有更多原因),微软在1993年发布了COM模型,之后又发布了一系列支持的接口(OLE, ActiveX, ATL等)。通过COM模型,我们可以编写出可分发的、跨编译器的、二进制兼容的C++组件。

欢迎关注我的小程序,小程序内容与网站自动保持同步

欢迎关注我的微信公众号,本网站所有的文章以及更新以后都会手动同步到微信公众号上。


公交车司机终于在众人的指责中将座位让给了老太太