catalinaの備忘録

ソフトウェアやハードウェアの備忘録。後で逆引きできるように。

WinRTを試してみる

桜の花も散っていき、春から初夏への移り変わりを感じます。かたりぃなです。

今回は新年度ということで、新しいことを試してみようと思います。C++/WinRTです。

C++/winRTを使う理由

Hololens用のUnityのネイティブプラグインを書こうと色々調べていたら、C++/CX終わるよみたいなアナウンスに気づきました。

https://docs.microsoft.com/ja-jp/cpp/cppcx/visual-c-language-reference-c-cx?view=vs-2019

要約すると、「C++17とWinRTを使うと、幸せになれるよ」ってことですね。

というわけで、今後のことも考えてwinrtを試してみます。ダメだったら戻ればいいかと思って試したところ、かなりイケてると思います。

なのでWinRT/C++でいきます。

WinRTとは

winRTはc++17の機能を使うことでC++/CXよりエレガントに記述できます。 実際に書いてみるとわかるのですが、C++使いなら直感的に記述できる感じでした。

プラットフォームの観点からは

  • UWPアプリを書ける
  • ネイティブアプリも書ける(従来のデスクトップアプリ)
  • Unityプラグインとしてもいける(はず?)

コードを書く観点から

  • 非同期操作が扱いやすくなっている(concurrencyTaskなんて無かった)
  • プロパティへのアクセスが直感的
  • イベントハンドラ登録が直感的に書ける
  • COMへの参照はwinrtの中で良しなにやってくれる

とかでしょうか。 特に非同期操作がラクになったのが大きくて、意味不明なテンプレートのエラーを見る回数が減ったと思います。

今回はまだ非同期操作まわりはよくわかってないので、同期的な処理で完結しています。

実際にwinrtでコード書いてみる

最終目標はunity ネイティブプラグインです。

なので、visual studio プロジェクトとしてはDLLになります。

また、DLLのテスト用にコンソールアプリケーションを作成します。

UWPアプリケーションからDLLを呼び出すテストはまた今度。。。(Hololens実機でのネイティブプラグイン相当)

環境

いつものです。

環境設定

Visual Studio 2017に色々いれたのですが、試行錯誤していたせいで失念してしまいました。

VisualStudioでオンラインのWinRTテンプレートをインストールか、VisualStudio のインストーラからコンポーネントを指定したかもしれません。

設定が終わると新規作成プロジェクトからC++/WinRTが選択できるようになります。

プロジェクト構成

VisualStudioのプロジェクトとしては2つ作ります。 1つはc++/winRTを使用したDLLそのものので、もう一つはテスト用プロジェクトです。

ソリューションにこの2つのプロジェクトを配置することにします。

このDLLは最終的にUnity側で利用するわけですが、テストのたびにUnityとVSを行ったり来たりしてのデバッグは大変なので、テストプロジェクトで普通のDLLとして扱えることを確認しておきます。

WinRTのDLLプロジェクトを作成する

WinRTのDLLプロジェクトのテンプレートが手元にないので、手作業で作ります。

具体的には次の手順です。

  1. 新規プロジェクト作成から、VisualC++ -> Windowsデスクトップ -> DLL を選択
  2. プロジェクトの設定値を変更する

これでWinRTでDLLが作成できるようになりました。

WinRTを利用するためのプロジェクト設定の手順はこちらのサイトを参考にさせていただきました。 https://blog.okazuki.jp/entry/2018/10/16/144556

実際のDLLのコードは後で示します。

テストプロジェクトを作成する

DLLをテストするプロジェクトとしてコンソールアプリケーションを作成します。 テストプロジェクトは、上記DLLプロジェクトと同じソリューションに含まれるようにしておきます。

具体的には ソリューションエクスプローラのroot要素(ソリューション)を右クリックして、

追加->新しいプロジェクト->コンソールアプリケーション

です。

テストプロジェクトは同じくコンソールアプリケーションを使います。

2つのプロジェクト間の依存関係の設定

テストプロジェクト側をスタートアッププロジェクトに設定し、プロジェクトの依存関係からDLLが先にビルドされるよう設定しておきます。

ついでに関数の呼び出し規約をstdcallに設定します。

ソリューションエクスプローラのプロジェクトを右クリックして「プロパティ」「C++タブ」「詳細設定」「呼び出し規約」のところを「stdcall/Gz」に設定します。

あとはビルドターゲットを

  • Releaseビルド
  • 32bit,64biをUnityのバージョンと合わせる

とします。

テストプログラムから呼び出す

DLLを呼び出すにはWindowsAPIでゴリゴリ(LoadLibrary, GetProcAddr)とかやってもいいのですが、手抜きでいきます。

DLLプロジェクトがDLLとインポートライブラリを生成してくれているので、インポートライブラリ利用します。

プロジェクト設定画面から、 インポートライブラリはリンカの設定で、DLL本体はデバッグ時のパスの設定で解決します。

リンク設定とデバッグ環境のパス設定はこんな感じです。

リンク設定

リンカ-> 入力-> 追加の依存ファイルに $(SolutionDir)x64\release\consoleDll.lib;%(AdditionalDependencies) を設定します。 ここでconsoleDllというのはDLL側プロジェクトの名前です。

デバッグ環境のパス設定

デバッグ-> 環境 の項目にPATH=$(SolutionDir)x64\release\を設定します。

ここまででDLLの生成とそれを読み込むテストプログラムの設定ができました。

設定が正しいかどうかDLLとアプリケーションをビルドして確認しておきます。パス指定を間違えたりしているとリンク時やデバッグ時にエラーが出るので状況確認してつぶしていきましょう。

実際にコードを書いてみる(DLL側)

単純なDLLではデスクトップアプリを作る場合と何も変わらないので、UWPで利用できるAPIを呼び出してみることにします。

昔の記事でC++/CXでやってたMediaCaptureを使ってみます。

#include <thread>

#pragma comment(lib, "windowsapp")

#include <winrt/Windows.UI.Notifications.h>
#include <winrt/Windows.Data.Xml.Dom.h>

using namespace winrt;
using namespace Windows::Foundation;
using namespace Windows::UI::Notifications;

#include "winrt/Windows.Foundation.h"
#include "winrt/Windows.Web.Syndication.h"
using namespace winrt;
using namespace Windows::Foundation;
using namespace Windows::Web::Syndication;

#include <winrt/Windows.Media.h>
#include <winrt/Windows.Media.Capture.h>
#include <winrt/Windows.Media.Capture.Frames.h>

using namespace Windows::Media::Capture;
using namespace Windows::Media::Capture::Frames;

// 新しいメディアフレームが到着した際にMediaCaptureから呼び出されるコールバックイベントハンドラ
void OnFrameArrived(
    MediaFrameReader sender,
    Windows::Media::Capture::Frames::MediaFrameArrivedEventArgs args)
{
    OutputDebugString(L"OnFrameArrived called \n");
    auto frame = sender.TryAcquireLatestFrame();
}


// MediaCaptureのための状態をまとめて管理する
struct MediacaptureWrapper {
    MediacaptureWrapper(MediaCapture cap, MediaFrameReader rd) :
        mc(cap),
        reader(rd) {};

    MediaCapture        mc;
    MediaFrameReader    reader;
};

// todo : できることならDLL利用側で管理してもらう
std::shared_ptr<MediacaptureWrapper>  g_capture;


extern "C" {
    // unityプラグインのP/Invoke呼び出し規約はstdcall
    __declspec(dllexport) int __stdcall mediatest(int a, int b) {
#if 1
        // todo : これはUnityエディタのときは呼び出してはいけない
        //        実行時環境を参照する方法を調べて、自動的に呼び出しを制御する方法を考える。
        winrt::init_apartment();

        // media capture の関数を一戸だけ呼び出すテスト
        // これ自体は問題なく成功するので、UnityからWinRTのAPIを呼びだすこと自体は問題ない
        // Win32アプリケーションでテストコードをかいて実験する必要あり
        auto groups = MediaFrameSourceGroup::FindAllAsync().get();

        MediaFrameSourceGroup selectedGroup = nullptr;
        MediaFrameSourceInfo selectedSourceInfo = nullptr;

        // 列挙したカメラの最初に見つけたものを使用する。
        // Hololens RS-4で確認したところ、NV12カラーフォーマットで列挙された
        for (MediaFrameSourceGroup sourceGroup : groups) {
            for (MediaFrameSourceInfo sourceInfo : sourceGroup.SourceInfos() ) {
                if (sourceInfo.SourceKind() == MediaFrameSourceKind::Color) {
                    selectedSourceInfo = sourceInfo;
                    break;
                }
            }

            if (selectedSourceInfo != nullptr) {
                selectedGroup = sourceGroup;
                break;
            }
        }

        // 利用可能なカメラが見つからない場合
        if (selectedGroup == nullptr || selectedSourceInfo == nullptr) {
            OutputDebugString(L"selected source group not found\n");
            // todo
        }

        // たぶんプロパティと同様の扱いでいいんだろうけど、どうなの?
        auto settings = MediaCaptureInitializationSettings();
        settings.MemoryPreference(MediaCaptureMemoryPreference::Cpu); // Need SoftwareBitmaps for FaceAnalysis
        settings.StreamingCaptureMode(StreamingCaptureMode::Video);   // Only need to stream video
        settings.SourceGroup(selectedGroup);

        // よくわかってないところ。
        // winrtのオブジェクトに対してnew演算子が使えなくて、こういう方法でインスタンス化できるっぽい?
        // ただ、MSDNにあるサンプルではインターフェースをもつクラスインスタンスを作る例なので、MediaCaptureではこれが正解かどうかは謎い
        auto mcaf = winrt::get_activation_factory<MediaCapture>();
        auto mc =  mcaf.ActivateInstance<MediaCapture>();

        // 使用するカメラのパラメータが確定したので、初期化を行う
        // 1, メディアキャプチャの初期化
        // 2, 読みだしストリームの作成
        // 3, キャプチャの開始
        mc.InitializeAsync(settings).get();
        auto mapview = mc.FrameSources();
        auto selectedSource = mapview.Lookup(selectedSourceInfo.Id() );

        auto media_reader = mc.CreateFrameReaderAsync(selectedSource).get();
        auto status = media_reader.StartAsync().get();

        // https://docs.microsoft.com/ja-jp/windows/uwp/cpp-and-winrt-apis/handle-events
        // デリゲートを使ってイベントを処理する方法
        media_reader.FrameArrived({&OnFrameArrived});

        g_capture = std::make_shared<MediacaptureWrapper>(mc, media_reader);

        if (status == MediaFrameReaderStartStatus::Success) {
            // mediacapture, mediaframereaderのペアができた
            // 選択されているカメラはselectedSourceである
            OutputDebugString(L"media capture start success\n");
        }
        else {
            // メディアキャプチャの読み出しが開始できない
            OutputDebugString(L"media capture start fail \n");
        }
        std::this_thread::sleep_for(std::chrono::seconds(5) );
#endif
        // 単純なDLLのテスト用として、加算した結果を返すだけ。
        return a + b;
    }
}

次にテストプログラム側です。

ライブラリのリンクはできているので、単純に呼び出すだけです。

int main()
{
    std::cout << "Hello World!\n"; 

    // dll のテスト
    mediatest(1,2);

    std::cout << "finish.\n";
}

コードの概要(全体像)

DLLはmediatestという関数をエクスポートし、テストプログラム側はこれを利用します。 引数、戻り値にint型をとっていますが、DLLが呼べるかなどの基本的なテスト用に残しています。

このコードは実行すると5秒間カメラから映像フレームを取得し続けます。

映像フレームは保存せずに捨てるだけなので害はありません。

コードの詳細(DLL側)

DLL側ではwinRTを使った実装をしています。慣れない部分は多々ありますが、理解した範囲でメモしておきます。

C++マングリング

ネームマングリングについて雑に説明します。

C++には関数のオーバーライドとか、デフォルト引数みたいな機能がたくさんあります。 これはC言語には存在していないものです。

関数を拡張したような機能を実現するためにC++コンパイラはネームマングリングといって、内部的に異なる関数名を生成します。

(リンクエラーで関数が見つからないときに記号や数字が付いた関数名が表示されると思いますが、アレがコンパイラ内部で扱われている関数名相当です。)

これがネームマングリングと呼ばれるもので、これだとDLLとして呼び出すときに非常に不便です。(マングル規則を知らないと呼び出せない)

そこで、「ネームマングリングしないで。Cの関数として扱って」というのがextern "C"です。

WinRTライブラリの指定

#pragma comment(lib, "windowsapp") は、ライブラリのリンクの指定で、ここではwindowsappのライブラリ一式をリンクする指定です。

includeとかusing

使う機能をincludeしてusingで以降の名前空間の指定を省略するという使い方です。

ここで注意点で、たとえばinclude <winrt/Windows.Media.Capture.Frames.h>を消すと、コンパイルは通るがリンクエラーになるという事態が起きます。

(これは公式マニュアルのFAQにもあるので読んでみるのが良いでしょう)

なぜコンパイルエラーではなくリンクエラーかというと、おそらくC++の「テンプレートの明示的インスタンス化」の影響だと考えています。(ヘッダオンリーライブラリにありがち)

たとえば#include <winrt/Windows.Media.Capture.h>をすると、MediaCaptureの入出力でFramesクラスを使うので「Framesクラスがあるよ」宣言が必要がありますが、ここでは実態は定義されません。で、テンプレートのインスタンスはFrames.h側にあるので、リンク時に問題になるわけですね。

順番ごちゃごちゃして汚いですが、こんな感じになりました。

#include <winrt/Windows.UI.Notifications.h>
#include <winrt/Windows.Data.Xml.Dom.h>

using namespace winrt;
using namespace Windows::Foundation;
using namespace Windows::UI::Notifications;

#include "winrt/Windows.Foundation.h"
#include "winrt/Windows.Web.Syndication.h"
using namespace winrt;
using namespace Windows::Foundation;
using namespace Windows::Web::Syndication;

#include <winrt/Windows.Media.h>
#include <winrt/Windows.Media.Capture.h>
#include <winrt/Windows.Media.Capture.Frames.h>

using namespace Windows::Media::Capture;
using namespace Windows::Media::Capture::Frames;

エクスポート関数の指定と呼び出し規約の指定

 __declspec(dllexport) int __stdcall mediatest(int a, int b) {

DLLが外部に公開する関数だということを明示します。今回はテスト用に1個の関数だけexportするので、defファイル書くまでもないなと思い、こうやってます。

__stdcallはプロジェクト設定でやってもいいですし、こうやって各関数個別に指定してもいいです。

COMの初期化

winrt::init_apartment()

winrtの内部実装はCOMらしいので、その初期化をします。 こいつも注意で、今回のテストプログラムから使うときは必要ですが、Unityから使う場合は消しておく必要があります。(Unityの中でCOMの初期化やってる?) まあ、DirectXもCOMですし、何かやっててもおかしくはないでしょう。

非同期操作(async)

非同期操作関連はコルーチンを使えばきれいに書けるみたいですが、コルーチンはまだよくわからないので同期的に処理することにしました。

たとえば auto groups = MediaFrameSourceGroup::FindAllAsync().get()では、FindAllAsync非同期操作の完了まで待機して結果を受け取ります。 もちろんFindAllAsync()を一旦変数で受けてからgetするというのもできます。

コレクションの処理

IVectorViewなどのWindows固有のコレクションも、範囲forで片づけます。

for (MediaFrameSourceGroup sourceGroup : groups)とかですね。

今回は勉強なので、各要素の型を明示しましたが、autoで受けても大丈夫でした。

プロパティ

C#のプロパティのように、クラスのフィールドにアクセスするには

  • 代入時は関数呼び出しの構文で
  • 取り出し時は変数 = プロパティ

と覚えておけばだいたい事足りました。

たとえばメディアキャプチャの設定をするのはこんな感じですね。

 auto settings = MediaCaptureInitializationSettings();
    settings.MemoryPreference(MediaCaptureMemoryPreference::Cpu);
    settings.StreamingCaptureMode(StreamingCaptureMode::Video);
    settings.SourceGroup(selectedGroup);

インスタンス

ここはまだ理解しきれていないので自信ないところです。

C++/CXのときはref new ClassName()とかしてた部分になります。

一旦activation_factoryテンプレートクラスを、インスタンス化したいクラス名のテンプレートとして構築し、そのactivationfactoryのActivateInstanceメソッドを呼ぶことでインスタンス化できるようです。

 auto mcaf = winrt::get_activation_factory<MediaCapture>();
    auto mc =  mcaf.ActivateInstance<MediaCapture>();

イベントハンドラ

イベントハンドラはコルーチン(?)に対するデリゲートでいけました。

media_reader.FrameArrived({&OnFrameArrived});

すごくシンプルですね。c++/WinRT素晴らしいですね。

一番楽になったのはコールバック関数をコルーチン(?)として与えることができるので、すごく楽になったと感じます。

ちなみにC++/CXではstd::bindにプレースホルダー指定しつつ、テンプレートのインスタンス化みたいな難読なコードでした。

できた

ここまでで、テストプログラムを実行するとコールバック関数が呼び出されることが確認できます。

DLL側にブレークポイントを打ってデバッグすることも可能です。

軽くUnityエディタで動かしてみたところ、問題なく動いたので、これを利用してネイティブプラグインの開発ができそうです。

感想と今後の展望

ここまでで、UWPのMediaCaptureを利用するDLLができました。

本当はUnityエディタから利用する部分まで書こうかと思っていたのですが、記事が長くなりそうなので今回はこれくらいにしておきます。

次回はUnityのネイティブプラグインのファイル配置、開発環境、デバッグ方法などを書いてみようと考えています。

少しずつですがUnityプラグイン作って色々やっていきたいと思います。

それでは今回はこれくらいで。