知識(shí)需要不斷積累、總結(jié)和沉淀,思考和寫作是成長的催化劑
一、COM和.NET
COM組件對(duì)象模型是在.NET之前的一種編程規(guī)范,它允許不同的語言之間可以互相操作。由于COM規(guī)范比較復(fù)雜,注冊(cè)表,內(nèi)存對(duì)象管理,錯(cuò)誤處理機(jī)制都和.NET不同,.NET做為其后秀,應(yīng)用起來更簡單,但一般不會(huì)因?yàn)樾录夹g(shù)可用就重寫已有的代碼,所以就引來COM的互操作性
我們可能不必編寫COM組件,但了解是有用的。經(jīng)常會(huì)遇到嵌入互操作類型,為COM設(shè)置互操作問題
先看一下COM的一些基本概念,挑了幾個(gè)重要的也是比較好理解的
元數(shù)據(jù)
COM的元數(shù)據(jù)信息存儲(chǔ)在tlb類型庫中,包含接口、方法和參數(shù)名稱等,在.NET程序集中元數(shù)據(jù)都存儲(chǔ)在程序集中的。
內(nèi)存管理
我們知道.NET托管對(duì)象的內(nèi)存釋放都有垃圾回收器GC完成,不同于COM,COM依賴引用計(jì)數(shù),
接口
COM三個(gè)基本接口,IClassFactory、IUnknown、Idispatch
IClassFactory,每個(gè)組件都有一個(gè)相關(guān)的類廠用于創(chuàng)建COM組件對(duì)象。非托管對(duì)象,客戶端是無法直接New對(duì)象的,所以只能通過交給類廠來創(chuàng)建實(shí)例然后把實(shí)例的指針交給客戶端
每個(gè)COM對(duì)象必須實(shí)現(xiàn)IUnknown接口,QueryInterface用于查詢組件實(shí)現(xiàn)的其它接口,說白了也就是看看這個(gè)組件的父類中還有哪些接口類,AddRef()遞增引用計(jì)數(shù),Release()遞減引用計(jì)數(shù),為0后就銷毀對(duì)象
IDispatch調(diào)度接口派生自IUnknown接口,在其基礎(chǔ)上又增加了GetIDsOfNames()和Invoke(),調(diào)用接口會(huì)創(chuàng)建方法或?qū)傩詫?duì)應(yīng)的調(diào)用ID映射表,這樣調(diào)用時(shí)先獲取根據(jù)名字獲取調(diào)度ID然后Invoke調(diào)用。因?yàn)椴⒉皇撬械恼Z言(客戶端)(像一些js腳本語言)都支持指針,也就不能通過虛函數(shù)表來調(diào)用,所以用調(diào)度接口增加函數(shù)ID映射。
注冊(cè)
.NET中區(qū)分私有程序集和共享程序集。在COM中,通過注冊(cè)表配置的所有組件都是全局可用的。所有COM對(duì)象都有一個(gè)唯一標(biāo)識(shí)符CLSID類ID,創(chuàng)建COM對(duì)象時(shí),COM API調(diào)用CoCreateInstacne()方法,在注冊(cè)表中查找CLSID的dll或exe路徑,然后加載,實(shí)例化組件
線程
COM使用單元模型,單元模型有單線程單元模型STA和多線程單元模型MTA
STA單線程單元模型,在Winfrom程序中經(jīng)??吹組ain入口函數(shù)上面標(biāo)記STAThread特性。在STA中只允許創(chuàng)建實(shí)例的線程訪問組件。一個(gè)進(jìn)程中也可以包含多個(gè)STA
MTA多線程單元模型,在MTA中,多個(gè)線程可以同時(shí)訪問組件
編組
.NET和COM之間的數(shù)據(jù)傳遞必須經(jīng)過轉(zhuǎn)換,這種機(jī)制就是編組(marshaling)。轉(zhuǎn)換過程取決于數(shù)據(jù)類型。簡單的數(shù)據(jù)類型如byte、short、int和long屬性blittable類型,在com和net中是一樣的表示方法,其他nonblittable類型的則需要進(jìn)行轉(zhuǎn)換,當(dāng)然會(huì)有些開銷
| COM數(shù)據(jù)類型 | .Net數(shù)據(jù)類型 |
|---|---|
| SAFEARRAY | Array |
| VARIANT | Object |
| BSTR | String |
| Iunknown,Idispatch | Object |
二、.NET客戶端調(diào)用COM組件
由于COM對(duì)象和.NET對(duì)象在生命周期、內(nèi)存管理、接口服務(wù)上的差異,運(yùn)行時(shí)提供了包裝類來使其互相調(diào)用。托管客戶端調(diào)用 COM 對(duì)象方法時(shí),運(yùn)行時(shí)就會(huì)創(chuàng)建一個(gè)運(yùn)行時(shí)可調(diào)用包裝器 (RCW)來封送引用機(jī)制之間的差異。 也會(huì)創(chuàng)建了一個(gè) COM 可調(diào)用包裝器 (CCW) 來逆轉(zhuǎn)此過程

三、COM客戶端調(diào)用.NET組件
沒寫過COM,也不是很了解,但一些約定規(guī)范必須遵守,原理和.NET客戶端調(diào)用COM組件類似
比如在C#類庫的AssemblyInfo.cs中修改
// 將 ComVisible 設(shè)置為 false 使此程序集中的類型
// 對(duì) COM 組件不可見。如果需要從 COM 訪問此程序集中的類型,
// 則將該類型上的 ComVisible 特性設(shè)置為 true。
[assembly: ComVisible(false)]
在程序集屬性中勾選COM互操作注冊(cè)
然后任何一個(gè)需要暴露給COM客戶端的都需要有接口
[ComVisible(true)]
[Guid("35A5CE1E-551C-41EC-81D4-005318550119")]
public interface IMyClass
{
void Initialize();
void Dispose();
int Add(int x, int y);
}
編譯時(shí)候因?yàn)楣催x的為COM互操作注冊(cè),所以需要以管理員運(yùn)行的才能注冊(cè)成功
四、嵌入互操作類型
引用PIA(主互操作程序集,COM組件生成)時(shí),可以設(shè)置是否嵌入互操作類型。嵌入互操作類型時(shí)(True)則PIA不隨著程序一起部署,程序只是引用COM中的類型信息,這樣的好處就是可以部署到不同COM版本的環(huán)境中。比如常用的Office開發(fā)Microsoft.Office.Interop.Excel,設(shè)置嵌入互操作類型,就可以不依賴office版本。改為互操作false后也就將PIA復(fù)制到本地
有時(shí)候會(huì)無法嵌入互操作類型請(qǐng)改為適當(dāng)?shù)慕涌?,單純一點(diǎn)就修改嵌入互操作設(shè)為false,OK編譯通過。不太單純的,可以修改創(chuàng)建對(duì)象的方式,像下面這樣,直接實(shí)例化的普通類,無法嵌入互操作類型
Application excelApp = new ApplicationClass();
Application excelApp = new Application()
這樣是可以的,Application雖然是一個(gè)接口,理論上應(yīng)該不能實(shí)例化的,當(dāng)它上面標(biāo)記了
[CoClass(typeof (ApplicationClass))
告訴運(yùn)行時(shí)CLR,當(dāng)有人要?jiǎng)?chuàng)建類型為Application的實(shí)例時(shí),它實(shí)際上應(yīng)該繼續(xù)創(chuàng)建ApplicationClass的實(shí)例。
用COM接口的可以嵌入,直接使用coclass的無法嵌入
五、平臺(tái)調(diào)用DllImport
還有一些非托管庫不包含COM對(duì)象,只包含倒出的函數(shù),這時(shí)候需要使用平臺(tái)調(diào)用服務(wù)(P-Invoke),CLR會(huì)加載包含所需調(diào)用函數(shù)的dll,并編組參數(shù)。在C++的非托管庫中使用dllexport暴露函數(shù),在C#中使用dllimport導(dǎo)入?;菊Z法如下
[DLLImport(“DLL文件”)]
修飾符 extern 返回變量類型 方法名稱 (參數(shù)列表)
dllimport在命名空間System.Runtime.InteropServices下,該特性用于對(duì)照非托管庫中導(dǎo)出的函數(shù)
[AttributeUsage(AttributeTargets.Method)]
public class DllImportAttribute: System.Attribute
{
public DllImportAttribute(string dllName) {…} //定位參數(shù)為dllName
public CallingConvention CallingConvention; //入口點(diǎn)調(diào)用約定
public CharSet CharSet; //入口點(diǎn)采用的字符接
public string EntryPoint; //入口點(diǎn)名稱
public bool ExactSpelling; //是否必須與指示的入口點(diǎn)拼寫完全一致,默認(rèn)false
public bool PreserveSig; //方法的簽名是被保留還是被轉(zhuǎn)換
public bool SetLastError; //FindLastError方法的返回值保存在這里
public string Value { get {…} }
}
需要注意的就是數(shù)據(jù)類型的映射,必須映射到.NET數(shù)據(jù)類型上??梢允褂?strong>P/Invoke Interop Assistant工具,它支持托管代碼和非托管代碼之間的方法簽名的轉(zhuǎn)換,可以直接生成調(diào)用代碼

一般會(huì)在一些特殊場(chǎng)合來調(diào)用win 32的api,比如像輸入法程序設(shè)置永遠(yuǎn)不獲取焦點(diǎn),一些任務(wù)處理時(shí)不希望用戶點(diǎn)擊別的操作(當(dāng)然窗體也不能崩了),這時(shí)候可以使用下面設(shè)置窗體控件不可用
[System.Runtime.InteropServices.DllImport("user32.dll")]
public static extern int SetWindowLong(IntPtr hWnd, int nIndex, int wndproc);
[System.Runtime.InteropServices.DllImport("user32.dll")]
public static extern int GetWindowLong(IntPtr hWnd, int nIndex);
public const int GWL_STYLE = -16;
public const int WS_DISABLED = 0x8000000;
public static void SetControlEnabled(Control c, bool enabled)
{
if (enabled)
{
SetWindowLong(c.Handle, GWL_STYLE, (~WS_DISABLED) & GetWindowLong(c.Handle, GWL_STYLE));
}
else
{
SetWindowLong(c.Handle, GWL_STYLE, WS_DISABLED + GetWindowLong(c.Handle, GWL_STYLE));
}
}
六、等等
關(guān)于COM也只是知曉一二,平常主要寫業(yè)務(wù),COM用的不多,充其量就是調(diào)用。做底層嵌入式開發(fā)應(yīng)該用的比較多,比如設(shè)備打印機(jī)驅(qū)動(dòng)等。了解總沒壞處,拜了個(gè)拜