Desktop App Converterで変換後のアプリ運用について

開発の橋本孔明です。

Desktop App Converter関連の連載第三回をお届けします。今回は、Desktop App Converter(以下、DAC)で変換したあとのアプリ運用について、サンプルを2つ用意した上で、より具体的かつ実践的な内容を紹介したいと思います。

まず用語定義ですが、本記事では、「デスクトップ アプリをDACで変換してMicrosoft ストアに登録した上、ストア経由でアプリをインストール・実行している状態(あるいは手動でAppXパッケージを生成・インストール・実行している状態)」のことを「ブリッジ モード」と呼称し、従来の、通常通りにEXEファイルを起動したモードを「デスクトップ モード」と呼称することにします。
ブリッジ モードのことを「UWPモード」、変換したアプリを「UWPアプリ」などと記載しているのも見かけることがありますが、これまでの連載でも記載してきたように、変換したからといってUWP上で動作しているわけではなく、(のちほど説明しますが)実はUWP APIをデスクトップ モードで利用することも可能であるため、あまり適切な用語ではないと考えています。

ファイルへの関連付け設定

ブリッジ モードのアプリでは、ファイル拡張子への関連付けを従来通りの手法(レジストリへの登録)では行うことができず、パッケージ マニフェストに関連付けの宣言を追加する必要があります。
今回のサンプルでは、拡張子「abc」と「xyz」の2種類のファイル形式を関連付けする宣言を追加してあります。詳しくは、マイクロソフトから「Desktop Bridgeアプリの拡張機能」というオンライン ドキュメントが提供されていますので、そちらを参照してください。

デスクトップ アプリからUWP APIを呼び出す

DACやブリッジ モードの話から少し逸れますが、実はデスクトップ モードの従来型アプリからであっても、Windows 10であれば大半のUWP APIを利用することができるようになっています。Microsoft ストアの課金システムやコントラクト・位置情報などといった機能です(ただし、XAML UIに関する機能はブリッジ モード含めて非サポート)。

C#であれば参照を追加する程度で比較的簡単にUWP APIを利用できるのですが、今回念頭に置いているのはWin32 SDKとC++(C++/CXではない)で書かれている、昔ながらのアプリです。C++コードからは、<roapi.h>というヘッダファイルで宣言されているAPI関数群(以下、roapi)と、それをラッピングしたWRL(Windows ランタイム C++ テンプレート ライブラリ)というC++クラス ライブラリを利用することで、UWP APIを呼び出すことができます。

以前、本ブログの記事で記述したことがあるように、UWPはWindows 8で導入された「WinRT(Windows Runtime)」をベースとして大幅拡張したAPIセットです(現在も各所にWinRTの名称が残っています)。roapiやWRLは、Windows 8の頃からWindows SDKで提供されていたものであり、UWPでもほぼそのまま利用できます。
UWP(WinRT)APIは、実はWindows 95の頃からあるCOM(Component Object Model)をベースとして実装されています。C++コードにおいては、「IUnknown」から派生したインターフェース(UWPではもう1段階、メタデータに関する情報を提供する「IInspectable」が共通の祖先になります)を基にオブジェクト インスタンスを生成し、メソッドを呼び出し、Release()でインスタンスを解放する…といった手順でAPIを呼び出しますが、その手順は見ての通りCOMそのものです。ただ、初期化・オブジェクト生成といった部分にはCOMオリジナルとは異なる専用のAPIがあり、それらをC++から利用しやすくするためにWRLライブラリが提供されています。

roapiやWRLについて詳しく解説を始めるとそれだけで長くなってしまうため、今回はばっさりと省略していますが、今回提供しているサンプル アプリではWRLを用いてUWP APIを呼び出していますので、Microsoftのドキュメントなどと併せて参考にしてください。

動作モードの判別

DACで変換してMicrosoft ストアに登録する場合でも、既存のデスクトップ アプリとしてのリリース形態を併行して続けるというケースが多いかと思います。
これまで紹介したように、ストア登録用のパッケージに格納するプログラム ファイルそのものには、なんらかの特殊な変換がかかるわけではありません。ですので、ストア登録用と従来通りのリリース用に、同じプログラム ファイルを使い回すことができます。
しかし、ブリッジ モードとデスクトップ モードでは、一部の機能に蓋をしたり、動作自体を変えたいというケースが多くあります。例えば、最新版への自動アップデート機能(ブリッジ モードでは、Microsoft ストア経由以外でのアップデートは許可されません)や、ファイルへの関連付け設定機能(前述のようにマニフェストでの宣言が必要)などです。そのためには、現在実行しているプロセスがブリッジ モードなのかどうかを確認する方法が必要です。

実は、現在実行しているプロセスがブリッジ モードなのかどうかをダイレクトに判定できるAPIというのは用意されていません。
Microsoftのドキュメントには、「Desktop Bridgeで変換されたアプリでサポートされているUWP API」というページがあり、ブリッジ モードで動作しているアプリから利用できるUWP APIが解説されています。

一見すると、ブリッジ モードでないとこれらのAPIを呼び出すことはできないかのように読めますので、UWP APIが動作するかどうかでブリッジ モードかどうかを判定することができるかのように見えます。
しかし、前述したように、実質的には大半のUWP APIがデスクトップ モードからでも使えてしまうため、本当にブリッジ モードでなければ動作しないAPIを調べ、それを呼び出すことで、擬似的にブリッジ モードかどうかを判定することにします。そのようなAPIにはいくつか候補がありますが、今回は2つの方式を紹介します。

1. パッケージ名の有無で判定する

kernel32.dllからエクスポートされている、GetCurrentPackageFullName() というAPIで、アプリのパッケージ名を取得できます。
パッケージ名は、アプリ パッケージからインストールされたアプリ上にしか存在しませんので、その有無でブリッジ モードかどうかを判定できます。
なお、Windows 7にはこのAPIが存在しませんので、Windows 7でも動作させるためには、GetProcAddress() を用いて動的にGetCurrentPackageFullName() APIを呼び出す必要があります(APIが無ければ強制的にデスクトップ モードと判定します)。

2. タイルAPIの有無で判定する

アプリのインストール時にスタート メニューに登録される「タイル」に関連した機能は、デスクトップ モードから呼び出しても動作しません。
そこで、呼び出しに失敗するかどうかでブリッジ モードかどうかを判定できます。

今回提供するサンプル アプリでは、上記の2つの判定方法を用い、以下の関数を実装しました。

bool api_IsBridgedAppMode_1(bool& bBridgedMode); // パッケージ名の有無で判定するバージョン
bool api_IsBridgedAppMode_2(bool& bBridgedMode); // タイルAPIの動作可否で判定するバージョン

引数のbool値に、ブリッジ モードかどうかの判定結果が格納されます(関数の戻り値がfalseの場合は、初期化失敗など致命的エラーを示しています)。

今回提供する「dac_testapp」は、ブリッジ モードとデスクトップ モードの両方に対応したサンプル アプリです。

dac_testapp.exeをデスクトップ モードで実行した場合

dac_testapp.exeをアプリ パッケージ(AppX)化し、ブリッジ モードで実行した場合

ブリッジ モードのアプリを引数付きで起動したい

デスクトップ モードのアプリから、CreateProcess()やShellExecuteEx()といったAPIで別のアプリを呼び出すケースは多く見られます。単なるランチャー機能のみならず、例えばデータの編集用として外部プログラムを利用している場合などです。
こういった場合、片方または両方のアプリがブリッジ モードになってしまうと、従来通りのやり方では動作させることができなくなります。また、ブリッジ モードのアプリは、そのままではデスクトップ モードのEXEファイルのようにコマンドライン引数を付けて起動することができません。
ブリッジ モードのアプリを引数付きで起動するためには、プロトコルという機構を利用します。そのためには、パッケージ マニフェストの<Package><Applications><Application>タグ内に以下のような宣言を追加します。

<Extensions>
	<uap3:Extension Category="windows.protocol">
		<uap3:Protocol Name="dactestapp-sample-command" Parameters="&quot;/FromProtocol:%1&quot;" />
	</uap3:Extension>
</Extensions>

上記の宣言を含むアプリがインストールされていると、「dactestapp-sample-command」というプロトコルでアプリの起動ができるようになります。
このプロトコルは「http:」や「ftp:」、「about:mozilla」などと同じ扱いで、コロンでプロトコルであることを示し、パラメーターを追加してURIを構成します。パラメーター部はマニフェスト宣言に従って書式が整えられ、ブリッジ モードで起動したプロセスにコマンドライン引数として渡されます。

今回のサンプル アプリ(dac_testapp)では、コマンドライン引数があればその内容をそのままメッセージ ボックスで表示するようにしてあります。dac_testapp.exeをデスクトップ モードで引数付き起動した場合と同様、プロトコル経由で起動した場合にもパラメーター文字列が表示されるのを確認できます。

「ファイル名を指定して実行」から、プロトコルでアプリの起動ができます

上記のコマンドで起動した場合、コマンドライン引数にパラメーターが入っています

上記の例では、「dactestapp-sample-command:hello world」というURIを実行した場合、URI文字列をマニフェスト宣言のParameters(「&quot;/FromProtocol:%1&quot;」)の「%1」の部分に代入して「”/FromProtocol:dactestapp-sample-command:hello world”」という文字列が生成され、これをコマンドライン引数としてブリッジ モードのプロセスが起動されます。
ブリッジ モードのアプリ側では、コマンドライン引数が「/FromProtocol」で始まっている場合、ブリッジ モードでプロトコル経由の起動が行われたものと判断し、パラメーター文字列を解析して対応した処理を行うことができます。

さて、「ファイル名を指定して実行」でのプロトコル起動ができることは確認しましたが、プログラム コード上から同じ処理を行うためにはどうすればよいでしょうか。もちろん、これもUWP APIで可能です。
ただ、手順がいろいろと面倒なので、簡易的に使えるユーティリティ関数を2つ用意しました。

bool api_QueryUriSupport(LPCWSTR lpszUri, bool& bSupported);

プロトコルが使用可能(≒ブリッジ モードのアプリがインストールされている)かどうかをチェックします。今回の例ですと、lpszUriに「dactestapp-sample-command:」という文字列を渡します。結果はbSupportedに格納されます。

bool api_LaunchUri(LPCWSTR lpszUri);

プロトコルおよびパラメーターを含むURIを実行します。

今回提供するもうひとつのサンプル アプリ(dac_client)は、これらの機能を確認するものです。api_QueryUriSupport()でdac_testappのプロトコルがサポートされているかを調べ、api_LaunchUri()で「dactestapp-sample-command:hello world;parameter1=100;parameter2=101」というプロトコルURIを起動します。dac_testapp側では、この文字列をセミコロンや等号を区切りとして分解すれば、追加パラメーターを取得できるわけです。

デスクトップ モードのアプリ(dac_client.exe)からブリッジ モードのアプリ(dac_testapp)をパラメーター付きで起動した例

なお、ブリッジ モードのアプリを起動した場合、その終了待ち(デスクトップ モードでいうところの、CreateProcess()+GetExitCodeProcess()またはWaitForSingleObject()に相当)をプログラム コードから簡単に行う方法は残念ながら無いようです。ウィンドウ メッセージを通じて信号をやりとりするといった、追加の対策が必要となります。

実装時の注意点

既存のC++コードで記述されたアプリに手を加え、UWP関連の機能を追加したい…という場合や、アプリをブリッジ モードで動作させた場合に、遭遇または発生しやすい問題がいくつかあります。

OSのバージョン チェックAPIの挙動

現在実行しているWindowsのバージョンをコードから判定するためには、GetVersionEx() API(現在は非推奨)やVerifyVersionInfo() API、あるいは<VersionHelpers.h>ヘッダファイルで定義されているヘルパー関数(IsWindows10OrGreater() など)を利用しているかと思います。
しかし、Windows 8.1およびWindows 10では、プログラムの動作互換性を高めるため、プログラム ファイルに埋め込むマニフェストにおいて、各OSへの対応宣言を記述していないと、常に「Windows 8」のバージョン数値(6.2)が返されるようになっています。
例として、マニフェストで宣言を行っていないプログラムでは、Windows 10上でIsWindows10OrGreater() 関数を呼び出しても、戻り値はfalseとなります。

しかし、ブリッジ モードで動作するプログラムでは、マニフェスト宣言の有無に関わらず、OSバージョン判定関数群がすべて正しい値を返すようになります。
このため、Windows 8より新しいOSバージョンを検知したら動作しないようなコーディング(本来はあまり推奨されませんが)を行っていたプログラムでは、ブリッジ モードで動作させることで、(これまでWindowsの互換機能によって防がれていた)問題が再現する可能性があります。
アプリにそのようなチェック コードが存在する場合は、ブリッジ モードで正しく動作できるかどうかを再検証するようにしてください。

SDKの選択

roapiやWRLを使うために必要なヘッダ ファイルは、Windows 8 SDKから含まれていますが、UWP API(Windows 8のWinRTの頃には存在しなかったもの)のインターフェース宣言などについてはWindows 10 SDKが必要です。プロトコルURIを起動するためのインターフェースなどがこれに該当します。今回のサンプルは、Visual Studio 2015で作成されており、プロジェクト プロパティの「ターゲット プラットフォーム バージョン」にWindows 10のSDKを指定してあります。
しかし、プロジェクト全体で使用するSDKをWindows 10に切り替えると、SDK側定義のマクロの値などが変化することで、Windows 7やWindows 8などでの実行に支障するコードが生成されてしまう可能性もあります(ただし、現在のところそのような問題に実際に遭遇したことはありません)。
これを回避したい場合は、UWPを使用する部分のみをスタティック ライブラリ(またはDLL)として個別にビルドし、それをリンクして使うという手法が利用できます(今回のサンプルですと「winrt.cpp」単体がこれに該当します)。

COM初期化の競合問題

前述のように、UWPのAPIはCOM技術をベースに実装されており、UWPの初期化はCOMの初期化処理を内部で呼び出しています。
既存のアプリに手を入れる場合、そのアプリがCOM・ActiveX・OLEなどの機能をすでに利用している場合があります。これらの機能にはいずれも初期化処理(CoIniitialize()/CoInitializeEx()/OleInitialize())が必要ですが、プログラム冒頭でその呼び出しをすでに行っている場合、UWPの初期化(RoInitialize()、あるいはそれを名前空間でラップしたWindows::Foundation::Initialize())を追加で呼び出すには都合が悪いことがあります。
これは、それぞれの機能で必要とされるCOMスレッド アパートメント モデルが異なる場合があるためです。例えばOLEはシングル スレッド アパートメントを使用しますが、UWPはマルチ スレッド アパートメントを使用するので必ず競合してしまいます。
この問題を回避するため、今回のサンプル コードでは、UWP APIを呼び出すコードを別のスレッド(std::threadを利用)で実行するようにしています。COMやUWPの初期化はスレッドごとに行う必要があるため、別スレッド上であれば既存のコードに関係なくUWPの初期化処理を呼び出すことができるのです。

Microsoft ストアで有償配布を行う場合の注意点

DACで変換したアプリをユーザーがインストールした場合、その実行中にタスク マネージャーからアプリを選択し、「ファイルの場所を開く」を行うことで、元のプログラム ファイルにアクセスし、パッケージの中身を取り出すことが比較的簡単にできてしまいます。
ブリッジ モードを利用したアプリ配布において、Microsoft ストアの販売網を利用し、有償でリリースしたいというケースもあるかと思います。しかし、単体動作可能なアプリをDACで変換し、ストアで有償に設定しただけでは、ユーザーが中身を取り出して単独実行することで、プロテクトを回避できてしまう可能性があります。
ブリッジ モードで動作していることを検出した場合は、購入されたアプリの試用状態・ライセンスの有無などをUWP APIを使ってチェックすることで、そのような問題にも対応できるようになるでしょう。

ランタイムDLLへの静的リンク問題

roapiやWRLを呼び出すコードを書くと、ビルドされたバイナリにおいて、「api-ms-win-core-winrt-l1-1-0.dll」および「api-ms-win-core-winrt-string-l1-1-0.dll」という2つのランタイム ライブラリへの静的リンクが発生します。
これらのDLLファイル(実際にロードする際には、両者とも「combase.dll」というファイルへリダイレクトされます)は、Windows 8以降で標準搭載されていますが、Windows 7には搭載されていません。Visual C++ランタイムのように、アプリに添付したり外部からインストールすることもできません(ランタイム再頒布パッケージには含まれていません)ので、そのままではWindows 7で実行できないプログラムになってしまいます。

そのままではWindows 7では起動しなくなります

このため、開発するアプリをWindows 7でも動作させたい場合は、

  • 上記2つのDLLファイルをリンカー設定で遅延ロードに設定し、Windows 7上ではUWP関連のコードを呼び出さないようにする
  • UWP関連のコードを外部DLL化し、対応環境ではそのDLLを動的にロードして使用する

といった対処が必要となります。
WRLの内部で直接APIを呼び出しているため、C++のプリプロセッサ マクロで小細工をするなどしない限り、GetProcAddress()を用いた動的なリンクは使用できません。

タグ , , , , | 2020/06/16 更新 |