運作環境:Visual Studio.NET Beta2, VC7, C#
參考資料:MSDN
級别:入門級
一、前言
COM元件對象與.NET類對象是完全不同的,但為了使COM客戶程式象調用COM元件一樣調用.NET對象,使.NET程式
象使用.NET對象一樣使用COM元件,MS使用了wrapper技術。本文詳細介紹了兩種不同的wrapper技術,并給出了
簡單的代碼執行個體。
二、COM wrapper簡介
傳統的COM對象與.NET架構對象模型有以下幾點不同:
(1)、COM對象的客戶必須自己管理COM對象的生存期,而.NET對象的生存期由CLR(Common Language Runtime)來管
理,即通過GC(Garbage Collection)機制自動回收。
(2)、COM對象的客戶通過調用QueryInterface查詢COM對象是否支援某個接口并得到其接口指針,而.NET對象的客
戶使用Reflection(System.Reflection.*)來獲得對象功能的描述,包括方法屬性等。
(3)、COM對象的客戶通過指針引用COM對象,對象在記憶體中的位置是不變的,而.NET對象在記憶體中的駐留由.NET框
架執行環境(execution environment)來管理,對象在記憶體中的位置是可變的,比如出于優化性能的考慮,同時
會更新所有對對象的引用。這一點也是以CLR中不使用指針為前提的。
為了實作傳統的COM程式與.NET程式之間的互相調用,.NET提供了包裝類RCW(Runtime Callable Wrapper)和
CCW(COM Callable Wrapper)。每當一個.NET客戶程式調用一個COM對象的方法時就會建立一個RCW對象,每當一個
COM客戶程式調用一個.NET對象的方法時就會建立一個CCW對象。
具體示意圖如圖1所示:
圖1 COM wrapper overview
三、.NET中調用COM元件
1、RCW(Runtime Callable Wrapper)簡介
其示意圖如圖2所示:
圖2 Accessing COM objects through the runtime callable wrapper
RCW的主要功能:
(1)RCW實際上是runtime生成的一個.NET類,它包裝了COM元件的方法,并内部實作對COM元件的調用。
(2)列集(marshal).NET客戶與COM對象之間的調用,列集的對象包括方法的參數傳回值等,比如C#中的string與
COM中的BSTR之間的轉換。
(3)CLR為每個COM對象建立一個RCW,與對象上的引用數無關,就是說每個COM對象有且隻會有一個RCW對象。
(4)RCW中包含了COM對象的接口指針,并管理COM對象的引用計數。RCW自身的釋放通過gc機制管理。
2、執行個體示範
(1)使用VC7/ATL建立一個最簡單的COM對象。元件類名叫AtlComServer,實作的接口名叫IAtlComServer,庫名叫
AtlServer。添加一屬性Name,并實作Get/Set函數。其idl如下所示:
import "oaidl.idl";
import "ocidl.idl";
[
object,
uuid(77506E08-D9FB-4F45-85E0-376F5187AF21),
dual,
nonextensible,
helpstring("IAtlComServer Interface"),
pointer_default(unique)
]
interface IAtlComServer : IDispatch{
[propget, id(1), helpstring("property Name")] HRESULT Name([out, retval] BSTR* pVal);
[propput, id(1), helpstring("property Name")] HRESULT Name([in] BSTR newVal);
};
[
uuid(9136EEE6-ECEE-4237-90B6-C38275EF2D82),
version(1.0),
helpstring("AtlServer 1.0 Type Library")
]
library AtlServerLib
{
importlib("stdole2.tlb");
[
uuid(0E733E15-2349-4868-8F86-A2B7FF509493),
helpstring("AtlComServer Class")
]
coclass AtlComServer
{
[default] interface IAtlComServer;
};
};
(2)建立一個最簡單的C# Console程式。執行菜單Project/Add Reference指令,在COM屬性頁中選中剛才建立的
AtlServer 1.0 Type Library并添加,系統會提示是否添加一個wrapper,選擇'是',然後會自動在C#程式的
bin目錄下生成一個檔案Interop.AtlServerLib_1_0.dll,這個就是AtlServer的RCW。另外使用指令行指令
tlbimp atlserver.tlb有同樣的效果。
(3)在程式中添加調用AltServer的代碼,如下所示:
using System;
using AtlServerLib; //通過namespace來引用庫,在wrapper(即Interop.AtlServerLib_1_0.dll)中定義
namespace CSharpClient
{
class Class1
{
static void Main(string[] args)
{
AtlComServer server = new AtlComServer();
server.Name = "Chang Ming";
Console.WriteLine("Hello, My Names is " + server.Name);
}
}
}
從上面可以看到,AtlServerLib.AtlComServer就代表了COM元件AtlComServer。在傳統的COM客戶中通過接口
IAtlComServer來調用,而在.NET中隻是把它當作了一個普通的.NET類。因為實際上調用的是wrapper中的類,
而不是真正的COM對象。
下載下傳RCW的示例代碼(23KB)
四、COM程式中調用.NET對象
1、CCW(COM Callable Wrapper)簡介
其示意圖如圖3所示:
圖3 Accessing .NET objects through COM callable wrapper
CCW的主要功能:
(1)CCW實際上是runtime生成的一個COM元件,它在系統資料庫注冊,有CLSID和IID,實作了接口,内部包含了對
.NET對象的調用。
(2)列集(marshal).NET對象與COM客戶之間的調用。
(3)每個.NET對象隻有一個CCW,多個COM客戶調用同一個CCW。
(4)COM客戶以指針的方式調用CCW,是以CCW配置設定在non-collected堆上,不受runtime管理。而.NET對象則配置設定
在garbage-collected堆上,受runtime管理,享受CLR的種種好處。
(5)CCW實際上是COM元件,是以它遵循引用計數規則。當它的引用計數為0時,會釋放它對它管理的.NET對象的
引用,并釋放自己的記憶體空間。當.NET對象上引用計數為0時,則會被GC回收。
.NET中受控類型(Manages types)如class、interface、struct和enum都可以無縫的與COM類型相結合,但是要
遵循以下規則:
(1)受控類型必須是public型。隻有public型的類型才會被輸出到類型庫中。
(2)隻有public型的methods、properties、fields和events才會被輸出到類型庫中,才會被COM客戶看見。
(3)受控類型必須有一個公用的預設構造函數。這是因為COM元件要求必須有預設構造函數。
(4)強烈推薦.NET類中顯式地實作接口。如果一個.NET類沒有顯式地實作一個接口,COM interop會自動為其生
成一個接口,該接口包含了這個.NET類及其父類的所有公有成員。這個被自動生成的接口被稱為"class interface"。
但是MS強烈推薦使用顯式的接口定義,原因在下面闡述。
2、執行個體示範一(不顯示定義接口)
(1)建立一個最簡單的C# Console工程,其程式如下所示:
using System;
using System.Runtime.InteropServices;
namespace CSharpServer
{
//預設的是ClassInterfaceType.AutoDispatch,該方式下隻生成dispatch接口
//隻能被使用script、VB等late binding方式的COM客戶使用
[ClassInterfaceAttribute(ClassInterfaceType.AutoDual)]
public class SharpObject
{
private string m_strName;
public SharpObject(){}
public string Name //Property: Name, Get/Set
{
get { return m_strName; }
set { m_strName = value; }
}
}
}
(2)在工程的屬性中設定Register for COM interop為True。這樣編譯後就會生成CSharpServer.tlb檔案,并且
自動将其注冊。指令行指令regasm有同樣的效果。系統資料庫内容如下:
[HKEY_CLASSES_ROOT/CLSID/{88994E22-E99F-320B-908C-96E32B7BFE56}]
@="CSharpServer.SharpObject"
[/InprocServer32]
@= "C://WINNT//System32//mscoree.dll"
"ThreadingModel"="Both"
"Class"="CSharpServer.SharpObject"
"Assembly"="CSharpServer, Version=1.0.583.39183, Culture=neutral, PublicKeyToken=null"
"RuntimeVersion"="v1.0.2914"
"CodeBase"="file:///E:/cm/net/C%23/exer/CSharpServer/bin/Debug/CSharpServer.dll"
[/ProgId]
@="CSharpServer.SharpObject"
CSharpServer.tlb檔案中包含了元件的類型庫資訊,包括CLSID、IID、接口定義等。而元件的真正實作,對.NET
對象的調用則是由通用語言運作時庫mscoree.dll完成的。可以說mscoree.dll和CSharpServer.tlb加起來就是runtime為CSharpServer這個.NET類生成的CCW。
(3)寫一個簡單的VBScript程式test.vbs,如下所示:
Dim obj
Set obj = CreateObject("CSharpServer.SharpObject")
obj.Name = "Chang Ming"
MsgBox "My Name is " & obj.Name
輕按兩下該檔案,成功運作。
(4)建立一個最簡單的MFC對話框工程,加入以下代碼:
//這裡應該用raw_interfaces_only,因為SharpObject預設的從Objec
//如果不加這個選項的話,也要為Object的公用函數和屬性生成包裝函數,
//而Object::GetType傳回Type型,而沒有為類Type生成包裝接口,是以編譯時會出錯
#import "../CSharpServer/bin/debug/CSharpServer.tlb" raw_interfaces_only no_namespace named_guids
...
{
CoInitialize(NULL);
//方法一
//因為使用了raw_interfaces_only,是以沒有生成屬性Name的包裝函數GetName,PutName
_SharpObjectPtr pSharpObject(__uuidof(SharpObject));
pSharpObject->put_Name(_bstr_t("Chang Ming"));
BSTR strName;
pSharpObject->get_Name(&strName);
AfxMessageBox("My Name is " + _bstr_t(strName));
//方法二
CoUninitialize();
}
自動生成的class interface中,接口名是'_'+類名,即_SharpObject。除此之外,使用方式與調用一般的COM對
象完全一樣。
(5)使用class interface的缺點在于.NET類的變化會影響到COM客戶。具體而言,對于使用Script、VB等late binding
方式的語言如test.vbs,NET類的變化對其沒有影響。而對于early binding的客戶,因為dispid與其在.NET類中
的位置相關,是以.NET類的變化很有可能會改變成員的dispid,進而會影響到客戶程式,客戶程式需要重新編譯。
對于通過指針直接調用的C++客戶程式,每次.NET的重新編譯都會導緻其重新編譯,因為class interface的IID
每次都是随機生成的!是以MS強烈要求不要使用這種方式,class interface不能算是一個真正的接口,它總是
不斷的改變,這違背了接口的精神,違背了COM的精神。
3、執行個體示範二(顯示定義接口)
(1)建立一個最簡單的C# Console工程,其程式如下所示:
using System;
using System.Runtime.InteropServices;
namespace CSharpServer2
{
//如果不指定guid,每次都會随機生成IID
[Guid("539448DE-9F3B-4781-A1F6-F3C852091FC9")]
public interface ISharpObject2
{
string Name //Property: Name, Get/Set
{
get;
set;
}
void Test();
}
//如果不指定guid,每次都會随機生成CLSID
[Guid("F5A31AAB-FAA9-47cc-9A73-E35606114CE8")]
public class SharpObject2 : ISharpObject2
{
private string m_strName;
public SharpObject2(){}
public string Name //Property: Name, Get/Set
{
get { return m_strName; }
set { m_strName = value; }
}
public void Test(){}
}
}
(2)在工程的屬性中設定Register for COM interop為True。這樣編譯後就會生成CSharpServer2.tlb檔案,并
且自動将其注冊。系統資料庫内容如下:
[HKEY_CLASSES_ROOT/CLSID/{F5A31AAB-FAA9-47CC-9A73-E35606114CE8}]
@="CSharpServer2.SharpObject2"
[/InprocServer32]
@= "C://WINNT//System32//mscoree.dll"
"ThreadingModel"="Both"
"Class"="CSharpServer2.SharpObject2"
"Assembly"="CSharpServer2, Version=1.0.583.38696, Culture=neutral, PublicKeyToken=null"
"RuntimeVersion"="v1.0.2914"
"CodeBase"="file:///E:/cm/net/C%23/exer/CSharpServer2/bin/Debug/CSharpServer2.dll"
[/ProgId]
@="CSharpServer2.SharpObject2"
(3)建立一個最簡單的MFC對話框工程,加入以下代碼:
//這裡不用raw_interfaces_only,因為SharpObject2隻從接口ISharpObject2繼承
//而ISharpObject2沒有父類,是以不會有SharpObject那樣的編譯錯誤
#import "../CSharpServer2/bin/debug/CSharpServer2.tlb" no_namespace named_guids
...
{
CoInitialize(NULL);
//方法一
ISharpObject2Ptr pSharpObject2(__uuidof(SharpObject2));
pSharpObject2->PutName("Chang Ming");
AfxMessageBox("My Name is " + pSharpObject2->GetName());
//方法二
CoUninitialize();
}
隻有接口ISharpObject2保持不變,就不會影響到COM客戶程式。
下載下傳CCW的示例代碼(50KB)