# 借助 C++ 進行 Windows 開發 - Windows 運行時中的高級類型
作者?[Kenny Kerr](https://msdn.microsoft.com/zh-cn/magazine/mt149362?author=Kenny+Kerr)?| September 2015

借助 Windows 運行時 (WinRT),組件開發者可以向應用開發者呈現高級類型系統。由于 Windows 運行時是完全使用 COM 接口進行實現,因此熟悉 C++ 和經典 COM 的開發者認為這似乎非常荒謬。畢竟 COM 是一種以接口(而不是類)為主的編程模型。借助 COM 激活模型,可以使用 CoCreateInstance 和友元來構造類,但這似乎只支持與默認構造函數類似的函數。WinRT RoActivateInstance 對這樣的困境也絲毫沒有影響。那么,具體的工作方式如何呢?
實際上,并不像第一次聽起來那樣不協調。COM 激活模型已經提供了支持高級類型系統所需的基本抽象層。它只是缺少向使用者進行描述所需的元數據。COM 激活模型定義了類對象和實例。某類的實例從不直接由使用者進行創建。即使應用開發者調用 CoCreateInstance 創建類的某個實例,操作系統也仍必須獲取類對象,才能檢索所需的實例。
雖然 Windows 運行時按新名稱調用類對象,但運作方式是一樣的。激活工廠是使用者調用組件的第一個端口。組件會導出 DllGetActivationFactory 函數,而不是實現 DllGetClassObject。雖然類的標識方式已改變,但結果只是一個可能已針對給定類進一步查詢過的對象。傳統的類對象(以下簡稱“激活工廠”)可能已實現了 IClassFactory 接口,便于調用方創建上述類的實例。在新的體系中,Windows 運行時中的激活工廠必須實現新的 IActivationFactory 接口,這可有效地提供同一服務。
正如我在我的上兩期專欄中所探討的一樣,Windows 運行時中使用可激活的特性修飾的運行時類能夠生成元數據來指示語言投影允許默認構造。其假定是,特性類的激活工廠將通過 IActivationFactory 的 ActivateInstance 方法提供此類默認構造對象。下面用 IDL 描述了一個簡單的運行時類,這是一個具有默認構造函數的類:
~~~
[version(1)]
[activatable(1)]
runtimeclass Hen
{
? [default] interface IHen;
}
~~~
其他構造函數也可以用 IDL 進行描述,其中包含封裝了一系列方法的接口(如您所猜),這些方法代表了具有不同參數集的其他構造函數。就像 IActivationFactory 接口定義了默認構造一樣,組件接口和類接口可能描述了參數化構造。下面是一個簡單的工廠接口,可用于創建發出特定次數的咯咯聲的 Hen 對象:
~~~
runtimeclass Hen;
[version(1)]
[uuid(4fa3a693-6284-4359-802c-5c05afa6e65d)]
interface IHenFactory : IInspectable
{
? HRESULT CreateHenWithClucks([in] int clucks,
????????????????????????????? [out,retval] Hen ** hen);
}
~~~
與 IActivationFactory 類似,IHenFactory 接口必須毫無理由地直接繼承自 IInspectable。然后,此工廠接口的每個方法會投影為具有給定參數的其他構造函數,每個方法最終必須具有與類同類型的邏輯返回值。接下來,通過指明接口名稱的其他可激活特性,明確關聯此工廠接口和運行時類:
~~~
[version(1)]
[activatable(1)]
[activatable(IHenFactory, 1)]
runtimeclass Hen
{
? [default] interface IHen;
}
~~~
如果默認構造函數對您的類沒有任何意義,那么您只需省略原始可激活特性即可,類的語言投影同樣也會缺少默認構造函數。如果我寄送我的一個禽類組件版本,隨后意識到它不具備使用大區創建母雞的功能,該怎么辦? 我現在當然不能更改已寄送給客戶的 IHenFactory 接口,因為 COM 接口始終必須采用二進制語義協定。沒關系,我只需定義另一個封裝了其他所有構造函數的接口即可:
~~~
[version(2)]
[uuid(9fc40b45-784b-4961-bc6b-0f5802a4a86d)]
interface IHenFactory2 : IInspectable
{
? HRESULT CreateHenWithLargeComb([in] float width,
???????????????????????????????? [in] float height,
???????????????????????????????? [out, retval] Hen ** hen);
}
~~~
然后,我可以將這第二個工廠接口與 Hen 類相關聯(和先前一樣):
~~~
[version(1)]
[activatable(1)]
[activatable(IHenFactory, 1)]
[activatable(IHenFactory2, 2)]
runtimeclass Hen
{
? [default] interface IHen;
}
~~~
語言投影負責合并,應用開發者只是看到大量的構造函數重載,如圖 1?所示。
?
圖 1:C# 中的 IDL 定義工廠方法
組件開發者會謹慎地實現這些工廠接口,以及必備的 IActivationFactory 接口:
~~~
struct HenFactory : Implements<IActivationFactory,
?????????????????????????????? ABI::Sample::IHenFactory,
?????????????????????????????? ABI::Sample::IHenFactory2>
{
};
~~~
此時,我再次依賴的是我的 2014 年 12 月專欄 ([msdn.microsoft.com/magazine/dn879357](https://msdn.microsoft.com/magazine/dn879357)) 中介紹的“實現類”模板。即使我選擇禁用默認構造,母雞的激活工廠也仍需實現 IActivationFactory 接口,因為組件必須實現的 DllGetActivationFactory 函數已經過硬編碼,可以返回 IActivationFactory 接口。也就是說,如果應用需要的不是默認構造,則語言投影必須先在生成的指針上調用 QueryInterface。仍必須實現 IActivationFactory 的 ActivateInstance 虛擬函數,但 no-op(如下所示)就足夠了:
~~~
virtual HRESULT __stdcall ActivateInstance(IInspectable ** instance) noexcept override
{
? *instance = nullptr;
? return E_NOTIMPL;
}
~~~
可以按照任意方式來實現其他構造方法,只要對給定實現有意義即可,而簡單的解決方案是只需將自變量轉發到內部類本身即可。類似如下輸出:
~~~
virtual HRESULT __stdcall CreateHenWithClucks(int clucks,
????????????????????????????????????????????? ABI::Sample::IHen ** hen) noexcept override
{
? *hen = new (std::nothrow) Hen(clucks);
? return *hen ? S_OK : E_OUTOFMEMORY;
}
~~~
此操作假定 C++ Hen 類不包含引發構造函數。我可能需要在這個構造函數中分配一個 C++ cluck 向量。在這種情況下,使用簡單的異常處理程序就足夠了:
~~~
try
{
? *hen = new Hen(clucks);
? return S_OK;
}
catch (std::bad_alloc const &)
{
? *hen = nullptr;
? return E_OUTOFMEMORY;
}
~~~
靜態類成員怎么樣? 這也是使用 COM 接口在激活工廠上進行實現,對此您不應該感到驚訝。回到 IDL,我可以定義一個接口來封裝所有的靜態成員。能不能報告后院中產蛋雞的數量:
~~~
[version(1)]
[uuid(60086441-fcbb-4c42-b775-88832cb19954)]
interface IHenStatics : IInspectable
{
? [propget] HRESULT Layers([out, retval] int * count);
}
~~~
然后,我需要將此接口與具有靜態特性的同一運行時類相關聯:
~~~
[version(1)]
[activatable(1)]
[activatable(IHenFactory, 1)]
[activatable(IHenFactory2, 2)]
[static(IHenStatics, 1)]
runtimeclass Hen
{
? [default] interface IHen;
}
~~~
C++ 實現同樣也相當簡單。我將按以下方式更新“實現可變參數”模板:
~~~
struct HenFactory : Implements<IActivationFactory,
?????????????????????????????? ABI::Sample::IHenFactory,
?????????????????????????????? ABI::Sample::IHenFactory2,
?????????????????????????????? ABI::Sample::IHenStatics>
{
};
~~~
同時,按照適當的方式實現 get_Layers 虛擬函數:
~~~
virtual HRESULT __stdcall get_Layers(int * count) noexcept override
{
? *count = 123;
? return S_OK;
}
~~~
我將把更準確地統計頭數的任務留給大家...我是指母雞計數。
對于使用者而言,在應用內,所有內容都變得十分簡單明了。如圖 1?所示,IntelliSense 體驗相當有幫助。我能夠使用 CreateHenWithClucks 構造函數,就像是使用 C# 類上的其他構造函數一樣:
~~~
Sample.Hen hen = new Sample.Hen(123); // Clucks
~~~
當然,為了讓此函數運行起來,C# 編譯器和公共語言運行時 (CLR) 有大量的工作要做。在運行時,CLR 會調用 RoGetActivationFactory,這會依次調用 LoadLibrary 和 DllGetActivationFactory 來檢索 Sample.Hen 激活工廠。然后,它會調用 QueryInterface 來檢索工廠的 IHenFactory 接口實現,并且只有在此之后,它才能調用 CreateHenWithClucks 虛擬函數來創建 Hen 對象。更不用說 CLR 堅持的對 QueryInterface 的許多額外調用,因為它促使進入 CLR 的每個對象發現各種特性和上述對象的語義。
調用靜態成員同樣也很簡單:
~~~
int layers = Sample.Hen.Layers;
~~~
不過,在這里再次強調一下,CLR 必須調用 RoGetActivationFactory 才能獲取激活工廠,后面需要調用 QueryInterface 檢索 IHenStatics 接口指針,然后才能調用 get_Layer 虛擬函數檢索此靜態屬性的值。然而,CLR 確實會緩存激活工廠,因此成本多少會分攤到多次此類調用上。另一方面,它還會盡各種努力嘗試在組件內緩存工廠,確切地說,這毫無意義,并會帶來不必要的復雜性。但這是下次要討論的主題了。下個月請與我一起繼續探討 C++ 中的 Windows 運行時。
* * *
Kenny Kerr?*是加拿大的計算機程序員,是 Pluralsight 的作者,也是一名 Microsoft MVP。他的博客網址是?[kennykerr.ca](http://kennykerr.ca/),您可以通過 Twitter?[twitter.com/kennykerr](http://twitter.com/kennykerr)?關注他。*
- 介紹
- 云連接移動應用 - 借助身份驗證和離線支持構建 Xamarin 應用
- 崛起 - 自由 Internet 廣播
- Microsoft Azure - 云中的容錯問題和解決方法
- 最前沿 - 適合常見應用程序的事件源
- Azure 深入了解 - 跨云平臺創建統一的 Heroku 式工作流
- 借助 C++ 進行 Windows 開發 - Windows 運行時中的高級類型
- 編譯器優化 - 借助按本機配置優化來簡化代碼
- 數據點 - 再探 JavaScript 數據綁定(現在包含 Aurelia)
- 云安全 - 借助 Azure 密鑰保管庫保護敏感信息的安全
- 測試運行 - 借助人工尖峰神經元進行計算
- 開發運營 - 在 Microsoft 堆棧上啟用開發運營
- 孜孜不倦的程序員 - 如何成為 MEAN: Node.js
- 新型應用 - 提升新型應用的易用性的做法
- 別讓我打開話匣子 - Darwin 的照相機
- 編輯寄語 - 汽車 Internet 發生故障