Interoperation com and .net
1.在.NET程序中使用经典的com组件
先写一个atl的com组件,这是idl
interface IAirlineInfo : IDispatch
{
[id(1), helpstring("method GetAirlineTiming")]
HRESULT GetAirlineTiming([in] BSTR bstrAirline,
[out,retval] BSTR* pBstrDetails);
[propget, id(2), helpstring("property LocalTimeAtOrlando")]
HRESULT LocalTimeAtOrlando([out, retval] BSTR *pVal);
};
下面是实现部分
CAirlineInfo::GetAirlineTiming(BSTR bstrAirline, BSTR *pBstrDetails)
{
_bstr_t bstrQueryAirline(bstrAirline);
if(NULL == pBstrDetails) return E_POINTER;
if(_bstr_t("Air Scooby IC 5678") == bstrQueryAirline)
{
// Return the timing for this Airline
*pBstrDetails = _bstr_t(_T("16:45:00 - Will arrive at Terminal 3")).copy();
}
else
{
// Return an error message if the Airline was not found
return Error(LPCTSTR(_T("Airline Timings not available for this Airline" )),
__uuidof(AirlineInfo), AIRLINE_NOT_FOUND);
}
return S_OK;
}
使用一个叫做TLBIMP.exe的工具来通过上面组件的type library 生成相应的metadata wrapper
TLBIMP AirlineInformation.tlb /out:AirlineMetadata.dll
可以使用ildasm来查看元数据信息,我们可以看到数据类型(参数或是返回值)都已经被转换成了
对等的.net部分。
在上面的例子中,bstr已经被换成了string ,用[out ,retval]标记的参数已经被直接转换成了实际的
函数返回值,而原来的返回值HRESULT已经被作为.net exception抛出
下面就来想办法用.net来调用这个com组件
直接使用AirlineMetadata.dll 这个元数据程序集直接操作com
String strAirline = "Air Scooby IC 5678";
String strFoodJunkieAirline = "Air Jughead TX 1234";
try
{
AirlineInfo objAirlineInfo;
objAirlineInfo = new AirlineInfo();
// Call the GetAirlineTiming() method
System.Console.WriteLine("Details for Airline {0} --> {1}",
strAirline,
objAirlineInfo.GetAirlineTiming(strAirline));
// This should make the COM object throw us the
// AIRLINE_NOT_FOUND error as a COMException
System.Console.WriteLine("Details for Airline {0} --> {1}",
strFoodJunkieAirline,
objAirlineInfo.GetAirlineTiming(
strFoodJunkieAirline));
}
catch(COMException e)
{
System.Console.WriteLine("Oops an error occured !. Error Code is : {0}.
Error message is : {1}",e.ErrorCode,e.Message);
}
直接new就可以了,真的是很神奇的。
不过这些工作都是RCW偷偷的为我们做的,它将.net调用转换成com调用,同时负责其中
数据类型的转换,最后将返回值转换和.net的形式,将HRESULT转换成为Exception抛出
那么经典的QueryInterface到那里去了,我们应该怎么来用它,
在.net中QueryInterface的调用直接有简单的cast来代替了,你只需要用相应的接口来
强制转换就可以了,如果不成功,就会返回InvalidCastException,相当的简单。
其中复杂的工作已经由rcw做了,我们其实只需要考虑程序的逻辑就可以了。
如果绝对这样直接转换太暴力了(确实,直接通过异常来判断总是不好的),那么可以通过
“is”操作符来判断一下类型,确认了在安全的转换。还可以通过“as”操作符来做转换,
转换不成功是不会抛出异常的,只会返回一个null.
那么如何来实现COM的后期绑定呢?
刚才我们在上面看到的例子都是使用metadata代理来前期绑定.net客户端到COM对象上面。
但是如果我们一定要做后期绑定怎么办?
使用reflection就可以了。这样不仅可以应用到com对象中,同时.net对象也可以这样来做
后期的绑定和加载。
如果你的com只实现了一个纯的dispinterface,那么你就只能使用reflection来生成你的对象
和调用你的函数了。
做后期绑定,你首先要知道对象的ProgID或是CLSID,
由于reflection 和核心函数CreateInstance 需要一个 Type信息来做为参数,而我们有的只是
简单的GUID信息,那么需要通过GetTypeFromProgID 或是 Type.GetTypeFromCLSID来做相应的转换
而调用方法,我们要使用InvokeMember。
.......
try
{
object objAirlineLateBound;
Type objTypeAirline;
// Create an object array containing
// the input parameters for the method
object[] arrayInputParams= { "Air Scooby IC 5678" };
//Get the type information from the progid
objTypeAirline =
Type.GetTypeFromProgID("AirlineInformation.AirlineInfo");
// Here's how you use the COM CLSID
// to get the associated .NET System.Type
// objTypeAirline = Type.GetTypeFromCLSID(new Guid(
"{F29EAEEE-D445-403B-B89E-C8C502B115D8}"));
// Create an instance of the object
objAirlineLateBound = Activator.CreateInstance(objTypeAirline);
// Invoke the 'GetAirlineTiming' method
String str = (String)objTypeAirline.InvokeMember("GetAirlineTiming",
BindingFlags.Default |
BindingFlags.InvokeMethod,
null,
objAirlineLateBound,
arrayInputParams);
System.Console.WriteLine("Late Bound Call" +
" - Air Scooby Arrives at : {0}",str);
// Get the value of the 'LocalTimeAtOrlando' property
String strTime = (String)objTypeAirline.InvokeMember("LocalTimeAtOrlando",
BindingFlags.Default |
BindingFlags.GetProperty,
null,
objAirlineLateBound,
new object[]{});
Console.WriteLine ("Late Bound Call - Local" +
" Time at Orlando,Florida is: {0}",
strTime);
}/* end try */
catch(COMException e)
{
System.Console.WriteLine("Error code : {0}, Error message : {1}",
e.ErrorCode, e.Message);
}/* end catch */
.......
上面基本是reflection的东西,真正和com有关的,恐怕就是那两个转换的函数。
接着是比较复杂的部分,com 的事件回调机制
一个com 对象如果要是要支持outgoing接口,那么它一定要实现IConnectionPointContainer
接口,客户端通过QI这个IConnectionPointContainer来检查它是否支持相应的outgoing接口
如果QI失败了,那么这个对象就根本不支持事件。如果成功了,那么可以通过FindConnectionPoint
来检查是否支持指定的接口。如果支持,那么就会返回对应的连接点。然后将sink对象的IUnknown
接口传给IConnectionPoint::advise,那么com就会将它加入一个map中,并在需要的时候触发这个
事件。同时它会返回一个cookie给客户端,这个cookie可以在你不像响应事件的时候通过IConnectionPoint::UnAdvise
来取消。
下面建立一个带连接点的atl com
interface IAirlineArrivalPager : IDispatch
{
[id(1), helpstring("method AddArrivalDetails")]
HRESULT AddArrivalDetails([in] BSTR bstrAirlineName,
[in] BSTR bstrArrivalTerminal);
};
....
dispinterface _IAirlineArrivalPagerEvents
{
properties:
methods:
[id(1), helpstring("method OnAirlineArrivedEvent")]
HRESULT OnAirlineArrivedEvent([in] BSTR bstrAirlineName,
[in] BSTR bstrArrivalTerminal);
};
....
coclass AirlineArrivalPager
{
[default] interface IAirlineArrivalPager;
[default, source] dispinterface _IAirlineArrivalPagerEvents;
};
注意第2个接口 声明的是 source的
.......
STDMETHODIMP CAirlineArrivalPager::AddArrivalDetails(
BSTR bstrAirlineName,BSTR bstrArrivalTerminal)
{
// Notify all subscribers that an Airline has hit the tarmac
Fire_OnAirlineArrivedEvent(bstrAirlineName,bstrArrivalTerminal);
// Return the status to the caller
return S_OK;
}
.......
这个是第一个接口的实现。
其中Fire_OnAirlineArrivedEvent就是那个触发函数,它遍历连接点的map,并调用其中
注册的sink的OnAirlineArrivedEvent函数,而它是帮助代理类通过继承IConnectionPointImpl
由atl自动生成的
如果是一个c++程序员,要做的就是通过一个AirlineArrivalPager对象获取连接点,然后通过advise
来将自己的sink的IUnknown接口传入连接点中。
过程对应.net程序员的确是复杂了。
下面就说说简单的,^_^
delegates
事件handle是基于Delegate
// Here's the SayGoodMorning delegate
delegate string SayGoodMorning();
public class HelloWorld
{
public string SpeakEnglish() {
return "Good Morning";
}
public string SpeakFrench() {
return "Bonjour";
}
public static void Main(String[] args) {
HelloWorld obj = new HelloWorld();
// Associate the delegate with a method reference
SayGoodMorning english = new SayGoodMorning(obj.SpeakEnglish);
SayGoodMorning french = new SayGoodMorning(obj.SpeakFrench);
// Invoke the delegate
System.Console.WriteLine(english());
System.Console.WriteLine(french());
}
}/* end class */
上面是典型的delegate的使用,典型的命令模式
如果一个com支持连接点,那么用tlbimp生成的metadata代理将包含相应的信息
void subscribePaging() {
// Create an AirlineArrivalPager object
m_pager = new AirlineArrivalPager();
// Add the delegate instance that references
// the OnMyPagerNotify method
// to the OnAirlineArrivedEvent event list (ICP::Advise)
m_pager.OnAirlineArrivedEvent +=
new _IAirlineArrivalPagerEvents_OnAirlineArrivedEventEventHandler(
OnMyPagerNotify);
}/* end subscribePaging */
订阅连接点的工作在这里只需要简单的把_IAirlineArrivalPagerEvents_OnAirlineArrivedEventEventHandler的代理
实例加入OnAirlineArrivedEvent event 表单。
OnAirlineArrivedEvent和com中的outgoing接口中的方法是一样的名字,代理名称_IAirlineArrivalPagerEvents_OnAirlineArrivedEventEventHandler
通常它会以这样的模式存在InterfaceName_EventNameEventHandler,
这就是要注册一个sink所要做的全部事情,你需要做的只是生成一个组件对象,并加一个delegate 引用到要调用的函数。
如果想注销这个事件,那么简单的remove引用就可以了。
在.NET中使用com collections
经典COM的重用机制
标准的做法是包含和聚合
a.混合模式的继承
在这种重用模式,你可以让你的.net 类扩展或是继承你的unmanaged COM
另外,这个managed类有overriding这个COM 接口或是接受COM实现的权利。
这是一个非常强大的模型,你可以在一个class中混合使用托管和非托管实现
下面是一个ATL COM
[
....
]
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的方式来生成代理,供托管代码使用
我们可以通过常规的继承机制来通过继承的方式实现托管类,
using System;
using MyFlyerMetadata;
// Inherit from the metadata type representing
// the unmanaged COM component
// Use the COM Component's implementation
// of TakeOff & Fly (Use the base class' implementation)
public class Bird : Flyer
{
}/* end class Bird */
// Inherit from the metadata type representing
// the unmanaged COM component
// Override the COM object's method implementations in our
// derived managed-class.
// (Also call base class implementation when
// necessary using base.MethodName())
public class Airplane : Flyer
{
// Override the COM Component's Flyer::TakeOff implementation
// with our own implementation
public override String TakeOff() {
return "Airplane::TakeOff - This is .NET taking off";
}/* end TakeOff */
// Override the COM Component's Flyer::Fly implementation
// with our own implementation
public override String Fly() {
// Can call the base class' implementation too if you
// wish to.
System.Console.WriteLine(base.Fly());
return "Airplane::Fly - This is .NET in the skies";
}/* end Fly */
}/* end class Airplane */
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());
}/* end Main */
}/* end FlightController */
有一点必须声明,在托管代码中使用非托管com中的类,要重载它的方法,必须
同时重载它所有的方法,不能只重载一个。
这个就是混合模式的继承
混合模式的包含
在这个托管类中使用了和经典COM的包含机制。
它就是把一个metadata 代理类作为一个成员。
如果它需要这个com组件的服务的时候,它就直接把请求发送给这个组件的方法
这个托管类需要做的就是在调用这个包含的com组件方法的前后加入自己的代码。
using System;
using MyFlyerMetadata;
.....
// Contains an instance of the metadata type representing
// the unmanaged COM Component
public class HangGlider
{
private Flyer flyer = new Flyer();
// Forward the call to the contained class' implementation
public String TakeOff()
{
return flyer.TakeOff();
}
// Forward the call to the contained class' implementation
public String Fly()
{
// Do what you need to do before or after fowarding the
// call to flyer
System.Console.WriteLine("In HangGlider::Fly - " +
"Before delegating to flyer.Fly");
return flyer.Fly();
}
}/* end class 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());
}/* end Main */
}/* end FlightController */
在.NET 应用程序中理解com的线程模型和套间
// Set the client thread's ApartmentState to enter an STA
Thread.CurrentThread.ApartmentState = ApartmentSTate.STA;
// Create our COM object through the Interop
MySTA objSTA = new MySTA();
objSTA.MyMethod()
作为另外一种做法,你可以在你的入口点函数中直接声明套间类型。
public class HelloThreadingModelApp {
.....
[STAThread]
static public void Main(String[] args) {
System.Console.WriteLine("The apartment state is: {0}",
Thread.CurrentThread.ApartmentState.ToString());
}/* end Main */
}/* end class */
如果不设置,默认的套间类型为MTA
2.在COM中调用.NET 组件
通过com interop和通过framework的工具,可以让com和.net组件交互,就像com和com交互一样
首先只有public 的类才能被com的客户度调用。
using System;
using System.Windows.Forms;
using System.Runtime.InteropServices;
public enum WeatherIndications
{
Sunny = 0,
Cloudy,
Rainy,
Snowy
}
[ClassInterface(ClassInterfaceType.AutoDual)]
public class TemperatureComponent
{
private float m_fTemperature = 0;
// Public Constructor
public TemperatureComponent()
{
m_fTemperature = 30.0f;
}
//Public Property Accessors (Defines get/set methods)
public float Temperature
{
get { return m_fTemperature; }
set { m_fTemperature = value;}
}/* end Temperature get/set property */
// Public Method that displays the Current Temperature
public void DisplayCurrentTemperature()
{
String strTemp = String.Format("The current " +
"temperature at Marlinspike is : " +
"{0:####} degrees fahrenheit",
m_fTemperature);
MessageBox.Show(strTemp,"Today's Temperature");
}/* end DisplayCurrentTemperature */
// Another public method that returns an enumerated type
public WeatherIndications GetWeatherIndications()
{
if(m_fTemperature > 70) {
return WeatherIndications.Sunny;
}
else {
// Let's keep this simple and just return Cloudy
return WeatherIndications.Cloudy;
}
}/* end GetWeatherIndications */
}/* end class Temperature */
注意,类有个标签 ClassInterface并且被设置为ClassInterfaceType.AutoDual。
这个可以看作一个将.net组件的public 成员导出成为一个默认的类接口
使用interface来导出类接口不是推荐的做法
下面就生成类型库和注册这个程序集
我们生成的.net assembly 对于COM客户端是不认识的,我们需要把它转换成更加友好的类型
这里我们使用tlbexp或是regasm.exe工具,对于regasm工具,它不仅可以做转换工作,同时它还可以用
来完成注册工作,
转换
regasm Temperature.dll /tlb:Temperature.tlb
这个命令同时完成了注册的工作
下面是在vb 6.0中 使用这个客户端
这里创建组件的工作和一般的com的创建过程是一样的
你可以早期绑定这个控件,也可以通过createobject来实现晚期绑定
Private Sub MyButton_Click()
On Error GoTo ErrHandler
' Create an instance of the temperature component
Dim objTemperature As New TemperatureComponent
' Display the current temperature
objTemperature.DisplayCurrentTemperature
' Set the temperature property
objTemperature.Temperature = 52.7
' Display the current temperature after property mutation
objTemperature.DisplayCurrentTemperature
' Check the weather indications
If (objTemperature.GetWeatherIndications() = _
WeatherIndications_Sunny) Then
MsgBox "Off to the beach"
Else
MsgBox "Stay at home and watch Godzilla on TV"
End If
Exit Sub
ErrHandler:
MsgBox "Error Message : " & Err.Description, _
vbOKOnly, "Error Code " & CStr(Err.Number)
End Sub
在注册表中 HKCR/CLSID/{...Component's CLSID...}中InprocServer32中标明的
dll路径指向的是mscoree.dll,这个是.NET runtime,在这个目录下还有class,assembly和
RuntimeVersion, com runtime在调用的时候,它先会调用mscoree.dll的DllGetClassObject入口点
函数,这个runtime使用传入给dllgetclassobject的classID来在assemble和class keys(在Inprocserver32)中加载
这个程序集,让这个程序集来为这个请求服务。runtimes为每个.net 组件创建一个CCW。这个对象将会是
调用透明化。
如果没有标记 ClassInterface 属性,一个默认的类接口同样会生成,而在这种情况下,基于idispatch 的接口
是不会包含任何关于方法类型信息和DISPIDs,这种类型的类接口不能使用晚期绑定的客户端。