はじめに
Windows 10では、UWP(ストアアプリ、ネイティブアプリ)から使えるいろいろな新しいAPI (WinRT) が追加されています。
強力な機能は魅力的ですが、UWPアプリはサンドボックス内で実行されるので、セキュリティによって実装に制約が生じたり、スタートメニューからしか起動できない、社内などの他の人への配布が大変(ストア経由でないアプリを使うためには設定が必要)と、少々使いづらい点もあります。
そこで、Windowsの普通の(exeをダブルクリックして起動できる)デスクトップアプリから、WinRTの機能を「つまみ食い」できると便利だと思います。
そのための仕組みとして「C++/WinRT」があります。
- C++/WinRT の概要 - UWP applications | Microsoft Docs
- ネイティブの C++ から UWP の API を呼ぶ (C++/WinRT を使って) - かずきのBlog@hatena
- C++/WinRTの始め方 - Qiita
しかし、アプリをUI部分も含めて全部ネイティブのC++で書くのは大変なので、C#のフォームアプリから呼び出せると都合がよいと思うわけです。
そこで、C++/WinRTでブリッジのライブラリを作成し、C#からそのクラスを参照させることによって、C#からWinRTの恩恵にあずかる方法を紹介します。
後の公式サンプルのreadmeによると、この機能が使えるのは「new feature in Windows Builds 18309+」ということなので、Windows 10 バージョン1903以降で使える機能ということになります。
開発環境
- Windows 10 Home 1903
- Visual Studio Community 2019
事前準備
Visual Studio Installerからコンポーネントを追加します。
- ワークロード: 以下の3つにチェック
- .NETデスクトップ開発
- C++によるデスクトップ開発
- ユニバーサルWindowsプラットフォーム開発
- 個別のコンポーネント
- Windows 10 SDK (10.0.18362.0以降を1つ以上)
- (他にもあったかどうか忘れました…)
さらに、C++/WinRT開発のためのVSIXをインストールしておきます。
C++/WinRT - Visual Studio Marketplace
公式サンプルの動作確認
C++/WinRTブリッジを介したC#フォームアプリの作成方法については、Microsoftから公式サンプルが提供されています。
microsoft/RegFree_WinRT: Sample Code showing use of Registration-free WinRT to access a 3rd party native WinRT Component from a non-packaged Desktop app
まずは RegFree_WinRT/CS/RegFree_WinRT.sln をビルド・実行できることを確認しましょう。
自分でブリッジを書く
いよいよここからが本番です。以下がMS公式のドキュメント。
Enhancing Non-packaged Desktop Apps using Windows Runtime Components - Windows Developer Blog
新しいソリューションに、以下2つのプロジェクトを追加します。
- Windows Runtime Component (C++/WinRT)
- ブリッジを書くためのプロジェクトになります。
- 名前はとりあえずデフォルトの RuntimeComponent1
- Windowsのターゲットバージョンは1903以降で。
- WPF アプリ (.NET Framework) または Windows フォーム アプリケーション (.NET Framework)
- アプリのメインのプロジェクトになります。
- 名前はとりあえずデフォルトの WpfApp1
- 対象のフレームワークは .NET Framework 4.7.2
C++/WinRT側
最初に何も変更せずに一度ビルドしておきます。するといくつかのファイルが自動生成されます。
IntelliSenseのエラーが気になりますが、ソリューションを開き直すと改善します。
ここで自分のコードを記述するときに変更する必要があるのが、以下の4ファイルです。
- Class.h
- Class.cpp
- Class.idl
- pch.h
Class.h, Class.cpp
これは普通のC++のクラスを書くときにもおなじみのペアですね。
基本的にはデフォルトで書かれているコードを真似してメソッドやプロパティを追加していけばよいです。
サンプルの MyProperty
を改造してプロパティを実装し、さらにメソッドを新しく1つ追加しておきます。
#pragma once
#include "Class.g.h"
namespace winrt::RuntimeComponent1::implementation
{
struct Class : ClassT<Class>
{
Class(int32_t initial); // 変更
int32_t MyProperty();
void MyProperty(int32_t value);
int32_t MyMethod(); // 追加
private:
int32_t _value; // 追加
};
}
// この中は触らなくてOK
namespace winrt::RuntimeComponent1::factory_implementation
{
struct Class : ClassT<Class, implementation::Class>
{
};
}
#include "pch.h"
#include "Class.h"
#include "Class.g.cpp"
namespace winrt::RuntimeComponent1::implementation
{
// コンストラクタ
Class::Class(int32_t initial)
{
_value = initial;
}
// プロパティ (get)
int32_t Class::MyProperty()
{
return _value;
}
// プロパティ (set)
void Class::MyProperty(int32_t value)
{
_value = value;
}
// メソッド
int32_t Class::MyMethod()
{
return _value * 2;
}
}
Class.idl
Microsoft インターフェイス定義言語 (MIDL) で書かれているファイルです。
C#のアプリからブリッジを参照したときにIntelliSenseで表示されるクラス定義を、このファイルによって与えるということのようです。
(Class.cpp, Class.h だけだと普通のネイティブC++のクラスなので、C#から参照するための情報は入っていないみたいです)
Microsoft インターフェイス定義言語3.0 の概要 - Windows UWP applications | Microsoft Docs
書き方はC++に似ています。
namespace RuntimeComponent1
{
[default_interface]
runtimeclass Class
{
Class(Int32 initial); // 変更
Int32 MyProperty;
Int32 MyMethod(); // 追加
}
}
-
Class(Int32 initial);
- コンストラクタ(引数名を value にするとエラーになったので名前を変えました)
-
Int32 MyProperty;
- 引数リストをつけないとプロパティ扱い
-
Int32 MyMethod();
- 引数リストをつけるとメソッド扱い
引数と戻り値のデータ型は、C++で使われるものとは違っていますが、名前から直感的に対応が類推できます。基本の数値データ型には以下があります。(前記公式ドキュメントより)
- Int16
- Int32
- Int64
- UInt8
- UInt16
- UInt32
- UInt64
- Single
- Double
pch.h
WinRTなどのヘッダファイルを追加する必要があるときに、このファイルに #include
文を追加します。
今回はデフォルトのまま置いておきます。
ここまでの変更を加えたあと、ビルドができることを確認します。
C#側
C#プロジェクトのデフォルトのプラットフォームは Any CPU になっていますが、x64 または Win32 に変えておいてください。RuntimeComponent1と合わせておきます(RuntimeComponent1がx86の場合は、WpfApp1側はWin32)。
WpfApp1から、先ほど作成したRuntimeComponent1を参照します。
これによって RuntimeComponent1
という名前空間が見えるようになり、その下にある Class
というクラスにアクセスできるはずです。
フォームに適当にボタンを配置し、ボタンを押したらクラスを使って何か処理させてみます。
/* Button_Click() 以外のコードは省略 */
private void Button_Click(object sender, RoutedEventArgs e)
{
var cls = new RuntimeComponent1.Class(100);
MessageBox.Show(cls.MyProperty.ToString());
MessageBox.Show(cls.MyMethod().ToString());
cls.MyProperty = 200;
MessageBox.Show(cls.MyProperty.ToString());
MessageBox.Show(cls.MyMethod().ToString());
}
確かに RuntimeComponent1.Class
が見えるようになりました。
しかし、この状態ではビルドは通りますが、実行時にエラーが発生します。
System.TypeLoadException: '要求された Windows ランタイム型 'RuntimeComponent1.Class' は登録されていません。'
MSのサンプルを見ればわかりますが、実はC#側にも「RuntimeComponent1.Class
の実装はこのDLLに入っていますよ」という情報を追加してあげないといけません。
プロジェクトに「アプリケーション マニフェスト ファイル」を追加します。
今回作成したブリッジと、そこに含まれているクラスの情報を追加します。
<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<assemblyIdentity version="1.0.0.0" name="MyApplication.app"/>
(略)
<file name="RuntimeComponent1.dll">
<activatableClass
name="RuntimeComponent1.Class"
threadingModel="both"
xmlns="urn:schemas-microsoft-com:winrt.v1" />
</file>
</assembly>
- 複数のブリッジライブラリがある場合は、その数だけ
<file>...</file>
を追加 - 1つのライブラリの中に複数のクラスがある場合は、
<activatableClass>
を追加
さらに、C++側で作ったDLLは「VCRT Forwarders」を参照して動くので、C#のexeと同じ場所にこれらのDLLがないといけません。
NuGetパッケージマネージャーから「Microsoft.VCRTForwarders.140」を追加しておくと、C#側のビルド時にexeと同じ場所に必要なDLLがコピーされるようになります。
実行
ウィンドウに配置したボタンを押して 100, 200, 200, 400 の順にメッセージが表示されたら成功です。
応用例
ここまでの例では、C++/WinRTのブリッジと言いながら、WinRTの機能を全く使っていませんでした。
ということで、実際にWinRTを呼び出す例も見てみましょう。
日本語の形態素解析(文章を単語単位に分割する)APIがあるので、それを使ってみます。
JapanesePhoneticAnalyzer Class (Windows.Globalization) - Windows UWP applications | Microsoft Docs
与えられた文章に対して、単語リストを返すようなメソッドを作ります。
C++/WinRT側
Class.h
#pragma once
#include "Class.g.h"
using namespace winrt::Windows::Foundation::Collections;
namespace winrt::RuntimeComponent1::implementation
{
struct Class : ClassT<Class>
{
Class() = default; // デフォルトコンストラクタ
static IVectorView<hstring> Class::Analyze(hstring text); // static指定も可能
};
}
namespace winrt::RuntimeComponent1::factory_implementation
{
struct Class : ClassT<Class, implementation::Class>
{
};
}
Class.h で Class() = default;
と書いておくと、処理のないデフォルトコンストラクタを使うことができます。Class.cpp にもコンストラクタの記述は必要ありません。
リストを返すには using namespace winrt::Windows::Foundation::Collections;
を書いた上で IVectorView<T>
インターフェイスを使うことができます。これはC# (.NET) 側から参照すると IReadOnlyList<T>
に見えます。1
また、Unicode文字列を扱うときは hstring
型を使います。C# (.NET) 側から参照すると string
に見えます。
Class.cpp
#include "pch.h"
#include "Class.h"
#include "Class.g.cpp"
using namespace winrt::Windows::Foundation::Collections;
using namespace winrt::Windows::Globalization;
namespace winrt::RuntimeComponent1::implementation
{
IVectorView<hstring> Class::Analyze(hstring text)
{
auto words = JapanesePhoneticAnalyzer::GetWords(text);
auto result = winrt::single_threaded_vector<hstring>();
for (auto word = words.First(); word.HasCurrent(); word.MoveNext()){
result.Append(word.Current().DisplayText());
}
return result.GetView();
}
}
使用したい機能に合わせて適切に using namespace
を記述してください。
また winrt::single_threaded_vector<hstring>()
を使って IVector
オブジェクトの実体を作っています。これを忘れると、NULLポインタアクセスになってアクセス違反となります。
Class.idl
namespace RuntimeComponent1
{
[default_interface]
runtimeclass Class
{
Class();
static Windows.Foundation.Collections.IVectorView<String> Analyze(String text);
}
}
idlでクラスの名前空間を書くときはドット区切り (Windows.Foundation.Collections.IVectorView
) になります。
また、文字列型はidlでは String
となります。
pch.h
#pragma once
#include <unknwn.h>
#include <winrt/Windows.Foundation.h>
#include <winrt/Windows.Foundation.Collections.h>
#include <winrt/Windows.Globalization.h>
使いたい機能に合わせてヘッダファイルの記述を追加します。
C#側
private void Button_Click(object sender, RoutedEventArgs e)
{
var result = RuntimeComponent1.Class.Analyze("本日は晴天なり");
MessageBox.Show(string.Join(",", result));
}
C#側のプロジェクトで、参照にC++側のプロジェクトを追加してビルド・実行し、「本日,は,晴天,なり」と単語単位で区切られたメッセージが出たら成功です。
身も蓋もない話
正直この程度だったらブリッジを作るまでもなくて、以下の方法でもっと手軽にできたりします。
[C#] デスクトップアプリ (WPF) から手軽にWinRT APIを活用しよう - Qiita
ただポインタやATLなど色々触り出すと、下手にC#で書くよりC++でブリッジを作ったほうがたぶん楽です。
トラブルシューティング
ブリッジクラスがC#側から見えない
C#側から参照にC++プロジェクトを追加していても見えない場合は、idlファイルの記述をご確認ください。
idlファイルを編集したら「リビルド」したほうが良いです。
エラー C2660 'winrt::RuntimeComponent1::implementation::Class::MyMethod': 関数に 1 個の引数を指定できません。
Class.idl に書いた名前と引数で呼び出せるメソッドが、Class.cpp, Class.h 側に定義されていないと思われます。(オーバーロードは可能ですが、idlの記述に不足がないようにご注意ください)
System.TypeLoadException: '要求された Windows ランタイム型 'RuntimeComponent1.Class' は登録されていません。'
app.manifest の <file>
や <activatableClass>
の記述が不足しているようです。
System.BadImageFormatException: ' は有効な Win32 アプリケーションではありません。 (HRESULT からの例外:0x800700C1)'
C++側とC#側のプラットフォーム設定が合っていないみたいです。x64同士、またはx86/Win32の組み合わせであることを確認してください。前述のように Any CPU はダメです。
System.IO.FileNotFoundException: '指定されたモジュールが見つかりません。 (HRESULT からの例外:0x8007007E)'
まずは RuntimeComponent1 のビルドが成功していて、C#側のプロジェクトで指定したexeの出力先と同じ場所に RuntimeComponent1.dll がコピーされているか確認してください。
もし RuntimeComponent1.dll があるのにこのエラーが出るときは、VCRT ForwardersのDLLが見つからないのが原因と思われます。
- NuGetパッケージマネージャーの設定で、既定のパッケージ管理方法を「Packages.config」にする
- C#側のプロジェクトに Microsoft.VCRTForwarders.140 を追加する(既に追加されている場合で、後からPackages.configの設定を変えたときは、VCRTForwardersを一度削除して再度追加する)
ビルドしたあとに bin\x64\Debug
の下などに msvcp140
や vcruntime140
などの名前で始まるDLLが大量にコピーされたら成功です。
0x00007FF90CD1DD44 (RuntimeComponent1.dll) で例外がスローされました (WpfApp1.exe 内): 0xC0000005: 場所 0x0000000000000000 の読み取り中にアクセス違反が発生しました
IVector
の初期化ができているかご確認ください。winrt::single_threaded_vector
をお忘れなく。