Windows 與 C++

Windows 執行階段應用程式模型

Kenny Kerr

Kenny Kerr我們的生活都充滿了抽象。 作為開發人員,我們經常留下當我們使用抽象不理解他們的苦苦掙扎。 抽象有時破碎並未能完全隱藏底層的複雜性。 Don沒誤會我,抽象是偉大的。 他們説明使用者和他們説明開發人員,但你做你自己世界的好如果你挖到你依靠定期瞭解他們是如何運作的抽象。 此外,圖書館,承認這一現實往往更加成功比不,部分是因為它們允許您周圍的抽象步如果你覺得有必要。

Windows 運行時 (WinRT) 是一種抽象,並在本月的專欄中我要說明這一點通過檢查 WinRT 核心應用程式模型。它圍繞的 CoreWindow 類,其中有生活裡面每個"現代的"Windows 應用商店和 Windows Phone 應用程式的實例。然而,相對較少的開發人員都知道它的存在,更不用說它是如何工作。也許這就是成功的抽象的證明。

2011 年首次公佈了 Windows 8 的 API,因為很多一直講了話,寫了關於提供一個抽象的概念,在 Windows 運行時的各種語言預測。然而,瞭解 Windows 運行時的最佳方法是避免各種語言預測,包括 C + + / CX,和擁抱標準 c + + 和 com 的經典。只有 c + + 允許您拉開帷幕放在一邊,看看什麼真的 (技術上,所以不會 C,但那會無謂地痛苦)。您仍然可以選擇使用一些或其他語言中的投影 (但願 C + + / CX),你大概應該,但至少你會更清楚地理解什麼真的。

首先,打開Visual Studio2012年並創建一個新的 Visual c + + 專案為 Windows 應用商店或 Windows Phone 的 app。不管您使用的範本。一旦載入了它,走到解決方案資源管理器和刪除一切都不重要。如果你選了一個基於 XAML 範本,刪除所有的 XAML 檔。您還可以刪除所有 c + + 的來原始檔案。你可能想要堅持的預編譯頭,但請務必刪除裡面的所有內容。應保持都部署應用程式所需的一攬子資產、 圖像、 證書和 XML 清單。

接下來,打開專案的屬性頁,並選擇編譯器屬性 — — C/c + + 在左側樹中的節點。找到 /ZW 編譯器選項的叫做消耗 Windows 運行時擴展的行,選擇否以禁用 C + + / CX 的語言擴展。這種方式,你可以肯定沒有什麼神秘超越了標準 c + + 編譯器的神奇奧秘。你在那兒,以及可能編譯器警告級別設置為 /W4。

如果您嘗試編譯該專案,您應該會看到通知您找不到該專案的 WinMain 進入點函數的連結器錯誤。將一個新的 c + + 原始檔案添加到專案中,和你要做的第一件是添加缺少的 WinMain 函數:

int __stdcall wWinMain(HINSTANCE, HINSTANCE, PWSTR, int)
{
}

正如你所看到的這是古老的 WinMain 函數為 C 運行時庫 CRT 基於 Windows 的應用程式。 當然,HINSTANCE 和 PWSTR 並不是基本的 c + + 類型,所以你需要包含 Windows 頭:

#include <windows.h>

如果你保持專案的預編譯的頭,你可以在裡面包括。 也可以使用 ComPtr 從 Windows 運行 c + + 範本庫 (WRL),所以現在將是好時機,以及包括:

#include <wrl.h>

我掩護 WRL 中更詳細地在接下來的幾列。 現在,我只會讓使用的 ComPtr 為維護一個 COM 介面指標的類範本。 所有您需要牢記這一階段是 WRL ComPtr 只是 COM 介面智慧指標。 雖然它提供了對 Windows 運行時是唯一的某些功能,但我不會使用他們在本月的專欄中。 相反,可以很容易地使用活動範本庫 (ATL) CComPtr 或您選擇的任何 COM 介面智慧指標。 WRL ComPtr Microsoft::WRL 命名空間中定義:

using namespace Microsoft::WRL;

我也要使用 ASSERT 宏,以及人力資源管理功能進行錯誤處理。 我前面討論過這些,所以我不會再對他們在這裡。 如果你不能確定這些步驟,檢查出我 2013 年 5 月的專欄文章,"引進 Direct2D 1.1"(msdn.microsoft.com/magazine/dn198239)。

最後,使用任何在此專欄中提到的 WinRT 功能,您需要給連結器的.lib 檔的名稱:

#pragma comment(lib, "RuntimeObject.lib")

應用程式模型預計,第一件事是多執行緒的單元 (MTA)。 沒錯 — — COM 單元模型的生活。 Windows 運行時提供的 RoInitialize 函數,這是 CoInitializeEx 的薄包裝:

HR(RoInitialize(RO_INIT_MULTITHREADED));

儘管事實上,CoInitializeEx 通常是充足的我建議你使用 RoInitialize。 此功能允許將來的改善措施到 Windows 運行時不可能打破經典 com。 它是 OleInitialize,這也叫 CoInitializeEx,然後有些類似。 點是什麼神秘的事您應用程式的主執行緒。 唯一可能稍微有點奇怪的是它不是單線程單元 (STA)。 Don擔心,您的應用程式視窗將仍然從內部運行 STA 執行緒,但 Windows 運行時將創建它。 此 STA 是實際應用 STA (ASTA),這是稍有不同,但在那以後更多。

下一位是有點棘手。 Windows 運行時姐弟傳統 COM 啟動使用模型,有利於啟動基於文本的類識別碼的類的模型基於 GUID 的類識別碼。 文本名稱基於JAVA和 Microsoft.NET 框架中,通過推廣範圍的命名空間的類名稱,但你嘲笑和說再見到註冊表之前,請牢記這些新的類識別碼仍然存儲在註冊表中。 從技術上講只有第一方類型登記在註冊表中,而在每個應用程式清單中只有註冊協力廠商類型。 有對這種改變的利弊。 缺點之一是它是稍硬時調用 WinRT 函數描述的類識別碼。 Windows 運行時定義一個新的可遠端處理的字串類型,以取代傳統的 BSTR 字串類型,和任何類識別碼必須提供使用這種新的媒介。 HSTRING,被稱為比遠小於出錯 BSTR,主要是因為它是不可變的。 最簡單的方式創建的 HSTRING 與 WindowsCreateString 函數是:

wchar_t buffer[] = L"Poultry.Hatchery";
HSTRING string;
HR(WindowsCreateString(buffer,
                       _countof(buffer) - 1,
                       &string));

WindowsCreateString 分配足夠的記憶體來存儲拷貝的源字串,以及終止 null 字元,然後將源字串複製到這個緩衝區。 要釋放的後備緩衝區,你一定要記得叫 WindowsDeleteString,除非向調用函數返回字串的擁有權:

HR(WindowsDeleteString(string));

鑒於 HSTRING,可以得到對其後備緩衝區指標用 WindowsGetStringRawBuffer 函數:

wchar_t const * raw = WindowsGetStringRawBuffer(string, nullptr);

可選的第二個參數,返回的字串的長度。 長度的字串,保存你從不必掃描以確定其長度的字串與存儲。 你跑去和編寫 c + + 包裝類之前,這是值得關注的 C + + / CX 編譯器不會困擾與 WindowsCreateString 和 WindowsDelete­字串生成一個字串字面或 const 陣列的代碼時。 相反,它使用被稱為快速傳遞字串,以避免額外的記憶體分配和我先前提到的副本。 這樣還可以避免記憶體洩漏的風險。 WindowsCreate­StringReference 函數創建快速傳遞的字串:

HSTRING_HEADER header;
HSTRING string;
HR(WindowsCreateStringReference(buffer,
                                _countof(buffer) - 1,
                                &header,
                                &string));

此函數使用調用方提供的 HSTRING_HEADER 來避免堆分配。 在這種情況下,HSTRING 的後備緩衝區是源字串本身,因此您必須確保源字串 (以及標頭) 仍然是不變的生活 HSTRING。 當您需要將字串返回給調用函數,但它是值得優化的時候您需要傳遞一個字串作為輸入到另一個函數,其存留期的作用範圍是由棧 (不非同步函數),這種做法很明顯不是任何使用。 WRL 還提供包裝為 HSTRING 和快速傳遞的字串。

RoGetActivationFactory 只是這種功能,用於獲取啟動工廠或靜態介面為給定的類。 這是類似于 COM CoGetClassObject 函數。 考慮到通常帶有 const 陣列由 MIDL 編譯器生成使用此函數,它意義寫一個簡單的函數範本提供快速傳遞字串的包裝。 圖 1 說明了這可能會是什麼樣子。

圖 1 GetActivationFactory 函數範本

template <typename T, unsigned Count>
auto GetActivationFactory(WCHAR const (&classId)[Count]) -> ComPtr<T>
{
  HSTRING_HEADER header;
  HSTRING string;
  HR(WindowsCreateStringReference(classId,
                                  Count - 1,
                                  &header,
                                  &string));
  ComPtr<T> result;
  HR(RoGetActivationFactory(string,
    __uuidof(T),
    reinterpret_cast<void **>(result.GetAddressOf())));
  return result;
}

GetActivationFactory 函數範本推斷的字串長度自動消除易出錯的緩衝區長度參數或代價高昂的運行時掃描。 它然後在調用實際的 RoGetActivationFactory 函數之前準備快速傳遞的字串。 在這裡,再次,函數範本推斷的介面識別碼和安全地返回生成的 COM 介面指標 WRL ComPtr 裹。

您現在可以使用此 helper 函數獲取的 ICoreApplication 介面:

using namespace ABI::Windows::ApplicationModel::Core;
auto app = GetActivationFactory<ICoreApplication>(
  RuntimeClass_Windows_ApplicationModel_Core_CoreApplication);

ICoreApplication 的介面是什麼獲取球滾動您的應用程式。 若要使用此 COM 介面,您需要包括應用程式模型頭:

#include <Windows.ApplicationModel.Core.h>

此標頭內的 ABI::Windows::ApplicationModel::Core 命名空間,以及核心定義 ICoreApplication,­應用程式的文本類識別碼。 你真的需要想的唯一介面方法是運行的方法。

我走前,它有説明感激 Windows 運行時如何將您的應用程式的生命。 正如我前面提到過,Windows 運行時認為你只是客串在您自己的進程內。 這是類似于 Windows 服務如何工作年。 在 Windows 服務中,Windows 服務控制管理員 (SCM) 啟動服務使用 CreateProcess 函數,或其變種之一。 然後,它等待的進程調用 StartServiceCtrlDispatcher 函數。 此函數建立連接回藉以可以溝通的服務和供應鏈管理,供應鏈管理。 如果,例如,該服務無法調用 StartService­CtrlDispatcher 及時,SCM 將承擔出事和拆除過程。 StartServiceCtrl­調度員函數將只返回時該服務已經結束,所以單片機需要創建輔助執行緒服務接收回調通知。 服務只對事件做出回應,並是 SCM 的擺佈。 你會發現,這是非常相似的 WinRT 應用程式模型。

運行時 Windows 等待進程獲取核心­應用程式介面和調用 Run 方法。 像單片機,如果進程未能做到及時,Windows 運行時假定什麼差錯和關閉進程。 值得慶倖的是,如果附加調試器,則 Windows 運行時通告和禁用的超時時間,與不同的供應鏈管理。 然而,該模型是相同的。 Windows 運行時電荷中和在運行時創建的執行緒上調用的應用程式,當事件發生時。 當然,Windows 運行時是基於 COM 的所以不要一個回呼函數 (如是 SCM 的情況) Windows 運行時 — —,這依賴于進程存留期管理器 (PLM) 上 — — 預計將應用程式與 COM 介面,它可以使用調用應用程式提供的運行方法。

您的應用程式必須提供實現的 IFramework­ViewSource,其中還讚揚從 ABI::Windows::ApplicationModel::Core 命名空間,和 CoreApplication 將調用其孤立的 CreateView 方法,一旦它創建應用程式的 UI 執行緒。 IFrameworkViewSource 真的只是沒有 CreateView 作為一種方法。 它源自 IInspectable,WinRT 的基介面。 IInspectable 反過來從 IUnknown,COM 基介面派生。

WRL 執行 COM 類,提供廣泛的支援,但我會存,為即將到來的列。 現在,我要強調如何 Windows 運行時真的植根于 COM,和什麼好方式,通過實施 IUnknown 表明比嗎? 對於我而言是有益的注意 c + + 類,將實現 IFrameworkViewSource — — 和其他幾個國家 — — 已堆疊由定義它的存留期。 本質上,應用程式的 WinMain 函數歸結于此:

HR(RoInitialize(RO_INIT_MULTITHREADED));
auto app = GetActivationFactory<ICoreApplication>( ...
SampleWindow window;
HR(app->Run(&window));

就是要寫的 SampleWindow 類,以便它正確地實現了 IFrameworkViewSource。 雖然 CoreApplication 不在意他們的實行,至少你的應用程式將需要實現不僅 IFrameworkViewSource IFrameworkView 和 IActivatedEventHandler 的介面。 在這種情況下,SampleWindow 類就可以實現他們所有:

struct SampleWindow :
  IFrameworkViewSource,
  IFrameworkView,
  IActivatedEventHandler
{};

IFrameworkView 介面也定義在 ABI::Windows::ApplicationModel::Core 命名空間,但 IActivatedEvent­處理常式是有點難度來牽制。 我已經把它定義自己,如下所示:

using namespace ABI::Windows::Foundation;
using namespace ABI::Windows::ApplicationModel::Activation;
typedef ITypedEventHandler<CoreApplicationView *, 
  IActivatedEventArgs *>
  IActivatedEventHandler;

如果您有一些 COM 的經驗,你可能會覺得這看起來是相當非正統 — — 你會正確。 正如你所期望的 ITypedEventHandler 是只是一個類範本,那是相當奇怪的方式來定義 COM 介面 — — 最明顯的問題是你不可能知道什麼介面識別碼屬性與它。 幸運的是,所有這些介面由 MIDL 編譯器,其中照顧,專注每一個生成和它對這些專業是它重視表示介面識別碼的 GUID。 作為複雜,可能會出現以前的 typedef,它定義了 COM 介面直接從 IUnknown 派生並提供調用調用單個方法。

我有幾個介面方法執行,所以讓我們開始吧。 首先是 IUnknown 和強大的 QueryInterface 方法。 我不想花太多時間 IUnknown 和 IInspectable 在這裡,因為我會在即將到來的列詳細涵蓋他們。 圖 2 提供 QueryInterface 的一個簡單實現這種基於堆疊類。

圖 2 SampleWindow QueryInterface 方法

auto __stdcall QueryInterface(IID const & id,
                              void ** result) -> HRESULT
{
  ASSERT(result);
  if (id == __uuidof(IFrameworkViewSource) ||
      id == __uuidof(IInspectable) ||
      id == __uuidof(IUnknown))
  {
    *result = static_cast<IFrameworkViewSource *>(this);
  }
  else if (id == __uuidof(IFrameworkView))
  {
    *result = static_cast<IFrameworkView *>(this);
  }
  else if (id == __uuidof(IActivatedEventHandler))
  {
    *result = static_cast<IActivatedEventHandler *>(this);
  }
  else
  {
    *result = nullptr;
    return E_NOINTERFACE;
  }
  // static_cast<IUnknown *>(*result)->AddRef();
  return S_OK;
}

幾件事值得一說此執行。 首先,該方法稱其參數是有效。 更多政治正確的執行可能會返回 E_POINTER,但它假定這種錯誤都可以解決發展過程中,因此無需再浪費額外的週期,在運行時的 bug。 這給的最佳可能的行為立即引起存取違規和很容易分析的損毀傾印。 如果您返回 E_POINTER,破碎的調用方可能只是忽略它。 最好的策略是要儘早失敗。 這其實是很多實現,包括 DirectX 和 Windows 運行時所採取的立場。 正確實施 QueryInterface 進入了很多。 COM 規範是相當具體,以便 COM 類將始終提供某些物件標識保證正確和一致。 Don擔心,如果該鏈的 if 語句看起來令人生畏。 我會在適當的時候介紹它。

值得一提的有關這種實現的最後一點是它不會調用 AddRef。 通常,QueryInterface 必須調用 AddRef 上結果 IUnknown 介面指標返回之前。 然而,因為,SampleWindow 類駐留在堆疊上,沒有點中的引用計數。 出於同樣的原因,實施的 IUnknown AddRef 和釋放的方法非常簡單:

auto __stdcall AddRef()  -> ULONG { return 2; }
auto __stdcall Release() -> ULONG { return 1; }

這些方法的結果只有諮詢,所以你可以利用這一事實,並將做任何非零值。 小心這裡的一個詞:你可能想要重寫新的運營商和刪除,以便明確類只為了在堆疊上的工作。 或者,你可以簡單地實現引用計數,以防萬一。

下一步,我需要實現 IInspectable,但它不會用這個簡單的應用程式,因為我會作弊離開未實現,其方法如圖所示,在圖 3。 這是不符合標準的實現並不能保證工作。 再次,我會掩護 IInspectable 在即將到來的列中,但這是足以起床 SampleWindow IInspectable 派生的介面和運行。

圖 3 SampleWindow IInspectable 方法

auto __stdcall GetIids(ULONG *,
                       IID **) -> HRESULT
{
  return E_NOTIMPL;
}
auto __stdcall GetRuntimeClassName(HSTRING *) -> HRESULT
{
  return E_NOTIMPL;
}
auto __stdcall GetTrustLevel(TrustLevel *) -> HRESULT
{
  return E_NOTIMPL;
}

接下來,我需要執行 IFrameworkViewSource 和 CreateView 方法。 SampleWindow 類也實施了 IFrameworkView,因為執行很簡單。 同樣要注意通常您需要調用 AddRef 上,由此產生的派生 IUnknown 介面指標返回之前。 你可能想要以防萬一調用 AddRef 這個函數體中:

auto __stdcall CreateView(IFrameworkView ** result) -> HRESULT
{
  ASSERT(result);
  *result = this;
  // (*result)->AddRef();
  return S_OK;
}

IFrameworkView 介面是最後變得有趣的應用程式。 調用後 CreateView,要從應用程式檢索的介面指標,Windows 運行時快速連續調用其方法中的大多數。 它是重要的你對這些呼籲迅速作出反應,因為他們都被算作使用者等待您要啟動的應用程式所花費的時間。 第一次被稱為初始化,,這是哪裡的應用程式必須註冊啟動事件。 啟動事件信號已啟動應用程式,但它于啟動其 CoreWindow 的應用程式。 初始化方法是相當簡單的:

auto __stdcall Initialize(ICoreApplicationView * view) -> HRESULT
{
  EventRegistrationToken token;
  HR(view->add_Activated(this, &token));
  return S_OK;
}

然後調用 SetWindow 方法,提供具有 ICoreWindow 的實際實現的應用程式。 ICoreWindow 只是模型裡面,Windows 運行時經常桌面 HWND。 不同于以前的應用程式模型介面,ICoreWindow ABI::Windows::UI::Core 命名空間中定義。 裡面的 SetWindow 方法你應該只是複印的介面指標,當你需要它很快:

using namespace ABI::Windows::UI::Core;
ComPtr<ICoreWindow> m_window;
auto __stdcall SetWindow(ICoreWindow * window) -> HRESULT
{
  m_window = window;
  return S_OK;
}

Load 方法下而且那裡你應該堅持任何及所有的代碼編寫為初始的演示文稿應用程式:

auto __stdcall Load(HSTRING) -> HRESULT
{
  return S_OK;
}

至少,你應該註冊視窗的大小和可見度的變化,以及更改 DPI 縮放與有關的事件。 你可能還借此機會創建各種不同的 DirectX 工廠物件、 負載獨立于設備的資源,等等。 這是個好的地方,這一切的原因是它是在此時向使用者顯示應用程式的初始螢幕。

Load 方法返回時,Windows 運行時假定您的應用程式準備好要啟動和觸發啟動事件,我會通過調用 IActivatedEventHandler 方法實施處理,就像這樣:

auto __stdcall Invoke(ICoreApplicationView *,
                      IActivatedEventArgs *) -> HRESULT
{
  HR(m_window->Activate());
  return S_OK;
}

與啟動的視窗,應用程式是終於準備好運行:

auto __stdcall Run() -> HRESULT
{
  ComPtr<ICoreDispatcher> dispatcher;
  HR(m_window->get_Dispatcher(dispatcher.GetAddressOf()));
  HR(dispatcher->ProcessEvents(CoreProcessEventsOption_ProcessUntilQuit));
  return S_OK;
}

有許多方式來實現此。 在這裡我只檢索視窗的 ICoreDispatcher 介面,它表示該視窗的消息泵。 最後,那裡是 Uninitialize 的方法,可能偶爾會調用,但否則無用,可以安全地忽略:

auto __stdcall Uninitialize() -> HRESULT
{
  return S_OK;
}

這樣就行了。 您現在可以編譯並執行應用程式了。 當然,你不會真的塗抹這裡的任何東西。 你可以抓住一份從 dx.h dx.codeplex.com 開始添加一些 Direct2D 渲染代碼和 (看我 2013 年 6 月的專欄文章,"現代圖書館為 DirectX 程式設計," msdn.microsoft.com/magazine/dn201741 有關的詳細資訊),或等到我的下一欄,那裡我告訴你如何最好地融入 Direct2D WinRT 核心應用程式模型。

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

感謝以下技術專家對本文的審閱:JamesMcNellis (Microsoft)
JamesMcNellis 是一個 c + + 愛好者和微軟的 Visual c + + 團隊的軟體發展人員在他那裡他生成 c + + 庫和維護的 C 運行時庫 (CRT)。他在微博 @JamesMcNellis,並通過線上其他地方可以發現 HTTP://jamesmcnellis.com/