C语言oo1cpp怎么进,用C实现的一个基本COM接口IFoo(来自COM Programmer's Cookbook)——C实现COM接口系列1...

用C实现的一个基本COM接口IFoo(来自COM Programmer's Cookbook)

把该文中实现的代码整理汇总到一个项目中。目前只是实现到一个中间阶段,重点在说明COM接口的实现原理,还没有包含类厂的部分。以后还需陆续添加类厂等高级功能。

文件组成:

ifoo.h         COM接口IFoo,接口ID IID_IFoo 声明文件。

outside.c      COM接口实现。这里实现IFoo的是一个结构体COutside.

util.h         一些宏定义、全局函数、变量声明文件。

main.c         笔者为实现项目添加的文件。提供main函数、内存管理函数Alloc,Free的实现(封装C运行库函数malloc和free.)、接口ID定义。

COM接口到底是什么?

COM接口是一个指向虚函数表的指针。通过这个指针可以访问内存中某处的各个功能块,执行预定义的功能,完成用户的任务。这些功能块以函数的形式存在(想不出还有其他形式:))并被调用。它们有一个共同点:都包含一个指针参数,指向这些功能要操作的数据地址。在C++中,这个地址就是对象的首地址,也就是类成员函数中隐含的this指针。在C函数中并没有这种现成的便利,因此代码实现中在接口定义时仍使用了接口指针(HRESULT (__stdcall * QueryInterface)   (IFoo * This,  const IID * const, void **)),而在接口函数实现时根据结构体布局结构,从这个接口指针推算得到对象实例指针。

typedef struct IFoo

{

struct IFooVtbl * lpVtbl;

} IFoo;

typedef struct IFooVtbl IFooVtbl;

struct IFooVtbl

{

HRESULT (__stdcall * QueryInterface)   (IFoo * This,  const IID * const, void **) ;

ULONG (__stdcall * AddRef)    (IFoo * This) ;

ULONG (__stdcall * Release)   (IFoo * This) ;

HRESULT (__stdcall * SetValue)         (IFoo * This,  int) ;

HRESULT (__stdcall * GetValue)         (IFoo * This,  int *) ;

};

COM接口的要求:

每一个COM接口(指向的虚函数表)的头三个函数必须是IUnknown接口的函数:QueryInterface,AddRef和Release。在C++中,称为从IUnknown接口继承。

对于调用QueryInterface响应查询IID_IUnknwon得到的接口指针值,同一个对象实现的所有接口必须相同。这是判断两个COM对象是否是同一个对象的标准。

宏定义“#define IUNK_VTABLE_OF(x) ((IUnknownVtbl *)((x)->lpVtbl))“说明

在预处理输出文件main.i中可以找到IUnknownVtbl和IFooVtbl的声明:

typedef struct IUnknownVtbl

{

HRESULT ( __stdcall *QueryInterface )(

IUnknown * This,

const IID * const riid,

void **ppvObject);

ULONG ( __stdcall *AddRef )(

IUnknown * This);

ULONG ( __stdcall *Release )(

IUnknown * This);

} IUnknownVtbl;

struct IUnknown

{

struct IUnknownVtbl *lpVtbl;

};

struct IFooVtbl

{

HRESULT (__stdcall * QueryInterface)   (IFoo * This,  const IID * const, void **) ;

ULONG (__stdcall * AddRef)    (IFoo * This) ;

ULONG (__stdcall * Release)   (IFoo * This) ;

HRESULT (__stdcall * SetValue)         (IFoo * This,  int) ;

HRESULT (__stdcall * GetValue)         (IFoo * This,  int *) ;

};

该宏定义的作用就是把IFoo接口中的IFooVtbl类型的指针拿出来((x)->lpVtbl)),并强制转换((IUnknownVtbl *))成IUnknownVtbl。

“强制转换”的结果是什么呢?是怎么做到的呢?

很明显,结果就是得到的指针不再是IFooVtbl *类型,而是变成了IUnknownVtbl *类型。至于做法,系统应该记录每一个变量、表达式的类型。当进行强制类型转换时,就(临时地)修改其类型为转换到的类型。

同理,QueryInterface, AddRef, Release宏定义中的(IUnknown *)也是这种用法。

可以看到,宏“IUNK_VTABLE_OF“的作用是供宏QueryInterface,宏AddRef,宏Release引用,把IFooVtbl *类型转换为IUnknownVtbl *类型,最终达到调用IUnknownVtbl中定义的三个QueryInterface,AddRef,Release函数。

那么,这种大费周章的目的是什么呢?为什么不以IFooVtbl中三个函数的定义形式(不通过强制转换来转换成必须的类型),直接调用IFooVtbl中定义的函数呢?虽然强制转换在参数值上并不会造成改变,最终调用的也是IFooVtbl定义的函数(FooQueryInterface,FooAddRef,FooRelease)。

为什么一定要通过IUnknown接口指针调用这三个函数呢?修改QueryInterface宏定义如下:

#define QueryInterface(pif, iid, pintf) \

(((pif)->lpVtbl)->QueryInterface(pif, iid, (void **)(pintf)))

即通过IFoo接口指针来调用由IUnknown引入的函数,有什么不对的地方吗?

试验表明,将QueryInterface宏定义如下也可以编译通过,执行起来也没有出现任何异常。

#define QueryInterface(pif, iid, pintf) \

(((pif)->lpVtbl)->QueryInterface(pif, iid, (void **)(pintf)))

对于IUnknown接口的三个函数,调用时传递的参数是IUnknown *类型(见QueryInterface, AddRef, Release宏定义),而函数定义中(FooQueryInterface, FooAddRef, FooRelease)声明的参数是IFoo *类型,这种不一致的情况是怎么出现的?这种不一致不会有问题吗?

这种不一致的产生是由于从不同的角度看待引起的。如果从IUnknown接口来看,那么接口函数中的第一个参数类型就是IUnknown *;如果从IFoo来看,那么第一个参数的类型就是IFoo *。

这种不一致性只是针对于编译器对于类型的编译要求有意义的,在接口实现及使用时,传递给lpVtbl->QueryInterface, lpVtbl->AddRef,lpVtbl->Release的第一个参数在值上都是相同的,都是实现该接口的内存地址(在本例中是COutside对象的首地址)。

一些语法现象回顾

函数指针变量定义、赋值及调用。

HRESULT (__stdcall * pQI)   (IFoo * This,  const IID * const, void **) ;

定义一个函数指针变量pQI,该变量指向“返回HRESULT,取3个参数分别为类型IFoo *,const IID * const, void **”的函数。

typedef HRESULT (__stdcall * QIType)   (IFoo * This,  const IID * const, void **) ;

定义一个函数指针类型,该类型的指针指向“返回HRESULT,取3个参数分别为类型IFoo *,const IID * const, void **”的函数。

HRESULT __stdcall QueryInterface(IFoo * This,  const IID * const, void **) ;//函数声明示例

pQI = 0;   // 函数指针赋值,0表示不指向任何函数。

pQI = QueryInterface;  // 函数指针赋值,pQI指向QueryInterface。

pQI = &QueryInterface; // 与上面等价。

QueryInterface(&this->ifoo, riid, ppv);  // 使用函数名直接调用

pQI(&this->ifoo, riid, ppv);             // 函数指针调用

(*pQI)(&this->ifoo, riid, ppv);          // 第二种函数指针调用方式

宏定义、展开规则

对于宏,一直有一种雾里看花的感觉,似乎很随意,怎么来都行,比如:

#define AddRef(pif) \

(IUNK_VTABLE_OF(pif)->AddRef((IUnknown *)(pif)))

宏定义应该是可以嵌套的,即宏定义的“内容“中还可以包含(嵌套)宏,如本例,“IUNK_VTABLE_OF”就是嵌套宏。在展开的时候,将嵌套的宏也一并展开(替换成定义的内容),直到不再有宏为止。

那么就有两个疑问:

1。如果被嵌套的宏包含(直接或间接)定义的宏,那么展开就没完没了,死循环了。

2。如果定义的内容中有跟定义的宏同名的字符串(比如上面的例子IUNK_VTABLE_OF),那么怎么区分这同名的东东是嵌套的宏(需要展开),还是一般的字符串(不需要展开)?

函数调用规范约定、main函数调用规范。

一开始把几个文件汇总到项目里时,编译通不过,错误提示大致意思是,不能把一种调用规范的函数指针转换成另一种调用规范的函数指针。后来把调用规范改为   /Gz(__stdcall),编译为(Compile As)改为/TC(Compile As C Code)就好了。

想来是对于.c文件,编译器缺省使用的是__cdecl,而IFoo中的接口宏定义在win32下展开成了__stdcall,所以出现了矛盾。而使用/Gz强制未声明调用规范的函数使用__stdcall,实现就与声明一致了。

(size_t)&(((s *)0)->m)

c++程序员也许都知道,访问地址“0”处的成员是一大忌,会造成GP。然而,取地址“0”处的成员的地址,却是个合法的操作。虽然地址“0”处并没有什么内容,但是,如果在地址0处存放一个内容,那么该内容中的成员也是有地址的。本例中正是巧妙地利用这种方法,从接口地址计算得出实现该接口的实例地址,进而访问实例的内部变量。

------------------------------------------------------------------------------------

2009年5月6日

附上源码:/Files/gracelee/outside.zip

代码执行结果:

17fe751691d999743ec63f46ea8873c6.png

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值