Unreal での WinRT

HoloLens の開発の過程で、WinRT を使用して機能を記述する必要が生じる場合があります。 たとえば、HoloLens アプリケーションでファイル ダイアログを開くときは winrt/Windows.Storage.Pickers.h ヘッダー ファイルの FileSavePicker が必要になります。 WinRT は、バージョン 4.26 以降の Unreal のビルド システムでサポートされています。

標準の WinRT API

WinRT を使用する最も一般的で簡単な方法は、WinSDK からメソッドを呼び出すことです。 これを行うには、YourModule.Build.cs ファイルを開いて、次の行を追加します。

if (Target.Platform == UnrealTargetPlatform.Win64 || Target.Platform == UnrealTargetPlatform.HoloLens)
{
	// These parameters are mandatory for winrt support
	bEnableExceptions = true;
	bUseUnity = false;
	CppStandard = CppStandardVersion.Cpp17;
	PublicSystemLibraries.AddRange(new string[] { "shlwapi.lib", "runtimeobject.lib" });
	PrivateIncludePaths.Add(Path.Combine(Target.WindowsPlatform.WindowsSdkDir,        
                                        "Include", 
                                        Target.WindowsPlatform.WindowsSdkVersion, 
                                        "cppwinrt"));
}

次に、以下の WinRT ヘッダーを追加する必要があります。

#if (PLATFORM_WINDOWS || PLATFORM_HOLOLENS) 
//Before writing any code, you need to disable common warnings in WinRT headers
#pragma warning(disable : 5205 4265 4268 4946)

#include "Windows/AllowWindowsPlatformTypes.h"
#include "Windows/AllowWindowsPlatformAtomics.h"
#include "Windows/PreWindowsApi.h"

#include <unknwn.h>
#include <winrt/Windows.Foundation.h>
#include <winrt/Windows.Perception.Spatial.h>
#include <winrt/Windows.Foundation.Collections.h>

#include "Windows/PostWindowsApi.h"
#include "Windows/HideWindowsPlatformAtomics.h"
#include "Windows/HideWindowsPlatformTypes.h"
#endif

WinRT コードをコンパイルできるのは Win64 および HoloLens プラットフォームだけなので、if ステートメントを使用すると WinRT ライブラリが他のプラットフォームで含まれなくなります。 IUnknown インターフェイスを使用するために unknwn.h が追加されています。

NuGet パッケージの WinRT

WinRT をサポートする NuGet パッケージを追加する必要がある場合は、もう少し複雑です。 この場合、Visual Studio で実質的にすべてのジョブを実行できますが、Unreal ビルド システムではできません。 幸い、これはそれほど難しくありません。 以下に、Microsoft.MixedReality.QR パッケージをダウンロードする方法の例を示します。 これを別のものに置き換えることができますが、winmd ファイルが失われないようにし、正しい dll をコピーする必要があります。

前のセクションの Windows SDK dll は、OS によって処理されます。 NuGet の dll を、モジュール内のコードが管理しなければなりません。 それらをダウンロードしてバイナリ フォルダーにコピーし、モジュールの起動時にプロセス メモリに読み込むためのコードを追加することをお勧めします。

最初の手順では、packages.config (https://docs.microsoft.com/nuget/reference/packages-config) をモジュールのルート フォルダーに追加する必要があります。 そこで、ダウンロードしたいパッケージ (すべての依存関係を含む) を追加する必要があります。 ここでは、プライマリ ペイロードとして Microsoft.MixedReality.QR を追加し、他の 2 つをそれに対する依存関係として追加しました。 そのファイルの形式は、Visual Studio の場合と同じです。

<?xml version="1.0" encoding="utf-8"?>
<packages>
  <package id="Microsoft.MixedReality.QR" version="0.5.2102" targetFramework="native" />
  <package id="Microsoft.VCRTForwarders.140" version="1.0.6" targetFramework="native" />
  <package id="Microsoft.Windows.CppWinRT" version="2.0.200729.8" targetFramework="native" />
</packages>

これで、必要なパッケージ NuGet をダウンロードできます、または、NuGet のドキュメントを参照してください。

YourModule.Build.cs を開き、次のコードを追加します。

// WinRT with Nuget support
if (Target.Platform == UnrealTargetPlatform.Win64 || Target.Platform == UnrealTargetPlatform.HoloLens)
{
	// these parameters mandatory for winrt support
	bEnableExceptions = true;
	bUseUnity = false;
	CppStandard = CppStandardVersion.Cpp17;
	PublicSystemLibraries.AddRange(new string [] { "shlwapi.lib", "runtimeobject.lib" });

	// prepare everything for nuget
	string MyModuleName = GetType().Name;
	string NugetFolder = Path.Combine(PluginDirectory, "Intermediate", "Nuget", MyModuleName);
	Directory.CreateDirectory(NugetFolder);

	string BinariesSubFolder = Path.Combine("Binaries", "ThirdParty", Target.Type.ToString(), Target.Platform.ToString(), Target.Architecture);

	PrivateDefinitions.Add(string.Format("THIRDPARTY_BINARY_SUBFOLDER=\"{0}\"", BinariesSubFolder.Replace(@"\", @"\\")));

	string BinariesFolder = Path.Combine(PluginDirectory, BinariesSubFolder);
	Directory.CreateDirectory(BinariesFolder);

	ExternalDependencies.Add("packages.config");

	// download nuget
	string NugetExe = Path.Combine(NugetFolder, "nuget.exe");
	if (!File.Exists(NugetExe))
	{
		using (System.Net.WebClient myWebClient = new System.Net.WebClient())
		{
			// we aren't focusing on a specific nuget version, we can use any of them but the latest one is preferable
			myWebClient.DownloadFile(@"https://dist.nuget.org/win-x86-commandline/latest/nuget.exe", NugetExe);
		}
	}

	// run nuget to update the packages
	{
		var StartInfo = new System.Diagnostics.ProcessStartInfo(NugetExe, string.Format("install \"{0}\" -OutputDirectory \"{1}\"", Path.Combine(ModuleDirectory, "packages.config"), NugetFolder));
		StartInfo.UseShellExecute = false;
		StartInfo.CreateNoWindow = true;
		var ExitCode = Utils.RunLocalProcessAndPrintfOutput(StartInfo);
		if (ExitCode < 0)
		{
			throw new BuildException("Failed to get nuget packages.  See log for details.");
		}
	}

	// get list of the installed packages, that's needed because the code should get particular versions of the installed packages
	string[] InstalledPackages = Utils.RunLocalProcessAndReturnStdOut(NugetExe, string.Format("list -Source \"{0}\"", NugetFolder)).Split(new char[] { '\r', '\n' });

	// winmd files of the packages
	List<string> WinMDFiles = new List<string>();

	// WinRT lib for some job
	string QRPackage = InstalledPackages.FirstOrDefault(x => x.StartsWith("Microsoft.MixedReality.QR"));
	if (!string.IsNullOrEmpty(QRPackage))
	{
		string QRFolderName = QRPackage.Replace(" ", ".");

		// copying dll and winmd binaries to our local binaries folder
		// !!!!! please make sure that you use the path of file! Unreal can't do it for you !!!!!
		string WinMDFile = Path.Combine(NugetFolder, QRFolderName, @"lib\uap10.0.18362\Microsoft.MixedReality.QR.winmd");
		SafeCopy(WinMDFile, Path.Combine(BinariesFolder, "Microsoft.MixedReality.QR.winmd"));

		SafeCopy(Path.Combine(NugetFolder, QRFolderName, string.Format(@"runtimes\win10-{0}\native\Microsoft.MixedReality.QR.dll", Target.WindowsPlatform.Architecture.ToString())),
			Path.Combine(BinariesFolder, "Microsoft.MixedReality.QR.dll"));

		// also both both binaries must be in RuntimeDependencies, unless you get failures in Hololens platform
		RuntimeDependencies.Add(Path.Combine(BinariesFolder, "Microsoft.MixedReality.QR.dll"));
		RuntimeDependencies.Add(Path.Combine(BinariesFolder, "Microsoft.MixedReality.QR.winmd"));

		//add winmd file to the list for further processing using cppwinrt.exe
		WinMDFiles.Add(WinMDFile);
	}

	if (Target.Platform == UnrealTargetPlatform.Win64)
	{
		// Microsoft.VCRTForwarders.140 is needed to run WinRT dlls in Win64 platforms
		string VCRTForwardersPackage = InstalledPackages.FirstOrDefault(x => x.StartsWith("Microsoft.VCRTForwarders.140"));
		if (!string.IsNullOrEmpty(VCRTForwardersPackage))
		{
			string VCRTForwardersName = VCRTForwardersPackage.Replace(" ", ".");
			foreach (var Dll in Directory.EnumerateFiles(Path.Combine(NugetFolder, VCRTForwardersName, "runtimes/win10-x64/native/release"), "*_app.dll"))
			{
				string newDll = Path.Combine(BinariesFolder, Path.GetFileName(Dll));
				SafeCopy(Dll, newDll);
				RuntimeDependencies.Add(newDll);
			}
		}
	}

	// get WinRT package 
	string CppWinRTPackage = InstalledPackages.FirstOrDefault(x => x.StartsWith("Microsoft.Windows.CppWinRT"));
	if (!string.IsNullOrEmpty(CppWinRTPackage))
	{
		string CppWinRTName = CppWinRTPackage.Replace(" ", ".");
		string CppWinRTExe = Path.Combine(NugetFolder, CppWinRTName, "bin", "cppwinrt.exe");
		string CppWinRTFolder = Path.Combine(PluginDirectory, "Intermediate", CppWinRTName, MyModuleName);
		Directory.CreateDirectory(CppWinRTFolder);

		// all downloaded winmd file with WinSDK to be processed by cppwinrt.exe
		var WinMDFilesStringbuilder = new System.Text.StringBuilder();
		foreach (var winmd in WinMDFiles)
		{
			WinMDFilesStringbuilder.Append(" -input \"");
			WinMDFilesStringbuilder.Append(winmd);
			WinMDFilesStringbuilder.Append("\"");
		}

		// generate winrt headers and add them into include paths
		var StartInfo = new System.Diagnostics.ProcessStartInfo(CppWinRTExe, string.Format("{0} -input \"{1}\" -output \"{2}\"", WinMDFilesStringbuilder, Target.WindowsPlatform.WindowsSdkVersion, CppWinRTFolder));
		StartInfo.UseShellExecute = false;
		StartInfo.CreateNoWindow = true;
		var ExitCode = Utils.RunLocalProcessAndPrintfOutput(StartInfo);
		if (ExitCode < 0)
		{
			throw new BuildException("Failed to get generate WinRT headers.  See log for details.");
		}

		PrivateIncludePaths.Add(CppWinRTFolder);
	}
	else
	{
		// fall back to default WinSDK headers if no winrt package in our list
		PrivateIncludePaths.Add(Path.Combine(Target.WindowsPlatform.WindowsSdkDir, "Include", Target.WindowsPlatform.WindowsSdkVersion, "cppwinrt"));
	}
}

SafeCopy メソッドを次のように定義する必要があります。

private void SafeCopy(string source, string destination)
{
	if(!File.Exists(source))
	{
		Log.TraceError("Class {0} can't find {1} file for copying", this.GetType().Name, source);
		return;
	}

	try
	{
		File.Copy(source, destination, true);
	}
	catch(IOException ex)
	{
		Log.TraceWarning("Failed to copy {0} to {1} with exception: {2}", source, destination, ex.Message);
		if (!File.Exists(destination))
		{
			Log.TraceError("Destination file {0} does not exist", destination);
			return;
		}

		Log.TraceWarning("Destination file {0} already existed and is probably in use.  The old file will be used for the runtime dependency.  This may happen when packaging a Win64 exe from the editor.", destination);
	}
}

NuGet の DLL は手動で Win32 プロセス メモリに読み込む必要があります。モジュールのスタートアップ メソッドに手動の読み込みを追加することをお勧めします。

void StartupModule() override
{
#if PLATFORM_WINDOWS
	const FString LibrariesDir = FPaths::ProjectPluginsDir() / "MyModule" / THIRDPARTY_BINARY_SUBFOLDER;
	FPlatformProcess::PushDllDirectory(*LibrariesDir);

	const FString DllName = "Microsoft.MixedReality.QR.dll";
	if (!FPlatformProcess::GetDllHandle(*DllName))
	{
		UE_LOG(LogHMD, Warning, TEXT("Dll \'%s\' can't be loaded from \'%s\'"), *DllName, *LibrariesDir);
	}

	FPlatformProcess::PopDllDirectory(*LibrariesDir);
#endif
}

最後に、前のセクションで説明したように、WinRT ヘッダーをコードに含めることができます。

次の開発チェックポイント

用意されている Unreal 開発体験に従っている場合、Mixed Reality プラットフォームの機能と API を探索している段階にいます。 ここから、任意のトピックに進むことも、デバイスまたはエミュレーターへのアプリのデプロイ操作に直接移動することもできます。

関連項目