CMake + C++を使って WinRT Component を作って C# WinUI3 アプリから使う (前編)

C# WinUI3 から使える[Windows Runtime (WinRT)](https://ja.wikipedia.org/wiki/Windows%E3%83%A9%E3%83%B3%E3%82%BF%E3%82%A4%E3%83%A0) Component を、CMake + C++ で作る方法をまとめます。

# モチベーション
## なぜWinRT?
C#からC/C++を利用するのが楽になるからです。
> 例えば従来のデスクトップアプリケーションの場合、C#コードからC++コードを再利用するときや、あるいはC++コードからC#コードを再利用するときは、いずれもP/Invoke、COM相互運用、もしくはマネージ言語であるC++/CLIによるラッパーを介する必要があったが、WinRTの場合はネイティブ拡張であるC++/CXまたはC++/WinRTを介することで相互運用可能なコンポーネントを作成・利用できるため、明示的にP/Invokeやマネージ言語を介する必要がなくなる

([Windows ランタイム:概要](https://ja.wikipedia.org/wiki/Windows%E3%83%A9%E3%83%B3%E3%82%BF%E3%82%A4%E3%83%A0#%E6%A6%82%E8%A6%81)) より

## なぜ CMake + C++?
オープンソースのライブラリで、CMake + C++(C) で書かれているものを利用するケースを想定しています。

# 全体像
全体の流れは以下の図のようになります。
<img width="400" alt="全体像" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1410370/f7177b41-ed1d-46e7-9218-332966a7e85f.png">

## IDL (.idl)
WinRT Component のAPIを以下のように[Microsoft インターフェイス定義言語](https://docs.microsoft.com/ja-JP/uwp/midl-3/)で定義します。
```
namespace Sample
{
	[default_interface]
	runtimeclass SampleClass
	{
		SampleClass();
		Double sqrt(Double x);
	}
}
```
Microsoft インターフェイス定義言語にはいくつかバージョンがありますが、WinRTではバージョン3.0を使います。このバージョンではシンプルにWinRT コンポーネントのインターフェイスを記述することができます。

IDLファイルはMIDLコンパイラを使ってコンパイルし、Windows Metadataを生成します。

### Windows Metadata (.winmd)
Windows Metadata は WinRT Component API を各言語で使用する(Projection)ためのメタデータが格納されています。このメタデータを媒介して、C++用のProjectionコードや、C#用のProjectionコードを生成します。

C++世界では、[CppWinRT](https://github.com/microsoft/cppwinrt)というツールを使ってProjectionコードを生成します。言語Projection自体は裏でCOM技術を使って実装されます。が、そのあたりの泥臭いコードは cppwinrt が helper として生成してくれます。WinRTとCOMがどのように動くかに興味がある方は[こちらのMSDNの連載](https://docs.microsoft.com/ja-jp/archive/msdn-magazine/2015/july/windows-with-c-windows-runtime-components)を参照してください。

### DLL
cppwinrtが生成したC++Projectionコードと、実際の中身のコードを合わせてビルドし、DLLを生成します。このDLLと、.winmd を使ってC#からWinRTコンポーネントを利用することができます。
ただし.Net 6.0 以降では直接.winmdを参照することができなくなっています。このためもうひと手間加える必要があります。こちらは別途紹介します。

# ソースコードの準備
```
namespace Sample
{
	[default_interface]
	runtimeclass SampleClass
	{
		SampleClass();
		Double sqrt(Double x);
	}
}
```
を Sample.idl として作っておきます。sqrtを計算するシンプルなクラスを提供します。

また、空のSampleClass.cppを作っておきます。SampleClass.cppのスケルトンはcppwinrtが生成しますが、初期段階では存在しないため、CMakeのDLLターゲットでこのファイルを使おうとするとエラーになるためです。

さらに
```
EXPORTS
DllCanUnloadNow = WINRT_CanUnloadNow                    PRIVATE
DllGetActivationFactory = WINRT_GetActivationFactory    PRIVATE
```
をSample.def として作っておきます。こちらはWinRT Component DLL の export のお約束です。

# いざ CMake
ではCMake世界を見ていきましょう。
## 環境
CMake は ver 3.22を想定しています。また、VisualStudio依存を減らすため、ジェネレータはNinja (1.10.2)を使います。
コマンドは x64 Native Tools CommandPrompt for VS 2022 から実行します。

## MIDL パート
MIDLコンパイラを探しましょう。find_programで見つけます。x64 Native Tools CommandPrompt for VS 2022環境であれば見つかるはずです。

```
find_program(MIDL_EXE midl REQUIRED)
```

MIDLに生成してもらう.winmdファイルの場所も決めておきます。 
```
set(WINMD_FILE "${CMAKE_CURRENT_BINARY_DIR}/Sample.winmd")
```

このWINMD_FILEを生成するコマンドを追加します。
```
add_custom_command(
	OUTPUT ${WINMD_FILE}
	COMMAND ${MIDL_EXE} /nologo /winrt /nomidl /metadata_dir ${METADATA_DIR} /reference ${FOUNDATION_CONTRACT} ${MIDL_NATIVE_PATH} /winmd ${WINMD_NATIVE_PATH}
	MAIN_DEPENDENCY ${MIDL_FILE}
	COMMENT "Generating winmd file"
)
```

### MIDLオプション
 - /nologo ロゴの表示を抑制します
 - /winrt WinRT API のIDLとして処理することを指定します。このオプションを指定すると、MIDL は MIDLRT に引数を渡します。このあたりの動きは[このMSDNの記事](https://docs.microsoft.com/ja-jp/archive/msdn-magazine/2015/august/windows-with-c-windows-runtime-components-with-midl)にあります。
 - /nomidl MIDLRTが呼び出された後にMIDLの呼び出しを抑制します。winmdの生成だけでよいので、このオプションを指定しています。
 - /metadata_dir メタデータディレクトリを指定します。ここでは`$ENV{WindowsSdkDir}/References/$ENV{WindowsSDKLibVersion}/Windows.Foundation.FoundationContract/4.0.0.0` を指定しています。
 - /reference 追加で参照するメタデータを指定します。ここでは`$ENV{WindowsSdkDir}/References/$ENV{WindowsSDKLibVersion}/Windows.Foundation.FoundationContract/4.0.0.0/Windows.Foundation.FoundationContract.winmd` を指定します。
 - /winmd 出力するwinmd ファイルを指定します

注意点ですが、midl.exe のオプションで指定するパスは、Windows Nativeにする必要があります。
`cmake_path(NATIVE_PATH)`を使ってNative Pathに変換したものを渡します。

### CppWinRT パート
cppwinrt.exe を使って Windows Metadata から必要なファイルを生成します。

#### cppwinrt.exe を使えるようにする
cppwinrt は nuget で取得します。具体的には、CMake の ExtenalProject 機能を使って、nupkg をダウンロード・展開します。configure や build や install は必要ないので何もしません。
```
include(ExternalProject)

ExternalProject_Add(
  CppWinRTBuild
  URL "https://github.com/microsoft/cppwinrt/releases/download/2.0.220131.2/Microsoft.Windows.CppWinRT.2.0.220131.2.nupkg"
  URL_HASH "SHA256=899c1c676c72404aea4c07ebd7e3314c245867f95918c79fc122642df85e168c"

  CONFIGURE_COMMAND ""
  BUILD_COMMAND ""
  INSTALL_COMMAND ""

  EXCLUDE_FROM_ALL
)
```

展開したソースからcppwinrt.exe を find_program して、CppWinRT target として使えるように設定します。
```
ExternalProject_Get_Property(CppWinRTBuild SOURCE_DIR)
find_program(CPPWinRT cppwinrt PATHS ${SOURCE_DIR}/bin REQUIRED)

add_executable(CppWinRT IMPORTED GLOBAL)
add_dependencies(CppWinRT CppWinRTBuild)
set_target_properties(CppWinRT PROPERTIES IMPORTED_LOCATION "${CPPWinRT}")
```
 
cppwinrt を使う側からは```$<TARGET_FILE:CppWinRT>``` としてアクセスします。

#### cppwinrt を使ってのコード生成

cppwinrt.exe に Sample.winmd を入力すると、以下のようなファイルを生成します。
 - module.g.cpp
 - Sample.h
 - SampleClass.g.h
 - SampleClass.h
 - SampleClass.cpp

SampleClass.h と SampleClass.cpp はSampleClass実装のスケルトンで、ここに肉付けをしていきます。実際にこれらのファイルには
```
// Note: Remove this static_assert after copying these generated source files to your project.
// This assertion exists to avoid compiling these generated source files directly.
static_assert(false, "Do not compile generated C++/WinRT source files directly");
```
というようにstatic_assert が書かれていてそのままではコンパイルできません。

cppwinrt.exe を起動するために、module.g.cpp をOUTPUTとするカスタムコマンドを追加します。

```
add_custom_command(
	OUTPUT
		${CMAKE_CURRENT_BINARY_DIR}/module.g.cpp
	COMMAND "$<TARGET_FILE:CppWinRT>" -in ${WINMD_NATIVE_PATH} -reference local -component "${OUT_DIR}/generated" -output ${OUT_DIR} -pch .
	WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
	MAIN_DEPENDENCY ${WINMD_FILE}
	COMMENT "Generating WinRT component files"
)
```

-component オプションでスケルトンファイルの出力先を、-output オプションでその他のファイルの出力先を指定します。前述のとおりスケルトンファイルはそのままでは使えないので、出力先を別にしておきます。

# DLL
cppwinrt により出力されたファイルと、スケルトンファイルを元にした実装ファイルを使ってDLLのターゲットを追加します。
```
add_library(Sample SHARED
	${CMAKE_CURRENT_BINARY_DIR}/module.g.cpp
	SampleClass.cpp
	Sample.def
)
```
この add_library()が失敗しないように、SampleClass.cpp は最初は空ファイルとして追加していました。
一度ビルドを実行すると、スケルトンファイルとして SampleClass.cpp と SampleClass.h が生成されるので、これらファイルと差し替えます。`static_assert` を削除するのを忘れずに。

ここまででDLLのビルドが成功するはずです。ちなみにスケルトンの実装は
```
namespace winrt::Sample::implementation
{
    double SampleClass::sqrt(double x)
    {
        throw hresult_not_implemented();
    }
}
```
と not implemented 例外を投げるので、中身を変えてあげる必要があります。

.Net 5 以前ではC#から Sample.winmd への参照を追加するだけでこのC++ WinRT 実装を使えるようになります。

.Net 6 以降ではこの方法がサポートされなくなり、
- C# projection DLL を作って
- nuget 経由で参照
する必要があります。

# まとめ
CMake + C++ でWinRT Component を作成する方法を説明しました。
[後編](https://qiita.com/unicodon/items/4351458bda722a106695)では作ったWinRT Component をWinUI C#アプリから使用する方法を説明します。

関連コードは https://github.com/unicodon/cmake-winui3/tree/sample-v1 に置いてあります。