怎麼通俗的解釋COM組件?

GUID IID IUnknow IDL 能夠通俗的解釋嗎


解釋不解釋也都是死掉了的技術了啊……

COM主要是一套給C/C++用的介面,當然為了微軟的野心,它也被推廣到了VB、Delphi以及其他一大堆奇奇怪怪的平台上。它主要為了使用dll發布基於interface的介面。我們知道dll的介面是為了C設計的,它導出的基本都是C的函數,從原理上來說,將dll載入到內存之後,會告訴你一組函數的地址,你自己call進去就可以調用相應的函數。

但是對於C++來說這個事情就頭疼了,現在假設你有一個類,我們知道使用一個類的第一步是創建這個類:new MyClass()。這裡直接就出問題了,new方法通過編譯器計算MyClass的大小來分配相應的內存空間,但是如果庫升級了,相應的類可能會增加新的成員,大小就變了,那麼使用舊的定義分配出來的空間就不能在新的庫當中使用。

要解決這問題,我們必須在dll當中導出一個CreateObject的方法,用來代替構造函數,然後返回一個介面。然而,介面的定義在不同版本當中也是有可能會變化的,為了兼容以前的版本同時也提供新功能,還需要讓這個對象可以返回不同版本的介面。介面其實是一個只有純虛函數的C++類,不過對它進行了一些改造來兼容C和其他一些編程語言。

在這樣改造之後,出問題的還有析構過程~MyClass()或者說delete myClass,因為同一個對象可能返回了很多個介面,有些介面還在被使用,如果其中一個被人delete了,其他介面都會出錯,所以又引入了引用計數,來讓許多人可以共享同一個對象。

其實到此為止也並不算是很奇怪的技術,我們用C++有的時候也會使用Factory方法來代替構造函數實現某些特殊的多態,也會用引用計數等等。COM技術的奇怪地方在於微軟實在是腦洞太大了,它們構造了一個操作系統級別的Factory,規定所有人的Interface都統一用UUID來標識,以後想要哪個Interface只要報出UUID來就行了。這樣甚至連鏈接到特定的dll都省了。

這就好比一個COM程序員,只要他在Windows平台上,調用別的庫就只要首先翻一下魔導書,查到了一個用奇怪文字寫的「Excel = {xxx-xxx-xxxx...}」的記號,然後它只要對著空中喊一聲:「召喚,Excel!CoCreateInstance, {xxx-xxx-xxxx...}」

然後呼的從魔法陣裡面竄出來了一個怪物,它長什麼樣我們完全看不清,因為這時候它的類型是IUnknow,這是腦洞奇大無比的微軟為所有介面設計的一個基類。我們需要進一步要求它變成我們能控制的介面形態,於是我們再喊下一條指令:

「變身,Excel 2003形態!QueryInterface, {xxx-xxx-xxxx...}」

QueryInterface使用的是另一個UUID,用來表示不同版本的介面。於是怪物就變成了我們需要的Excel 2003介面,雖然我們不知道它實際上是2003還是2007還是更高版本。

等我們使喚完這隻召喚獸,我們就會對它說「回去吧,召喚獸!Release!」但是它不一定聽話,因為之前給它的命令也許還沒有執行完,它會忠誠地等到執行完再回去,當然我們並不關心這些細節。

微軟大概會覺得自己設計出了軟體史上最完美的二進位介面,從今以後所有的第三方庫都可以涵蓋在這套介面之下。然而歷史的車輪是無情的,它碾過那些自以為是的人的速度總是會比想像的更快。Java的直接基於類的介面被廣泛應用,開發使用起來遠遠來的簡單,即便偶爾出點問題大家也都想辦法解決了,事實證明程序員並不願意花10倍的編寫代碼的時間來解決二進位庫的版本兼容問題,他們更願意假裝沒看見。很快微軟也抄了一個.NET託管dll的方案出來,於是純的二進位介面COM就慢慢被拋棄了。

COM,OLE,ActiveX,OCX,VBScript,歷史不會忘記你們的,如果歷史忘了,我替歷史記住你們。安息吧。

=============================================================

這怎麼還火了,這不應該是一個討論遺留技術的冷門問題嗎……

補充說明一下,其實並沒有貶低COM的意思,COM在當時一定是一個偉大的發明,只是技術革新太快。到今天,其實COM都是C++的二進位本機代碼的動態鏈接的最佳的選擇(Windows上的)。但是,C++,二進位本機代碼,動態鏈接,這三件事現在沒有一件是重要的……

COM在Windows操作系統底層繼續發揮餘熱的時間肯定還會很長,因為永遠都會有偏底層的C++開發的需要,必須遊戲之類。只是技術趨勢已經很明確了。


因為公司產品的關係,一直都使用COM框架。

下面來一個一個解釋吧 。

GUID:

全局唯一標識符,可以看成是唯一的一個ID,類似於物理網址那樣。

IID:

也就是介面的唯一ID。C++中本沒有介面的概念,是COM強行引入的,也就是一個類中全部都是純虛函數,這樣的類稱為介面(interface),微軟甚至定義了一個宏,大概就是這樣:#define interface struct;

IUnknown介面:

這個解釋起來會比較麻煩。如 @靈劍 所說,當自己寫的一個dll升級的時候,內部可能增加了成員,導致分配的空間發生變化,從而使得次dll和以前的dll不能兼容。這個就是臭名昭著的dll hell,為此微軟最開始想了個很挫的方法,那就是在dll後面加上自己的版本號,如:

myDll_1.dll, myDll_2.dll……如果你打開system32目錄看看就知道是怎麼回事了。

但是這樣總不是一個辦法,假如有個實現類MyClass,我這樣操作:MyClass* ptr = new MyClass(); MyClass可能在不同dll版本中占的空間不同產生兼容問題,我拿一個指向MyClass的指針調用方法也會產新問題,那麼,如果是指向一個介面(只含有純虛方法,不含有成員)的指針不就沒有問題了嗎,於是就變成了這樣:

IMyClass* ptr = new MyClass();

但是這樣還沒有解決new的問題,最好有一個創建實例的方法,並且返回共同的介面,這便是IUnknown的由來。微軟一拍腦袋,想出了類似下面這樣的代碼:

IUnknown* pUnk = NULL;

HRESULT hr = CreateObject(pUnk);

通過統一的函數,創建出個統一的介面的實例來避免產生dll兼容性的問題。

再談GUID、CLSID、IID:

從上來看,所有的COM類其實都繼承了IUnknown。但是,我拿個IUnknown介面有毛用啊,我還是需要把它轉為我的具體類才行。假設有個汽車類Car,它繼承於ICar,像這樣:

IUnknown* pUnk = NULL;

CreateCar(pUnk);

ICar* pCar = (ICar*)pUnk;

這樣,我們拿到ICar指針才有意義。

但是微軟認為,直接由用戶來轉型是不安全的,比如,你怎麼知道pUnk一定可以轉成ICar*呢。除此之外,ICar這個類不具有唯一性,我們需要唯一的一個標識符來確定一個類,那麼這個標識符就是GUID。類ID就叫作CLSID,介面ID就叫作IID。我們需要一個轉型的函數叫QueryInterface

QueryInterface作為IUnknown中的一個純虛函數,做的事情其實很簡單,判斷自己能不能轉成某個GUID所指向的類而已。如果不可以,則返回E_NOTIMPL(謝謝 @Gee Law 指出),可以的話返回S_OK,並將轉換後的指針作為參數返回,代碼類似如下,可以體會一下:

public class Car : IUnknown, ICar
{
HRESULT QueryInterface(REFIID riid, void **ppvObject)
{
if (ISEQUAL_IID(riid, IID_ICar)) //riid和ICar的IID相同,說明可以轉換成ICar
{
*ppvObject = static_cast&(this);
return S_OK;
}
else if (ISEQUAL_IID(riid, IID_IUnknown))
{
*ppvObject = static_cast&(this);
return S_OK;
}
return E_NOTIMPL;
}
}

一個真正的QueryInterface要做的事情還要多一點,如增加引用計數等,這裡就不多說了。

外部是這樣調用:

ICar* pCar = NULL;

pUnk-&>QueryInterface(IID_ICar, (void**)pCar);

這樣,我們就從pUnk得到了個ICar*。

其實,這種寫法,丑爆了。

IDL:

微軟說我們的COM很NB,和語言是無關的!只要你按照我們的要求,建立一個介面,就可以實現COM。

但是,不同語言的語法不同,怎麼才能有個通用的方案來定義介面呢,於是微軟用了洪荒之力發明了一種語言,叫作IDL。

遵循IDL,就可以根據不同平台生成不同代碼,如我定義了一個整數,在Java中可能是double,在VB中可能是Integer,但是我只需要寫一份IDL,用IDL的解析器,如midl.exe可自動生成目標語言的代碼(想法往往是美好的,但是好像沒有什麼人會生成其他語言吧……)

COM其實是個很龐大的體系,還有很多內容沒有說,如引用計數、Invoke、RPC之類的,但是基本的思想就是上面我說的這樣了。


@靈劍 說得有理,然而在 1993 年(COM 發明),兼容機的硬碟非常的小。現在流行的那種,所有依賴都打包的方法沒法用,DLL 都是能共享就共享的,為了避免依賴衝突只能用這麼扭曲的方式實現。

1993 年的「多媒體電腦」配置是啥樣的呢,維基里說是這樣的

In 1993, an MPC Level 2 minimum standard was announced:

  • 25 MHz 486SX CPU.
  • 4 MB RAM.
  • 160 MB hard disk.
  • 16-bit color, 640×480 VGA video card.
  • 2× (double speed) CD-ROM drive using no more than 40% of CPU to read at 1x, with &< 400 ms seek time.
  • Sound card outputting 44 kHz, 16-bit CD quality sound.
  • Windows 3.0 with Multimedia Extensions, or Windows 3.1.

哦對了 M$ 為了照顧市場很可能是針對幾年前的 PC 配置設計的,1991 年的 Multimedia PC Level 1 配置是這樣:

The first MPC minimum standard, set in 1991, was:

  • 16 MHz 386SX CPU.
  • 2 MBRAM.
  • 30 MB hard disk.
  • 256-color, 640×480 VGA video card.
  • 1× (single speed) CD-ROM drive using no more than 40% of CPU to read, with &< 1 second seek time.
  • Sound card (Creative Sound Blaster recommended as closest available to standard at the time) outputting 22 kHz, 8-bit sound; and inputting 11 kHz, 8-bit sound.
  • Windows 3.0 with Multimedia Extensions.


已經將近7年沒碰COM了,強行答一發。

從原理來說,C++並不像Java的JVM一樣到二進位級別都有詳細定義。C++對compiler vendor在二進位細節上並沒有約束:因為C++更貼近裸機,每一個約束都可能造成二進位代碼並不能在某些平台上高效執行。

舉個簡單的例子,C++的對象模型中實現多態和動態綁定的部分,是通過虛表(vtable)和虛指針(vptr)實現的。但是標準中沒有說,vptr應該長在對象的首還是尾,沒說各個虛函數的坑應該怎麼排列,是按照函數定義的順序排列還是按照字母表順序排,廠商也可以在虛表實現里增加自己的奇怪插入信息,比如加個對象識別符什麼的,甚至函數重載所使用的Name Mangling(同一個名字的函數能綁定到不同的參數版本,需要在名字上做手腳,因為symbol table在link的時候需要看到不同的名字才能找到函數實體)機制也沒有規範。

這意味著什麼?Java產生的對象,至少ByteCode級別是兼容的,不論你是用IBM的JDK或者OpenJDK還是Oracle的原裝JDK,只要大版本一樣,互相之間是可以調用的。C++就不行了,你一個Intel C++ Compiler或者g++哪怕在同一個平台,格式也是不同的,你想做動態調用,我說

ptr-&>func()

就這麼個簡單的動作,不同的編譯器產生的對象而言,你都無法確定虛表怎麼定位(當然你也可以說你見過的compiler都把虛表放在對象頭部,其實他們也可以不這麼做,沒人攔著他們),就算定位了,你甚至都找不到對應的虛函數指針。

對於所有工程的源碼都混在一個項目里編譯的場景,這不是問題,因為你用同一個編譯器,同一套規範產生代碼,代碼也是編譯時就link起來的,二進位細節你完全無需關心。但是你要發布一個組件呢?我想做一個時鐘類,不開放代碼,二進位形式,比如以DLL方式發布出來,給別的C++項目,甚至一個JAVA項目去調用,沒有COM的話,你大概只能選擇以沒有OO的基本方式發布一個函數組合,裡面沒有繼承,沒有類,什麼都沒有,只有一堆函數。但是如果我還是希望有面向對象呢,還是希望有類,有繼承呢?

COM一大部分是為了解決這個問題而產生的。本質上來說,COM就是定義了一套二進位規範,用C、C++無歧義的部分,手工擼了一套OO機制出來。這樣的機制,在你不考慮跨組件之間共享的時候,是很累贅很扯淡的,畢竟以前用編譯器幫忙搞定的事情,現在都得手動了。但是遵循這套規範的好處是,一個組件對象,可以被不同編譯器動態載入和調用。甚至,即便如.NET Java VB這樣畫風完全不一樣的東西,也能調用一個C++寫出來的COM組件。

既然自己定義OO規範,那麼微軟就可以按照自己的想法對整個體系進行全新設計。例如用引用計數(相對於另一種在VM中非常普遍的設計:垃圾回收,這個更容易在沒有VM的環境乾淨地實現)來管理對象生命周期,所以有了AddRef和Release;為了實現安全的動態轉型(Dynamic Cast),微軟規定需要用QueryInterface來查詢介面;這三個放在一起,就是COM的根介面,類似Java的Object根對象,它定義了所有COM對象都需要實現的功能:生命周期管理,安全的動態轉型。

IDL則是為了方便對COM對象的介面定義而設計的語言。IDL規定了一個COM組件有些什麼介面,介面有些什麼函數,但是不涉及具體實現。你可以簡單粗暴地認為這個東西類似COM的頭文件。用這個介面定義語言,可以方便地產生跨平台的COM調用輔助代碼。

而GUID則是一個全局唯一標識符(通過奇怪的演算法產生一個基本不可能重複的ID),這個標識符可以用在標示組件本身,也可以用來標示一個介面,標示介面的時候它就被稱為IID(Interface ID)。唯一的標示解決了組件重名可能的混淆,或者同一個組件不同版本,介面之間不同版本的識別。不同於Java的用包路徑標識類還可能會版本衝突,GUID標識的類和介面,基本可以認為沒有這個問題。

需要詳細理解的,請去看《COM本質論》,在此之前可能要先看《Inside C++ Object Model》。後一本看完讓你對C++的OO實現有比較深入的理解,再看前一本你才能明白為何COM要被設計出來。


簡單說COM就是一個通用的ABI規範,只要遵從這個規範,不管用什麼語言寫的程序都可以互相調用。

之後的DCOM/COM+又把這個規範擴展到了網路上,變成了一個通用的RPC規範。

IUnknown是用來實現安全的dynamic cast的,GUID就是全局唯一的類型標示,IDL則是介面描述。

另外COM明明沒有死,整個WindowsRT的API都是遵從COM規範的,反倒是之前的Win32 native API已經不再更新了。


COM是OO技術的巔峰之作,COM之後的OO都是COM的子集而已

COM的意圖在於標準化OO組件的界面,使得

  1. 跨平台
  2. 跨語言

  3. 跨機器
  4. 跨進程

的分散式對象服務可以標準化,而且服務方可以被動態替換。COM的本質是一個分散式OO-RPC規範

如果光是RPC規範,那麼COM倒也不稀奇。COM最酷的一點是,允許把本地的RPC轉化成In-Process Procedure Call(進程內過程調用)。這點極大地優化了性能,而且使得COM成為了有史以來最強的RPC方案


IUnknown 是所有介面的共同介面,QueryInterface 就是 dynamic_cast


首先很多大型軟體都使用 COM 來提供 API,這個死不了,Office 軟體、Adobe PDF、IE的API、正則表達式介面、CAD的API,winnet 介面等等。

就是使用 NET 技術,也會經常接觸到它。

具體的,可以看看, VS 中的引用菜單中的 COM 項目,那裡有很多很多COM組件,它們所提供的功能,用起來都十分的方便。

有些人說 使用 當獨的DLL也可以,Windows 下,有些場景中,使用 COM 要方便的多。特別是「對像瀏覽器」中可以直接查看到 COM 組件的成員。並且VBE ,VS 這些開發環境中,對COM組件中的類提供了自動代碼實例,成員提示等功能,對程序員來說,這是一件很方便的事情。

COM 技術是由2部分組成:

第一部分:程序的二進位內存結構約定,

第二部分:由操作系統提供的支持 COM API 介面(如API函數: CoInitialize函數) 以及 工具集(如:regver32.exe 和 註冊表)

相信我,COM 技術掌握好了,對你快速掌握其他的 Windows 下的編程技術有無可替代的重要性,如果你只是在 linux 下編程,學習 COM 技術,也可以拓寬的你的視野,總得說來,只有好處,沒有壞處,如果你說沒時間學,那當我沒說。

====不要爭論技術的好壞,所有技術都有優點和缺點,我們要利用的是它好的一面,以及適合自己的一面======


COM思想光芒四射,千秋萬代,就是編程界的毛神思想。你們人類早晚會接受的。windows領先*x一個時代。


COM的確過時了,為了紀念我曾經肯過的的多本COM、ATL、WTL的書,我來說說。

COM的最核心的思想,說白了就是要做個跨語言的 「class」 「object」 「function」 。

因為所有OOP語言,這三個都是核心,如果能做到這三個語言要素跨語言,所有語言不都是可以互通了,互通了,那麼所有程序員不都可以很happy的在windows上用自己喜歡的語言編程了,這是目的。是不是很簡單?

但做起來就費事兒了,而且用起來過於複雜,這就導致了後來的vm的方式來達到語言互通,vm提供了基礎的語言或者說運行時的抽象,上層語言架構在vm之上,之後的語言互通就好辦了,畢竟有了「經濟基礎」,這條路被java證實了,而且很成功。所以微軟就放棄了com,走了CLR。

繼續說COM的實現,為了跨語言,該怎麼做呢?

1)肯定需要有種中立的,對語言里的類型系統的抽象,這就是type library,大家對別的語言的類型,不是直接去理解的,而是通過公共的類型來互相翻譯。

2)類型必須要要有名字,我才能知道你說的是什麼。而類型的名字沒法跨語言,首先有些語言,比如c++,編譯之後,根本不存類型的名字,其次這麼多語言要求名字不重複,簡直沒法編程了,所以這就有了 class id,他是UUID,也就是說一個計算出來的名字,就像網卡mac一樣,絕B不可能重複,你可以看看註冊表裡有很多「CLASSID」,就是這麼來的。

3)我們真的那麼需要類型跨語言嗎?回想一下我們對class的使用,絕大多數時候,我們是不會訪問成員的,而是通過function來訪問類成員,所以我們的首要工作是研究怎麼跨語言調用別的語言的function。特別是我們不會簡單的調用一個function,我們會把功能相關的function組織起來,成為」一堆",這一堆function我們叫他interface,到這裡,我們想到了,我們不會直接把class裸露的暴露給別的語言,我們只是暴露給別的語言以interface,當然函數調用的時候,參數也有類型,所以類型的跨語言也是必須的。

什麼叫COM組件,說白了,就是一堆功能相關的interface,他是某種語言像另種語言暴露功能的最大單位。

4)COM組件並不需要名字,或者說不需要UUID,因為我們總是使用他裡面的介面,而不是直接使用COM組件,所以介面也要UUID。說了這麼多,COM架構這麼複雜,肯定需要一個中間層,或者說擺渡人,這就是COM Library(一堆dll) + 註冊表。A應用通知COM Library,並輸入介面的UUID,由COM Library裝入B應用的該組件對應的dll,並把介面指針返回給A應用,指針里指示的是一堆函數指針,由這些指針,可以調用到B應用里的函數功能。

5)還可以說很多很多,DCOM,套間線程,COM+ service, 統一結構化存儲, URL名字對象等等,你可以看到細節是魔鬼,這一切太細節太複雜了。

畢竟是死了的東西,我也不想浪費時間去回憶了,通過對本文黑體字的理解,你應該可以知道COM到底是什麼了,他想幹嘛。


記得有一篇文章里說過,可以把COM組件理解為一塊一塊的樂高積木


com就沒用了,com就過時了,這種說法真是讓人無力吐槽。你倒是找個替代品看看。


COM 就是一套介面規範,就好象交通規則一樣,不存在過時的說法。

將inole2裡面的例子掃一遍,基本就能夠掌握COM ,而且發現微軟是一點點小功能也搞個介面。若好奇某個介面,可以快速搜下reactos,了解思想就可以了。

特別是inole2中CHAP14的COSMO例子,提供了Office 軟體、Adobe PDF、IE的API類似介面框架。明了後,可短時間掌握很多大型程序框架。

MFC源碼是學習C++的好題材,但我們一直過於注重細節,浪費了。


推薦閱讀:

如何理解「面向對象編程的精髓在於將操作綁定在數據上」?
類(class)能不能自己繼承自己?
新手求教python 面向對象編程的一個問題?
為什麼有的人連OO、FP等基本的語言概念都搞不清楚,卻整天吹噓OO/FP的好處?

TAG:微軟Microsoft | 編程 | 面向對象編程 | ComponentObjectModel |