天天看點

COM元件對象與.NET類對象的互相轉換

運作環境: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所示:

COM元件對象與.NET類對象的互相轉換

圖1 COM wrapper overview

三、.NET中調用COM元件

1、RCW(Runtime Callable Wrapper)簡介

其示意圖如圖2所示:

COM元件對象與.NET類對象的互相轉換

圖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所示:

COM元件對象與.NET類對象的互相轉換

圖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)