4
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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

Last updated at Posted at 2022-08-03

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 に置いてあります。

4
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?