LoginSignup
8
10

More than 3 years have passed since last update.

[C#] C++/WinRTのブリッジを作ってC#から呼び出す方法

Last updated at Posted at 2020-09-27

はじめに

Windows 10では、UWP(ストアアプリ、ネイティブアプリ)から使えるいろいろな新しいAPI (WinRT) が追加されています。
強力な機能は魅力的ですが、UWPアプリはサンドボックス内で実行されるので、セキュリティによって実装に制約が生じたり、スタートメニューからしか起動できない、社内などの他の人への配布が大変(ストア経由でないアプリを使うためには設定が必要)と、少々使いづらい点もあります。
そこで、Windowsの普通の(exeをダブルクリックして起動できる)デスクトップアプリから、WinRTの機能を「つまみ食い」できると便利だと思います。

そのための仕組みとして「C++/WinRT」があります。

しかし、アプリを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 をビルド・実行できることを確認しましょう。
image.png

自分でブリッジを書く

いよいよここからが本番です。以下が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つ追加しておきます。

Class.h
#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>
    {
    };
}
Class.cpp
#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++に似ています。

Class.idl
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)。
image.png
WpfApp1から、先ほど作成したRuntimeComponent1を参照します。
image.png
これによって RuntimeComponent1 という名前空間が見えるようになり、その下にある Class というクラスにアクセスできるはずです。

フォームに適当にボタンを配置し、ボタンを押したらクラスを使って何か処理させてみます。

MainWindows.xaml.cs
/* 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に入っていますよ」という情報を追加してあげないといけません。
プロジェクトに「アプリケーション マニフェスト ファイル」を追加します。
image.png

今回作成したブリッジと、そこに含まれているクラスの情報を追加します。

app.manifest
<?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

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

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

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

pch.h
#pragma once
#include <unknwn.h>
#include <winrt/Windows.Foundation.h>
#include <winrt/Windows.Foundation.Collections.h>
#include <winrt/Windows.Globalization.h>

使いたい機能に合わせてヘッダファイルの記述を追加します。

C#側

MainWindows.xaml.cs
        private void Button_Click(object sender, RoutedEventArgs e)
        {
            var result = RuntimeComponent1.Class.Analyze("本日は晴天なり");
            MessageBox.Show(string.Join(",", result));
        }

C#側のプロジェクトで、参照にC++側のプロジェクトを追加してビルド・実行し、「本日,は,晴天,なり」と単語単位で区切られたメッセージが出たら成功です。
image.png

身も蓋もない話

正直この程度だったらブリッジを作るまでもなくて、以下の方法でもっと手軽にできたりします。
[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 の下などに msvcp140vcruntime140 などの名前で始まるDLLが大量にコピーされたら成功です。
image.png

0x00007FF90CD1DD44 (RuntimeComponent1.dll) で例外がスローされました (WpfApp1.exe 内): 0xC0000005: 場所 0x0000000000000000 の読み取り中にアクセス違反が発生しました

IVector の初期化ができているかご確認ください。winrt::single_threaded_vector をお忘れなく。

8
10
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
8
10