創建 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。