到 2015 2015年 7 月

30 卷數 7

Windows 使用 c + +-Windows 運行時元件

Kenny Kerr |到 2015 2015年 7 月

Kenny Kerr未來幾個月我要去探索 Windows 運行時的要點。目的是打破開發人員使用的各種語言預測和工具鏈以檢查 Windows 運行時在應用程式二進位介面 (ABI) 是如何工作的較高級別的抽象 — — 應用程式與它們是靠訪問作業系統服務的二進位元件的邊界。

在某些方面 Windows 運行時是只是 COM,並有效地重用代碼的二進位標準仍然是一種流行的方式來構建複雜的應用程式和作業系統元件的演變。與 COM 不同,然而,Windows 運行時更集中和主要用作 Windows API 的基礎。應用程式開發人員會更傾向于使用 Windows 運行時作為一個消費者的作業系統元件和編寫元件不太可能自己承擔。然而,所有不同的優雅抽象介面的實現和投射到各種程式設計語言的良好理解只可以説明您編寫更高效的應用程式和更好地診斷互通性和性能問題。

為什麼這麼少的開發人員瞭解 Windows 運行時是如何工作 (其他比其相當稀疏的文檔) 的原因之一是因為模具和語言的預測真的掩蓋的基礎平臺。這可能是自然的 C# 開發人員,但它當然不會令生活舒適為 c + + 開發真的想要知道怎麼回事的被窩。因此,讓我們開始通過使用Visual Studio2015年開發人員命令提示符的標準 c + + 編寫一個簡單的 Windows 運行時元件。

我將開始與出口兩個函數的簡單和傳統的 DLL。如果你想要跟著,創建一個示例資料夾,裡面創建以 Sample.cpp 開頭的幾個原始程式碼檔:

C:\Sample>notepad Sample.cpp

我要做的第一件事是照顧卸載 DLL,我會從這裡調用元件。該元件應支援卸載查詢通過匯出的函式呼叫,DllCanUnloadNow,和它是應用程式控制卸載以 CoFreeUnused­庫函數。我不會花很多時間在這,因為這是以同樣的方式,元件被卸載經典 com。因為元件不靜態連結到應用程式 — — 與一個 LIB 檔,例如 — — 而是載入動態通過裝載­庫函數,需要有某種方式為最終要卸載的元件。只有元件真的知道多少未完成的引用被關押所以 COM 運行庫可調用其 DllCanUnloadNow 函數,以確定它是否是安全卸載。應用程式也可以執行這個管家自己使用 CoFreeUnusedLibraries 或 CoFreeUnusedLibrariesEx 函數。在元件中的實現非常簡單。我需要一種鎖,將跟蹤的多少個物件還活著:

static long s_lock;

每個物件可以簡單地增加這種鎖在其建構函式中和遞減在其析構函數中。保持簡單,我會寫一個小的 ComponentLock 類:

struct ComponentLock
{
  ComponentLock() noexcept
  {
    InterlockedIncrement(&s_lock);
  }
  ~ComponentLock() noexcept
  {
    InterlockedDecrement(&s_lock);
  }
};

然後應防止卸載該元件的任何和所有物件可以簡單地作為成員變數都嵌入 ComponentLock。DllCanUnloadNow 功能現在可以實現非常簡單:

HRESULT __stdcall DllCanUnloadNow()
{
  return s_lock ? S_FALSE : S_OK;
}

真的有兩種類型的物件,您可以在元件中創建 — — 啟動工廠,被稱為經典的 COM,和一些特定的類的實例的類工廠。我要去實現一個簡單的"母雞"類,我將開始定義 IHen 介面,所以母雞可以咯咯:

struct __declspec(uuid("28a414b9-7553-433f-aae6-a072afe5cebd")) __declspec(novtable)
IHen : IInspectable
{
  virtual HRESULT __stdcall Cluck() = 0;
};

這是週期性 COM 介面,只是碰巧獲得 IInspectable 而不是直接從 IUnknown。然後,我可以使用 2014 年 12 月問題所述的實現類範本 (msdn.com/magazine/dn879357) 來實現此介面,並提供實際類的實現的母雞在元件內:

struct Hen : Implements<IHen>
{
  ComponentLock m_lock;
  virtual HRESULT __stdcall Cluck() noexcept override
  {
    return S_OK;
  }
};

啟動工廠只是一個 c + + 類實現 IActivationFactory 介面。此 IActivationFactory 介面提供單一的 ActivateInstance 方法,類似于經典的 COM IClassFactory 介面和 CreateInstance 方法。經典的 COM 介面是實際上略優於,它允許調用方請求一個特定的介面直接,而 Windows 運行時 IActivationFactory 只是返回一個 IInspectable 介面指標。然後應用程式負責調用檢索更有用的介面到物件的 IUnknown 調用方法。不管怎麼說,它使啟動­實例方法實現相當簡單:

struct HenFactory : Implements<IActivationFactory>
{
  ComponentLock m_lock;
  virtual HRESULT __stdcall ActivateInstance(IInspectable ** instance)
    noexcept override
  {
    *instance = new (std::nothrow) Hen;
    return *instance ? S_OK : E_OUTOFMEMORY;
  }
};

元件允許應用程式檢索特定啟動廠通過匯出另一個函式呼叫 DllGetActivation­工廠。再次,這類似于 DllGetClassObject 匯出函數支援 COM 啟動模型。主要的區別是所需的類指定一個字串,而不是 GUID:

HRESULT __stdcall DllGetActivationFactory(HSTRING classId,
   IActivationFactory ** factory) noexcept
{
}

HSTRING 是代表一個永恆不變的字串值的控制碼。這類識別碼,也許"Sample.Hen",並指示應返回哪個啟動工廠。在這一點上有大量的原因為什麼對 DllGetActivationFactory 的調用可能會失敗,所以我會開始通過清除 nullptr 廠變數:

*factory = nullptr;

現在我需要支援緩衝區獲取 HSTRING 類識別碼:

wchar_t const * const expected = WindowsGetStringRawBuffer(classId, nullptr);

我可以將此值與所有我的元件發生執行的類進行比較。到目前為止只有一點:

if (0 == wcscmp(expected, L"Sample.Hen"))
{
  *factory = new (std::nothrow) HenFactory;
  return *factory ? S_OK : E_OUTOFMEMORY;
}

否則,我會返回 HRESULT 指示請求的類不可用:

return CLASS_E_CLASSNOTAVAILABLE;

這就是我需要拿到這個簡單元件和運行的所有 c + + 卻仍然有點更多的工作要做,實際上使該元件 DLL,然後向那些討厭 C# 編譯器不知道如何解析標頭檔描述它。若要使一個 DLL,我需要參與連結器,特別是它能夠定義從 DLL 匯出的函數。我可以使用 Microsoft 編譯器特定 dllexport __declspec 說明符,但這是一個罕見的情況下,哪來直接跟連結器和相反的出口清單提供的模組定義檔。我發現這種方法更少的錯誤傾向。所以它是追溯到第二個原始檔案的主控台:

C:\Sample>notepad Sample.def

此 DEF 檔只是需要一個稱為出口的部分,列出了要匯出的函數:

EXPORTS
DllCanUnloadNow         PRIVATE
DllGetActivationFactory PRIVATE

我現在可以提供 c + + 原始檔案和此模組-­定義檔的編譯器和連結器生成 dll 檔,然後使用一個簡單的批次檔作為一種方便生成元件並將所有生成工件放置在子資料夾中:

C:\Sample>type Build.bat
@md Build 2>nul
cl Sample.cpp /nologo /W4 /FoBuild\ /FeBuild\Sample.dll /link /dll /def:Sample.def

我會輕輕帶過深的魔術,這是批次檔指令碼語言和重點放在 Visual c + + 編譯器選項。/Nologo 選項取消顯示版權標誌。該選項也轉發給連結器。不可或缺的 /W4 選項告訴編譯器要顯示更多的警告,為常見的編碼錯誤。還有沒有 /FoBuild 選項。編譯器已藉以輸出路徑遵循的選項,在本例中 /Fo,沒有空間,在這兩者之間本難讀公約。無論如何,/Fo 選項用來要脅編譯器將轉儲目的檔生成子資料夾中。它是不會預設為用鐵基選項定義的可執行檔相同的輸出檔案夾中的唯一的生成輸出。/Link 選項告訴編譯器後續參數則由連結器的解釋。這樣就不必調用第二個步驟連結器和與不同的編譯器,連結器選項不區分大小寫和做雇用為例 /def 選項,以指示要使用的模組定義檔的選項的名稱和任何值之間的分隔符號。

簡單地說現在可以我的元件和由此產生的生成子資料夾包含大量檔,唯一的哪些事項。自然,這是 Sample.dll 可執行檔,可以載入到應用程式的位址空間。但這還不夠。應用程式開發人員需要知道該元件包含一些方式。C + + 開發人員可能會滿意,與包括 IHen 介面,一個標頭檔,但即使這並不是特別方便。Windows 運行時包括語言預測,藉以描述一個元件的概念在不同的語言可以發現和專案及其類型到其程式設計模型的方式。讓我們,我將探討語言投影在未來幾個月,但是現在只是得到此示例工作從一個 C# 應用程式,因為這是最令人信服。如前所述,C# 編譯器不知道如何解析 c + + 標頭檔,所以我需要提供的 C# 編譯器將快樂一些中繼資料。我需要產生一個包含描述我的元件的 CLR 中繼資料的 WINMD 檔。這是不簡單的事,因為我可能用元件的 ABI 的本機類型可以經常時看起來非常不同投影到 C#。幸運的是,微軟 IDL 編譯器已改作他用來產生一個 WINMD 檔,給出了一個 IDL 檔,使用幾個新的關鍵字。所以它是回我們第三次的原始檔案的主控台:

C:\Sample>notepad Sample.idl

首先,我需要導入系統必備的 IInspectable 介面的定義:

import "inspectable.idl";

然後,我可以定義元件的類型的命名空間。它必須匹配元件本身的名稱:

namespace Sample
{
}

現在我需要定義我以前在 c + +,但這一次作為一個 IDL 介面中定義的 IHen 介面:

[version(1)]
[uuid(28a414b9-7553-433f-aae6-a072afe5cebd)]
interface IHen : IInspectable
{
  HRESULT Cluck();
}

這是好的老 IDL,如果您已經在過去使用 IDL 定義 COM 元件,你應當不會感到任何的這。所有 Windows 運行時類型必須然而,都定義一個版本屬性。這用來是可選的。所有介面必須也都源自 IInspectable 直接。在 Windows 運行時有有效沒有介面繼承。這有一些消極的後果,在未來幾個月再說。

並且,最後,我需要先界定母雞類本身使用新的 runtimeclass 關鍵字:

[version(1)]
[activatable(1)]
runtimeclass Hen
{
  [default] interface IHen;
}

再次,版本屬性是必需的。雖然不要求,啟動的屬性,指示此類可能被啟動。在這種情況下,它指示預設啟動通過 IActivationFactory ActivateInstance 方法支援。語言投影應提出,作為一個 c + + 或 C# 的預設建構函式或任何有意義到特定語言。最後,預設屬性之前介面關鍵字指示 IHen 介面是母雞類的預設介面。預設介面是介面發生參數和返回類型,當這些類型指定類本身。因為 ABI 只交易中的 COM 介面和母雞類本身並不是一個介面、 預設介面是其 ABI 一級的代表。

還有很多在這裡探討,但這將做一會兒。現在,我可以更新我的批次檔來生成一個描述我的元件的 WINMD 檔:

@md Build 2>nul
cl Sample.cpp /nologo /W4 /FoBuild\ /FeBuild\Sample.dll /link /dll /def:Sample.def
"C:\Program Files (x86)\Windows Kits\10\bin\x86\midl.exe" /nologo /winrt /out %~dp0Build /metadata_dir "c:\Program Files (x86)\Windows Kits\10\References\Windows.Foundation.FoundationContract\1.0.0.0" Sample.idl

我會再掩飾批次檔並集中研究新與 MIDL 編譯器選項是什麼魔法。/Winrt 選項是關鍵,表示 IDL 檔包含 Windows 運行時類型,而不是傳統的 COM 或 RPC 樣式的介面定義。/Out 選項只是確保 WINMD 檔駐留在相同的資料夾中的 DLL,因為這由 C# 工具鏈所必需。/Metadata_dir 選項告訴編譯器它在哪裡可以找到用於生成 OS 的中繼資料。我寫這篇文章的時候,Windows 10Windows SDK仍定居下來,我需要要小心調用 MIDL 編譯器附帶Windows SDK並不是一個由Visual Studio工具命令提示符中的路徑。

現在運行批次檔生成 Sample.dll 和 Sample.winmd,我然後可以從 C# Windows 通用應用程式和使用的母雞類引用,就好像它只是另一個 CLR 類庫專案:

Sample.Hen h = new Sample.Hen();
h.Cluck();

Windows 運行時是在 COM 和標準 c + + 基礎上建立的。我們作出讓步,以支援 CLR 和輕鬆為 C# 開發人員能夠使用新的 Windows API 而不需要任何交互操作的元件。Windows 運行時是 Windows API 的未來。

我專門提出開發 Windows 運行時元件的經典 COM 角度和它的根從 c + + 編譯器中,以便你能理解這種技術來自何方。然而,這種方法很快變得相當不切實際。MIDL 編譯器實際上提供遠不止是 WINMD 檔,我們可以實際使用它,除其他外,在 c + + 生成的 IHen 介面的正常化版本。我希望你能加入我下個月,我們探索 Windows 運行時元件創作更可靠的工作流,也解決了沿途的幾個交互操作問題。


Kenny Kerr 是設在加拿大,以及作者 Pluralsight 和微軟最有價值球員的電腦程式師。在他博客 kennykerr.ca,你可以跟著他在 Twitter 上 twitter.com/kennykerr

感謝以下的微軟技術專家對本文的審閱:拉裡 · 奧斯特曼