接口指针的中立形式
《
COM线程模型》中已经说明,接口指针是线程相关,虽然逻辑上指向同一个对象,但不同的线程由于代理对象的原因而实际获得不同的接口指针。但由于逻辑上是同一个对象,因此应该可以有一种接口的中立形式,与线程无关,唯一表示真正的接口指针。
当我们调用CoMarshalInterThreadInterfaceInStream获得一个IStream*,以后只要调用CoGetInterfaceAndReleaseStream就可以从IStream*得到散集出来的接口,这里IStream*就是接口指针的中立形式。但如果真的要保存IStream*这个中立形式,就应该调用CoUnmarshalInterface以防止释放了IStream*。
前面多少显得有点麻烦,COM为此提供了一个对象,叫做全局接口表(Global Interface Table——GIT),其实现IGlobalInterfaceTable接口,以提供对上面所说步骤的封装。其组件类和接口的ID分别为CLSID_StdGlobalInterfaceTable和IID_IGlobalInterfaceTable。
通过调用IGlobalInterfaceTable::RegisterInterfaceInGlobal注册一个接口,并返回一个cookie,在后继的调用中使用这个cookie来表示注册的接口。这里,这个cookie就是接口的中立形式。IGlobalInterfaceTable中的另外两个方法是RevokeInterfaceFromGlobal和GetInterfaceFromGlobal,分别用于注销接口和根据cookie获得正确的接口指针。
下面提供一个使用GIT的样例以说明如何记录接口的中立形式而非直接的接口,在本系列后继的文章中给出的代码里再演示IStream*的使用方式(假设类CABCD实现一个COM组件,ABC和CBA是其实现的某个接口中的方法):
extern IGlobalInterfaceTable *g_pGIT; STDMETHODIMP CABCD::ABC( IABC *pAbc ) { if( !pAbc ) return E_POINTER;
// 做需要的处理,接着保留pAbc以供以后使用,比如回调 // 使用成员变量DWORD m_dwABC来保存IABC*,如下: ASSERT( g_pGIT ); if( FAILED( g_pGIT->RegisterInterfaceInGlobal( pAbc, IID_IABC, &m_dwABC ) ) ) return E_FAIL;
return S_OK; } STDMETHODIMP CABCD::CBA() { if( m_dwABC == static_cast< DWORD >( -1 ) ) return E_FAIL;
// 使用成员变量m_pABC通过g_pGIT获得IABC*,如下: IABC *pAbc = NULL; ASSERT( g_pGIT ); if( FAILED( g_pGIT->GetInterfaceFromGlobal( m_dwABC, IID_IABC, reinterpret_cast< void** >( &pAbc ) ) ) ) return E_FAIL;
// 做需要的处理,然后回调IABC中的某些方法以作类似通知的工作 ASSERT( pAbc ); pAbc->ABCD(); // 通知客户 pAbc->Release();
return S_OK; }
|
回调问题
回调是一种技术,当客户欲反过来,向服务器提供服务时,就使用回调技术,这在Win32 API中很流行,还专门提供一个CALLBACK以标示一个回调函数(主要是指定调用规则)。
回调就是客户写好一个函数,然后将函数指针传给服务器,服务器在适当的时候就通过客户传进来的函数指针调用客户提供的函数以获取客户提供的服务。这其实就是服务器预留一个编程接口(前面的函数指针的原型),以增加其自身的灵活性。
这个技术非常适合通知消息的实现。当服务器完成某个任务后,其调用客户传进来的函数指针,而客户则在那个函数中编写相应的响应代码,结果就表现为客户响应了服务器的通知(任务完成了)。
在COM中,不能在接口方法中传递函数指针,因为函数指针不仅仅是个指针就完了,它还有其附加的函数调用规则、参数类型、返回值类型,不同的语言使用不同的函数调用规则,则可能参数压栈顺序不同,参数提取方式不同,而最后导致失败(当然,可以提供统一的函数调用规则,COM也正是统一使用__stdcall调用规则来解决这个问题的)。并且函数指针是个内存地址,即是进程相关的,在这个进程根据那个进程中的函数指针发起调用结果将是不可预知的,因此接口方法中是不应该传递函数指针的。
但回调技术是如此的有用,所以当要在COM接口间传递函数指针时,应将那个函数的原型定义进一个接口,改为传递接口的指针。
传递接口指针,在适当的时候(比如跨套间时),会自动生成代理对象,这样前面提到的不在同一个进程空间中的问题,就可以通过在服务器进程中加载代理对象,然后通过调用代理对象来实现回调。而由于使用了接口,所以其中的方法也就统一使用了__stdcall调用规则,因此就不存在任何问题。并且COM还提供了一组标准接口以专门实现上述的回调以支持消息通知,此组接口被称为连接点(Connection Point)技术,已成为标准的消息通知实现模型了(虽然COM+提供了另一种事件模型)。
但上面依旧有问题,就是当多线程时,由于传递的是接口,接口是线程相关的,因此需要进行同步处理,故而需要拥有正确的代理对象指针。而在如上面的样例中,由于组件是在MTA套间中运行的,代码可能被多个线程访问,需要保证不同线程调用时,发出通知消息的接口指针都能正确按照套间的规则做应有的动作,因此在组件中使用成员变量记录时不能直接记录接口指针,而必须是接口的中立形式。否则,在向STA线程回调时,可能导致向错误的线程发送消息,并进而导致同步的失败,引起系统的不稳定。
查看本文来源