建立 C++ WinRT 元件
通過 Cpp/WinRT 專案模板建立一個 WinRT 元件工程 CppWinrtComponent.vcxproj,主要介面定義如下:
namespace CppWinrtComponent
{
[default_interface]
runtimeclass Class
{
Class();
String GetModule();
}
}
最終該專案 CppWinrtComponent 可以被編譯生成兩個 WinRT 元件的核心部分:
- CppWinrtComponent.winmd, 提供介面描述
- CppWinrtComponent.dll, 負責介面實現,基於 COM 技術
由於該專案 CppWinrtComponent.vcxproj 預設的平臺是 Universal Windows,所以它使用的 VC 執行時函式都是 Store 版本,這導致在非 Universal Windows 應用中會出現找不到 VC 執行時庫的異常。為了解決這個問題,針對該型別專案,Visual Studio 2019 version 16.9 版本開始支援屬性 Windows Desktop Compatible,設定該屬性後所有的 VC 執行時函式會被連結到 Deskotp 版本。
該屬性可以在 VS 專案屬性皮膚中配置,對應的 vcxproj 中配置如下:
<PropertyGroup>
<DesktopCompatible>true</DesktopCompatible>
</PropertyGroup>
消費 C++ WinRT 元件
本文主要討論在如下語言、框架中使用該元件:
- C++ Desktop
- C++ UWP
- .Net Framework Desktop
- .Net5 Desktop
C++ Desktop
使用 VS 的 C++ 專案模板,建立一個空的控制檯應用程式 ConsumerCppConsole.vcxproj。
1. 使用 Nuget 包管理器給該專案安裝 Microsoft.Windows.CppWinRT
建議安裝最新版本,在 Microsoft.Windows.CppWinRT 2.0.200115.8 之後版本中,引入了對 Reg Free 的支援,可以簡化 WinRT 元件使用流程,不需要建立額外的 Manifest 檔案。
2. 新增對專案 CppWinrtComponent 生成的 CppWinrtComponent.WinMD 的引用
不能直接在 VS 中新增對專案 CppWinrtComponent.vcxproj 的工程引用,但是可以直接新增對檔案 CppWinrtComponent.winmd 的引用,在該檔案引用的屬性中,VS 預設會把"Copy Local" 設定為 True,該操作會導致 CppWinrtComponent.dll 在編譯後會被複制到輸出目錄,這也是期待的行為。
雖然在 VS 的 GUI 中無法直接新增對 CppWinrtComponent.vcxproj 的工程引用,但是可以手動編輯 ConsumerCppConsole.vcxproj 來新增!
...
<ItemGroup>
<ProjectReference Include="..\CppWinrtComponent\CppWinrtComponent.vcxproj">
<Project>{184b97de-746b-47d4-b055-d18f24b57dba}</Project>
</ProjectReference>
</ItemGroup>
...
3. 使用 Nuget 包管理器給該專案安裝 Microsoft.VCRTForwarders.140
因為 CppWinrtComponent 是用 C++ 程式碼編寫,必不可少會用到 VC 的執行時庫,但是預設情況下,CppWinrtComponent.vcxproj 是一個 Universal Windows 的專案,所以它連結的 VC 執行時庫都是 Store 版本,Store 版本 VC 執行時庫不隨 Windows 分發,所以我們的 Desktop 應用在執行時找不到這些 VC 執行時庫。
Microsoft.VCRTForwarders.140 的原理是把所有的 Store 版本的 VC 執行時 API 全部連結到 Desktop 版本。元件如其名,就是一個 Forwarder!
如果 CppWinrtComponent.vcxproj 中的 DesktopCompatible 被開啟,則此步驟3不再必要。
4. 呼叫 CppWinrtComponent 元件
CppWinrt.exe 會解析 CppWinrtComponent.winmd 生成標頭檔案 winrt/CppWinrtComponent.h, 通過該標頭檔案,就可以呼叫元件 CppWinrtComponent 了。
C++/WinRT UWP
C++/WinRT 有一個 VS 的擴充包,提供了幾種基於 C++/WinRT 的專案模板。(https://marketplace.visualstudio.com/items?itemName=CppWinRTTeam.cppwinrt101804264)
用模板建立一個 UWP 專案 ConsumerCppWinrtUWP.vcproj。
1. 新增對專案 CppWinrtComponent 生成的工程引用
因為 ConsumerCppWinrtUWP 和 CppWinrtComponent 都是 Universal Windows 專案,所以可以直接通過 VS 新增對 CppWinrtComponent 的工程引用,CppWinrtComponent 輸出的 CppWinrtComponent.winmd 和 CppWinrtComponent.dll 會被複制到 ConsumerCppWinrtUWP 的生成目錄。
2. 呼叫 CppWinrtComponent 元件
CppWinrt.exe 會解析 CppWinrtComponent.winmd 生成標頭檔案 winrt/CppWinrtComponent.h, 通過該標頭檔案,就可以呼叫元件 CppWinrtComponent 了。
.Net Framework Desktop
在 VS 裡面建立一個 .Net Framework console 專案 ConsumerNetFrameworkConsole.csproj,對於使用 Winrt 元件而言,基於.Net Framework 的 WPF 和 WinFrom 應用與 Console 應用沒有任何區別。
值得注意的是,微軟在 .Net Framework 4.5引入了對 WinRT 的支援。在 .Net5 中移除了對 WinRT 的原生支援。
1. 新增對專案 CppWinrtComponent 生成的工程引用
2. 建立 Reg Free Manimest 檔案
C++ WinRT 元件實際是一種程式內(In Process)COM 伺服器,使用之前需要向 OS 註冊。在 UWP 應用中,Package Manifest 裡面可以宣告所有用到的 WinRT 元件,在執行時或者安裝時,OS 可以根據此檔案完成 COM 伺服器的註冊呼叫(此處具體細節暫時不清楚,但大致原理應該是這樣)。
同樣的,針對桌面應用也可以生成一個 manifest 檔案,用來描述 WinRT 元件,該檔案會被嵌入到桌面應用生成的可執行檔案中,在執行時幫助我們完成對 WinRT 元件的載入(COM呼叫)。
該檔案結構如下:
<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<!--名稱 MyApplication.app 無關緊要,確保唯一就可以-->
<assemblyIdentity version="1.0.0.0" name="MyApplication.app"/>
<!-- WinRT 元件的 DLL 名稱 -->
<file name="CppWinrtComponent.dll">
<!-- 執行緒模型預設 both(代表 STA 和 MTA),name 是在 WinRT 元件中定義的 "名稱空間.介面名稱" -->
<activatableClass
name="CppWinrtComponent.Class"
threadingModel="both"
xmlns="urn:schemas-microsoft-com:winrt.v1" />
</file>
</assembly>
可以將該檔案任意命名,此處命名為 app.manifest, 然後把它新增到 ConsumerNetFrameworkConsole.csproj 中,為避免出錯,選擇手動編輯該專案檔案,加入如下節點:
<PropertyGroup>
<ApplicationManifest>app.manifest</ApplicationManifest>
</PropertyGroup>
最後編譯專案 ConsumerNetFrameworkConsole.csproj,該檔案會被嵌入到 ConsumerNetFrameworkConsole.exe 中。所以在分發 ConsumerNetFrameworkConsole.exe 的時候,並不需要將 manifest 檔案一起分發。
3. 使用 Nuget 包管理器給該專案安裝 Microsoft.VCRTForwarders.140
參考 C++ Desktop
步驟3。
4. 呼叫 CppWinrtComponent 元件
因為新增了對專案 CppWinrtComponent 的引用,VS 可以從 CppWinrtComponent.winmd 中讀取到介面的後設資料資訊,所以和普通的 .Net 物件一樣,可以通過名稱空間、類名訪問到該 WinRT 元件。
.Net5 Desktop
在 VS 裡面建立一個 .Net5 Console 專案 ConsumerNet5Console.csproj。
微軟把 WinRT 的支援從 .Net5 中拿掉後,建立了專案 C#/WinRT, 它包含一個 cswinrt.exe, 可以解析 WinRT 元件的 wimmd 檔案並建立相應的 C# 程式碼,這些程式碼是關於如果通過 COM 的方式訪問 WinRT 元件。這些程式碼和 C++ Desktop
中 CppWinRT 生成的程式碼非常一致,幾乎就是同一份程式碼 C# 改寫版。實際上 C#/WinRT 和 CppWinRT 是用相同的解析器來解析 winmd 檔案的。 詳情可參考微軟開源專案 xlang。
1. 建立 .Net5 Library CppWinrtComponentProjection.csproj
因為 C#/WinRT 會建立用來訪問 WinRT 元件的程式碼,為了方便使用這些程式碼,在這兒建立一個單獨的 .Net5 Library 來包裝這些程式碼。
實際上可以略過這一步,直接在專案 ConsumerNet5Console.csproj 上執行以下步驟!
2. 使用 Nuget 包管理器給 CppWinrtComponentProjection.csproj 安裝 Microsoft.Windows.CsWinRT
3. 給 CppWinrtComponentProjection.csproj 新增專案 CppWinrtComponent.csproj 的工程引用
C#/WinRT 會檢查專案 CppWinrtComponentProjection.csproj 依賴的所有 WinRT 元件,並根據步驟4中指定的 WinRT 元件中的名稱空間,為其建立C#訪問程式碼。
4. 新增CsWinRTIncludes
C#/WinRT 會讀取該專案屬性,它指定了引入的 WinRT 元件中的名稱空間,這樣才能為這個名稱空間生成 C# 訪問程式碼。
需要手動在 CppWinrtComponentProjection.csproj 中新增如下節點:
<PropertyGroup>
<CsWinRTIncludes>CppWinrtComponent</CsWinRTIncludes>
</PropertyGroup>
5. 編譯CppWinrtComponentProjection.csproj
編譯這個 Library, 在該專案 Obj 目錄 obj\x64\Debug\net5.0-windows10.0.19041.0\Generated Files\
會生成如下四個檔案:
- CppWinrtComponent.cs
- WinRT.cs
- WinRT_Interop.cs
- WinRTEventHelpers.cs
它們就是用來訪問 WinRT 元件 CppWinrtComponent 的全部 C# 程式碼! 同時它們也會被編譯成一個 .Net5 Library CppWinrtComponentProjection
, 這個 Library 就是所謂的 CppWinrtComponent 的 Projection。
6. 給ConsumerNet5Console.csproj 新增工程引用 CppWinrtComponentProjection.csproj
因為最終需要在 ConsumerNet5Console.csproj 使用 WinRT 元件,所以加入對 projection 專案的引用,該引用是一個標準的 .Net5 Library。
7. 複製 CppWinrtComponent.dll 到輸出專案ConsumerNet5Console的輸出目錄
前6步所作的工作只是用來呼叫 WinRT 元件,真正的元件伺服器是 CppWinrtComponent.Dll,它會在執行時被載入到 ConsumerNet5Console 的程式中。
8. 使用 Nuget 包管理器給該專案 ConsumerNet5Console.csproj 安裝 Microsoft.VCRTForwarders.140
9. 呼叫 CppWinrtComponent 元件
新增完對 CppWinrtComponentProjection 的引用後,就可以直接用 名稱空間.類名
訪問WinRT 元件 CppWinrtComponent。