COM學習(三)——COM的跨語言


COM是基於二進制的組件模塊,從設計之初就以支持所有語言作為它的一個目標,這篇文章主要探討COM的跨語言部分。

idl文件

一般COM接口的實現肯定是以某一具體語言來實現的,比如說使用VC++語言,這就造成了一個問題,不同的語言對於接口的定義,各個變量的定義各不相同,如何讓使用vc++或者說Java等其他語言定義的接口能被別的語言識別?為了達到這個要求,定義了一種文件格式idl——(Interface Definition Language)接口定義語言,IDL提供一套通用的數據類型,並以這些數據類型來定義更為復雜的數 據類型。一般來說,一個文件有下面幾個部分說明

  1. 接口的定義
  2. 組件庫的定義
  3. 實現類的定義
    而各個部分又包括他們的屬性定義,以及函數成員的定義

屬性:

屬性是在接口定義的上方,使用“[]”符號包裹,一般在屬性中使用下面幾個關鍵字:
object:標明該部分是一個對象(可以理解為c++中的對象,包括接口和具體的實現類)
uuid:標明該部分的GUID
version:該部分的版本

接口定義

接口定義采用關鍵字interface,接口函數定義在一對大括號中,它的定義與類的定義相似,其中函數定義需要修飾函數各個參數的作用,比如使用in 表示它作為輸入參數,out表示作為輸出參數,retval表示該參數作為返回值,一般在VC++定義的接口中,函數返回值為HRESULT,但是需要返回一個值供外界調用,此時就使用輸出參數並加上retval表示它將在其他語言中作為函數的返回值。

組件庫定義

庫使用library關鍵字定義,在定義庫的時候,它的屬性一般定義GUID和版本信息,而在庫中通常定義庫中的實現類的相關信息,庫中的信息也是寫在一對大括號中

實現類的定義

接口實現類使用關鍵字coclass,接口類的屬性一般定義一個object,一個GUID,然后一般定義實現類不需要向在C++中那樣定義它的各個接口,各個數據成員,只需要告知它實現哪些接口即可,也就是說它繼承自哪些接口。
下面是一個具體的例子:

import "unknwn.idl";

[
	object,
	uuid(CF809C44-8306-4200-86A1-0BFD5056999E)
]
interface IMyString : IUnknown
{
	HRESULT Init([in] BSTR bstrInit);
	HRESULT GetLength([out, retval] ULONG *pretLength);
	HRESULT Find([in] BSTR bstrFind, [out, retval] BSTR* bstrSub);
};

[
	uuid(ADF50A71-A8DD-4A64-8CCA-FFAEE2EC7ED2),
	version(1.0)
]
library ComDemoLib 
{
	importlib("stdole32.tlb");
	[
		uuid(EBD699BA-A73C-4851-B721-B384411C99F4)
	]
	coclass CMyString
	{
		interface IMyString;
	};
};

上面的例子中定義了一個IMyString接口繼承自IUnknown接口,函數參數列表中in表示參數為輸入參數,out表示它為輸出參數,retval表示該參數是函數的返回值。import導入了一個庫文件類似於include。而importlib導入一個tlb文件,我們可以將其看成VC++中的#pragma comment導入一個lib庫
從上面不難看出一個IDL文件至少有3個ID,一個是接口ID,一個是庫ID,還有一個就是實現類的ID
在VC環境中通過midl命令可以對該文件進行編譯,編譯會生成下面幾個我們在編寫實現時會用到的重要文件:

  1. 一個.h文件:包含各個部分的聲明,以及接口的定義
  2. 一個_i.c文件:包含各個部分的定義,主要是各個GUI的定義

需要實現的導出函數

一般我們需要在dll文件中導出下面幾個全局的導出函數:

STDAPI DllRegisterServer(void);
STDAPI DllUnregisterServer(void);
STDAPI DllGetClassObject(const CLSID & rclsid, const IID & riid, void ** ppv);
STDAPI DllCanUnloadNow(void);

其中DllRegisterServer用來向注冊表中注冊模塊的相關信息,主要注測在HKEY_CLASSES_ROOT中,主要定義下面幾項內容:

  1. 字符串名稱項,該項中包含一個默認值,一般給組件的字符串名稱;CLSID子健,一般給實現類的GUID;CurVer子健一般是子健的版本
  2. 以版本字符串為鍵的注冊表項,該項中主要保存:默認值,當前版本的項目名稱;CLSID當前版本庫的實現類的GUID
  3. 在HKEY_CLASSES_ROOT/CLSID子健中注冊以實現類GUID字符串為鍵的注冊表項,里面主要包含:默認值,組件字符串名稱;InprocServer32,組件所在模塊的全路徑;ProgID組件名稱;TypeLib組件類型庫的ID,也就是在定義IDL文件時,定義的實現庫的GUID。
    下面是具體的定義:
const TCHAR *g_RegTable[][3] = {
	{ _T("CLSID\\{EBD699BA-A73C-4851-B721-B384411C99F4}"), 0, _T("FirstComLib.MyString")}, //組件ID
	{ _T("CLSID\\{EBD699BA-A73C-4851-B721-B384411C99F4}\\InprocServer32"), 0, (const TCHAR*)-1 }, //組建路徑
	{ _T("CLSID\\{EBD699BA-A73C-4851-B721-B384411C99F4}\\ProgID"), 0, _T("FirstComLib.MyString")}, //組件名稱
	{ _T("CLSID\\{EBD699BA-A73C-4851-B721-B384411C99F4}\\TypeLib"), 0, _T("{ADF50A71-A8DD-4A64-8CCA-FFAEE2EC7ED2}") }, //類型庫ID
	{ _T("FirstComLib.MyString"), 0, _T("FirstComLib.MyString") }, //組件的字符串名稱
	{ _T("FirstComLib.MyString\\CLSID"), 0, _T("{EBD699BA-A73C-4851-B721-B384411C99F4}")}, //組件的CLSID
	{ _T("FirstComLib.MyString\\CurVer"), 0, _T("FirstComLib.MyString.1.0") }, //組件版本
	{ _T("FirstComLib.MyString.1.0"), 0, _T("FirstComLib.MyString") }, //當前版本的項目名稱
	{ _T("FirstComLib.MyString.1.0\\CLSID"), 0, _T("{EBD699BA-A73C-4851-B721-B384411C99F4}")} //當前版本的CLSID
};

使用上一篇博文的代碼,來循環注冊這些項即可
DllGetClassObject:該函數用來生成對應的工廠類,而工廠類負責產生對應接口的實現類。
DllCanUnloadNow:函數用來詢問是否可以卸載對應的dll,一般在COM中有兩個全局的引用計數,用來記錄當前內存中有多少個模塊中的類,以及當前有多少個線程在使用它,如果當前沒有線程使用或者存在的對象數為0,則可以卸載

實現類的定義

實現部分的整體結構圖如下:

類圖

由於所有類都派生自IUnknown,所在在這里就不顯示這個基類了。
每個實現類都對應了一個它具體的類工廠,而項目中CMyString類的類廠的定義如下:

class CMyClassFactory : public IClassFactory
{
public:
	CMyClassFactory();
	~CMyClassFactory();

	STDMETHOD(CreateInstance)(IUnknown *pUnkOuter, REFIID riid, void **ppvObject);
	STDMETHOD(LockServer)(BOOL isLock);

	STDMETHOD(QueryInterface)(REFIID riid, void **ppvObject);
	STDMETHOD_(ULONG, AddRef)(void);
	STDMETHOD_(ULONG, Release)(void);

protected:
	ULONG m_refs;
};

STDMETHOD宏展開如下:

#define STDMETHOD(method) virtual HRESULT __stdcall method

所以上面的代碼展開后就變成了:

virtual HRESULT __stdcall CreateInstance((IUnknown *pUnkOuter, REFIID riid, void **ppvObject);

另外3個派生自IUnknown接口就沒什么好說的,主要說說另外兩個:
CreateInstance:主要用來生成對應的實現類,然后再調用實現類——CMyString的QueryInterface函數生成對應的接口
LockServer:當前是否被鎖住:如果傳入的值為TRUE,則表示被鎖住,對應的鎖計數器+1, 否則 -1
至於CMyString類的代碼與之前的大同小異,也就沒什么說的。
其他語言想要調用,以該項目為例,一般會經歷下面幾個步驟:

  1. 調用對應語言提供的產生接口的函數,該函數參數一般是傳入一個組件的字符串名稱。如果要引用該項目中的組件則會傳入FirstComLib.MyString
  2. 在注冊表的HKEY_CLASSES_ROOT\組件字符串名\CLSID(比如HKEY_CLASSES_ROOT\FirstComLib.MyString\CLSID)中找到對應的CLSID值
  3. 在HKEY_CLASSES_ROOT\CLSID\對應ID\InprocServer32(CLSID\{EBD699BA-A73C-4851-B721-B384411C99F4}\InprocServer32)位置處找到對應模塊的路徑
  4. 加載該模塊
  5. 根據IDL文件告知其他語言里面存在的接口,由語言調用對應的創建接口的函數創建接口
  6. 調用模塊的導出函數DllGetClassObject將查詢到的CLSID作為第一個參數,並將接口ID作為第二個參數傳入,得到一個接口
    6.后面根據idl文件中的定義,直接調用接口中提供的函數

真實ATLCOM項目的解析

最后來看看一個正式的ATLCOM項目里面的內容,來復習前面的內容,首先通過VC創建一個ATLCOM的dll項目
在項目上右鍵-->New Atl Object,輸入接口名稱,IDE會根據名稱生成一個對應的接口,還是以MyString接口為例,完成這一步后,整個項目的類結構如下:
項目類結構圖
這些全局函數的作用與之前的相同,它里面多了一個_Module的全局對象,該對象類似於MFC中的CWinApp類,它用來表示整個項目的實例,里面封裝了對於引用計數的管理,以及對項目中各個接口注冊信息的管理,所以看DllRegisterServer等函數就會發現它們里面其實很簡單,大部分的工作都由_Module對象完成。
整個IDL文件的定義如下:


import "oaidl.idl";
import "ocidl.idl";
	[
		object,
		uuid(E3BD0C14-4D0C-48F2-8702-9F8DBC96E154),
		dual,
		helpstring("IMyString Interface"),
		pointer_default(unique)
	]
	interface IMyString : IDispatch
	{
	};

[
	uuid(A61AC54A-1B3D-4D8E-A679-00A89E2CBE93),
	version(1.0),
	helpstring("FirstAtlCom 1.0 Type Library")
]
library FIRSTATLCOMLib
{
	importlib("stdole32.tlb");
	importlib("stdole2.tlb");

	[
		uuid(11CBC0BE-B2B7-4B5C-A186-3C30C08A7736),
		helpstring("MyString Class")
	]
	coclass MyString
	{
		[default] interface IMyString;
	};
};

里面的內容與上一次的內容相差無幾,多了一個helpstring屬性,該屬性用於產生幫助信息,當使用者在調用接口函數時IDE會將此提示信息顯示給調用者。
由於有系統框架給我們做的大量的工作,我們再也不用關心像引用計數的問題,只需要將精力集中在編寫接口的實現上,減少了不必要的工作量。

至此從結構上說明了為了實現跨語言COM組件內部做了哪些工作,當然只有這些工作是肯定不夠的,后面會繼續說明它所做的另一塊工作——提供的一堆通用的變量類型。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM