C# WinUI3 から使えるWindows Runtime (WinRT) Component を、CMake + C++ で作る方法をまとめます。
モチベーション
なぜWinRT?
C#からC/C++を利用するのが楽になるからです。
例えば従来のデスクトップアプリケーションの場合、C#コードからC++コードを再利用するときや、あるいはC++コードからC#コードを再利用するときは、いずれもP/Invoke、COM相互運用、もしくはマネージ言語であるC++/CLIによるラッパーを介する必要があったが、WinRTの場合はネイティブ拡張であるC++/CXまたはC++/WinRTを介することで相互運用可能なコンポーネントを作成・利用できるため、明示的にP/Invokeやマネージ言語を介する必要がなくなる
(Windows ランタイム:概要) より
なぜ CMake + C++?
オープンソースのライブラリで、CMake + C++(C) で書かれているものを利用するケースを想定しています。
全体像
IDL (.idl)
WinRT Component のAPIを以下のようにMicrosoft インターフェイス定義言語で定義します。
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というツールを使ってProjectionコードを生成します。言語Projection自体は裏でCOM技術を使って実装されます。が、そのあたりの泥臭いコードは cppwinrt が helper として生成してくれます。WinRTとCOMがどのように動くかに興味がある方はこちらのMSDNの連載を参照してください。
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の記事にあります。
- /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 を作成する方法を説明しました。
後編では作ったWinRT Component をWinUI C#アプリから使用する方法を説明します。
関連コードは https://github.com/unicodon/cmake-winui3/tree/sample-v1 に置いてあります。