技术开发 频道

重用托管代码中的Classic COM 组件


【IT168 专稿】

映射C#中的方法参数关键字到IDL的方向属性

    当在C#(例如:out, ref)映射方法参数关键词到它们相应的方向属性(例如:[in], [out], [in,out], [out,retval])时,Interop会使用一些特定的规则。

   1. 当C#中,方法参数没有被关键词认为是合格的,它通常被映射到IDL中的[in]属性,担当按值传递语法。
   2. C#方法中的返回值总是被映射到IDL中的[out, retval]双向属性。
   3. Ref方法参数关键词被映射成IDL的[in,out]双向属性。
   4. Out方法参数关键词被映射成IDL里的[out]双向属性。
   5. 使用方发的返回值,不会返回 .NET世界里发生的错误,但是,相反,这些错误会被作为异常。

     在这里阅读更多的关于错误处理的内容。这里有一些例子,是关于C#参数类型是怎样映射成IDL里的双向属性的。



重用托管代码中的Classic COM 组件

    Interop其中一个优秀的特点就是你可以让你的托管 .NET类型使用继承模式或者内含包容模式,以此来重用功能(此功能由现有的COM组件提供)。这一特点的好处就是:一个使用组件的应用程序永远不会知道托管组件正在内部从Classic COM 组件中平衡非托管代码执行。我们可以研究托管 .NET组件重用现有的COM组件的某些方法。

    ? Mixed-Mode inheritance 混合模式继承
    ? Mixed-Mode containment 混合模式包容

    我们把这个叫做混合模式,因为我们有托管类型重用代码,以及在非托管 COM组件中,早已有可以利用的功能逻辑。


Classic COM中的重用机制


    Classic COM从来没有预定过实作码方案,一直由界面继承执行。COM中,传统的重用机制一直使用了内含包容和汇总。

   只是为了唤醒记忆,内含包容允许你展示外部组件,此外部组件在其内部完全包含内部组件。客户端只能看到外部组件接口。外部组件接口显示的方法通常自己处理工作,或者在必要的时候把工作委派给内部组件。外部组件创建了一个内部组件实例,当它需要平衡某些功能(此功能由内部组件显示)时,外部组件向内部组件发出呼叫。从客户端方面来看,它从不知道这里有一个被外部组件保护的内部组件,而此外部组件确实为外部组件工作。



Classic COM中的重用机制

    Classic COM从来没有预定过实作码方案,一直由界面继承执行。COM中,传统的重用机制一直使用了内含包容和汇总.

通过COM类的容器重用组件

    只是为了唤醒记忆,内含包容允许你展示外部组件,此外部组件在其内部完全包含内部组件。客户端只能看到外部组件接口。外部组件接口显示的方法通常自己处理工作,或者在必要的时候把工作委派给内部组件。外部组件创建了一个内部组件实例,当它需要平衡某些功能(此功能由内部组件显示)时,外部组件向内部组件发出呼叫。从客户端方面来看,它从不知道这里有一个被外部组件保护的内部组件,而此外部组件确实为外部组件工作。

通过COM类的聚集重用组件

    在汇总的情况中,外部对象再也不向内部组件发送呼叫。相反,通过处理内部组件接口指针,它允许客户端直接与内部组件相连。外部组件不再在内部组件接口上中途阻止方法呼叫,因为客户端直接与内部组件相作用。如果没有汇总在一起,内部组件使用一个默认的IUnknown (Non-delegating unknown)执行;如果汇总在一起了,它使用外部组件的IUnknown 执行 (Controlling/Delegating unknown)。这就保证了客户端总是得到外部组件的IUnknown接口指针,而不是内部组件的non-delegating IUnknown接口指针。再一次,以客户端看来,它并不知道这里有一个内部组件。客户端认为内部组件接口只是另一个被外部组件显示的接口。

    接着看一下从.NET类型在现有COM组件中重用非托管代码的不同方法。

通过混合模式继承重用

   在这个重用模式中,可以从非托管 COM类型中扩展或者继承你的托管 .NET类型。除此以外,托管类型可以选择覆盖COM coclass接口里的方法,或者接受基本COM coclass执行。这是一个强有力的模式,你可以在同一类型中混合托管 和非托管执行。


从COM组件的托管类中继承非托管代码

    通过看下面的代码片断,来更好地理解。我们将使用ATL建立一个被称为Flyer的COM 对象,此ATL用两种方法支持一个被称为IFlyer的接口,这两种方法就是TakeOff() 和Fly()。下面是组件的IDL声明。
[ 
....
]
interface IFlyer : IDispatch
{
[id(1), helpstring("method TakeOff")]
HRESULT TakeOff([out,retval] BSTR* bstrTakeOffStatus);
[id(2), helpstring("method Fly")]
HRESULT Fly([out,retval] BSTR* bstrFlightStatus);
};

[
.....
]
coclass Flyer
{
[default] interface IFlyer;
};
    两个方法的执行情况:

STDMETHODIMP CFlyer::TakeOff(BSTR *bstrTakeOffStatus) 
{
*bstrTakeOffStatus =
_bstr_t(_T("CFlyer::TakeOff - This is COM taking off")).copy();
return S_OK;
}

STDMETHODIMP CFlyer::Fly(BSTR *bstrFlyStatus)
{
*bstrFlyStatus =
_bstr_t(_T("CFlyer::Fly - This is COM in the skies")).copy();
return S_OK;
}
    在这个组件被托管代码使用之前,你必须从它的类型库为这个组件生成元数据代理。为了完成这一过程,你需要用到命令行中的以下命令。

tlbimp MyFlyer.tlb /out:MyFlyerMetadata.dll
    现在创建托管类型,托管类型是通过使用普通的继承语法从这个组件继承来的,因此这个组件的功能可以被重用。其中一个托管类型—Bird 是从元数据类型(此元数据类型与Flyer COM对象相对应)继承而来的。这意味着它将继承Flyer COM 组件的所有方法。其它托管 类型—Airplane 用它自己的执行覆盖TakeOff 和 Fly 方法。这里的一个警告就是:在你的托管中,你不能有选择地仅仅覆盖来自于COM 类型的特殊方法。如果你决定覆盖你的托管代码内COM 类型中的单一方法,你也要覆盖其它方法。换句话说,你不能仅仅为TakeOff方法提供一个被覆盖的执行,你不能暗中让托管类型使用来自于COM对象的Fly执行。你必须在托管类型中覆盖Fly方法,为它提供一个实作码。如果想重用COM coclass实作码,你可以从托管类型的Fly执行中调用base.Fly。在编译时不需要选择具体的方法。在执行时找到System.TypeLoadException异常而结束,并且附带有一个错误信息:'Types extending from COM 对象s should override all methods of an interface implemented by the base COM class'。
Collapse 
using System;
using MyFlyerMetadata;

public class Bird : Flyer
{

public class Airplane : Flyer
{

public override String TakeOff() {

return "Airplane::TakeOff - This is .NET taking off";

public override String Fly() {

System.Console.WriteLine(base.Fly());
return "Airplane::Fly - This is .NET in the skies";


public class FlightController
{
public static void Main(String[] args)
{
Bird falcon = new Bird();
System.Console.WriteLine("BIRD: CLEARED TO TAKE OFF");
System.Console.WriteLine(falcon.TakeOff());
System.Console.WriteLine(falcon.Fly());

Airplane skyliner = new Airplane();
System.Console.WriteLine("AIRPLANE: CLEARED TO TAKE OFF");
System.Console.WriteLine(skyliner.TakeOff());
System.Console.WriteLine(skyliner.Fly());

}/* 结束 Main */

}/* 结束FlightController */
   通过使用DOS命令行里的以下命令,你可以编译上面的程序。

csc /target:exe /out:FlightClient.exe /r:MyFlyerMetadata.dll FlightClient.cs
   运行上面的程序,就能给你提供下面的输出。

BIRD: CLEARED TO TAKE OFF 
CFlyer::TakeOff - This is COM taking off
CFlyer::Fly - This is COM in the skies
AIRPLANE: CLEARED TO TAKE OFF
Airplane::TakeOff - This is .NET taking off
CFlyer::Fly - This is COM in the skies
Airplane::Fly - This is .NET in the skies
    使用Bird和 Airplane 托管类型的使用者不需要知道这些类型是通过继承来重用现有的COM组件。如果必要的话,在COM 组件中,托管类型用它自己的执行覆盖所有的方法。这个重用模式,也就是托管代码从非托管代码继承的地方,被称作混合模式继承重用模式。

通过混合模式内含包容重用

    在这个模式中,托管类型使用Classic COM中与内含包容相同的法则。它所作的就是中断元数据代理类型的实例,此元数据类型作为一个成员委托非托管 COM组件。无论什么时候它要求COM组件的服务时,它给组件方法发出请求。


通过Containment/Composition重用非托管 COM代码

    托管类型有能力在调用被包含的COM组件之前或者之后输入自己的代码。这里有一个例子:

Collapse
using System;
using MyFlyerMetadata;

.....

// 非托管 COM 组件
public class HangGlider
{
private Flyer flyer = new Flyer();

// 转发调用
public String TakeOff()
{
return flyer.TakeOff();
}

// 转发调用
public String Fly()
{
System.Console.WriteLine("In HangGlider::Fly - " +
"Before delegating to flyer.Fly");
return flyer.Fly();
}

}/*结束HangGlider类 */

public class FlightController
{
public static void Main(String[] args)
{
....

HangGlider glider = new HangGlider();
System.Console.WriteLine("HANGGLIDER: CLEARED TO TAKEOFF");
System.Console.WriteLine(glider.TakeOff());
System.Console.WriteLine(glider.Fly());

}/* 结束 Main */

}/* 结束 FlightController */


   这就是上面程序的输出:
HANGGLIDER: CLEARED TO TAKEOFF 
CFlyer::TakeOff - This is COM taking off
In HangGlider::Fly - Before delegating to flyer.Fly
CFlyer::Fly - This is COM in the skies
    在上面的例子中,HangGlider类型创建了Flyer COM组件的一个实例,把它作为一个专用成员储存起来。无论什么时候,当方法调用到达时,就要求Flyer组件服务,通过使用被它在前面中断专用实例,它调用组件。而且,在调用被委派到Flyer组件方法之前或者之后,HangGlider类型能自由输入代码。除非在托管类型中覆盖了所有的基本类型,否则混合模式继承重用类型不可能完成这一任务。

从.NET应用程序的角度了解COM线程模式和单元

    当我第一次用COM 编辑程序时不敢冒险使用COM线程模式和apartments ,而且也不知道它们到底是什么。我认为自由地编辑项目是非常好,而且假设它是最好的程序编辑线程模式。但是我几乎没有感知到简单的外表下面发生了什么。由我的MTA对象创建的STA客户端线程时,可能回导致性能终结。而且,因为对象并不是多线程安全,我从不知道当并行线程访问对象时,可能会陷入麻烦。在那个时候,忽视COM 线程模式真的是一种幸福。但是那种幸福只是短暂的,服务器开始出乎意外地崩溃。然后不得不学习COM 线程模式,学习每一个模式是怎样工作的,以及COM 托管单元是怎样工作的,当调用两个不相容的单元时,性能蕴含式会产生什么。在线程调用COM对象之前,通过宣告它是否会输入一个STA或者MTA. STA客户端线程调用CoInitialize(NULL)或者CoInitializeEx(0, COINIT_APARTMENTTHREADED),以此来输入一个STA单元,或者是宣告它是否会输入一个MTA线程调用CoInitializeEx(0, COINIT_MULTITHREADED),以此来输入一个MTA ,以此方式来宣告它自己与单元相联系。相似地,在.NET 托管世界中,你可以允许托管空间里的调用线程宣告它与单元相联系。通过默认,托管 应用程序里的调用线程选择居住在MTA 中。就好像用CoInitializeEx(0, COINIT_MULTITHREADED)初始化了它自己。但是考虑一下开销和性能终结,如果调用一个classic STA COM 组件, 此STA COM 组件被设计成线程单元,就会招致开销和性能终结。不相容的单元将导致额外的代理/存根的开销,很明显,这就是性能终结。通过使用System.Threading.Thread类型里的ApartmentState属性,在.NET应用程序里,你可以为托管线程覆盖单元的默认选择。ApartmentState属性执行下面其中一个枚举 值:MTA, STA以及Unknown. ApartmentState.Unknown相当于默认MTA行为。在调用COM对象之前需要将调用线程指定ApartmentState。一旦COM对象创建了,就不可能改变ApartmentState。因此,尽可能早地在代码中建立线程ApartmentState是非常有意义的。

// 设置用户线程 
Thread.CurrentThread.ApartmentState = ApartmentSTate.STA;

// 创建COM对象
MySTA objSTA = new MySTA();
objSTA.MyMethod()
    作为另一个可以选择的方法,你可以用STAThreadAttribute 或者MTAThreadAttribute标记你的托管客户端主入口点方法,首先以建立完美的线程辅助开始,以此来使用COM组件。例如,看下面的代码片段:

public class HelloThreadingModelApp { 

.....

[STAThread]
static public void Main(String[] args) {

System.Console.WriteLine("The apartment state is: {0}",
Thread.CurrentThread.ApartmentState.ToString());

}/* 结束Main */

}/* 结束class */
   你得到的输出可能是这样的:

The apartment state is: STA
    如果创建了MTAThread属性,然后在MTA上建立ApartmentState。如果客户端主入口点中没有指定线程状态属性,或者如果从被创建的COM组件中,没有为线程创建ApartmentState属性,然后就不会知道ApartmentState,ApartmentState 不执行MTA行为。
0
相关文章