JavaScriptを有効にすると、ここにメニューを表示します。
Windows 10 Universal Windows Platform API (C++)で同期的にディレクトリ内のファイル名の取得

ysflight.com

最近、手持ちのコードをUniversal Windows Platformに移植しようと、あれこれやっている。概ね、APIはきれいに整理してあって、使いやすいような印象を持っている。Open GL ES 2.0もサポートされたし、手元のOpen GL 2.0/ES2.0を使った例題・テストなどは軒並み動くようになった。CMakeの次のバージョン、3.8からは、OpenGL ES 2.0, Win2Dといったパッケージをライブラリとして利用できるようになる。この機能は、自分で欲しかったから、コードを書いて送って採用してもらった。と、言っても、CMakeはよくできていて、僕はせいぜい20〜30行ぐらい書いただけでできてしまった。

順調に移植が進むかと思ったのだが、ディレクトリ内のファイル一覧の取得で壁に当たった。どうも、非同期処理 (IAsyncOperation) を使わないと、ファイルの一覧を取得できない、ようにに見えた。どういうことかというと、あるディレクトリ内のファイル一覧を取得しようとしたとき、ひとつの関数で完結させることができない。ファイルの一覧を取得しようと思ったら、ある関数内で、ファイル一覧取得のリクエストを発行して、いったんその関数は抜ける。そして、ファイル一覧ができた、というイベントに対するイベントハンドラで初めてファイル一覧を取得できる。それだと、かなり用が足りない。

これは、ドラゴン・クエスト8の錬金釜のiOS版とプレステ版の違いに似ている。iOS移植版では、錬金釜の結果はすぐ出る。待たなくていい。プレステ版だと、(僕はプレイしてないから聞いた話だけど)、錬金釜を仕掛けてから、かなり長い距離を歩かないと(待たないと)結果が出ないらしい。iOS版の錬金釜は同期処理、プレステ版は非同期処理と言える。

あるいは、Win32 APIのモーダル・ダイアログとiOSのモーダル・ダイアログの違いとも似ている。Win32では、メッセージを出して、Yes/Noを選んでもらおうと思ったら、MessageBoxという関数一発で済んだ。MessageBox関数を呼ぶと、ユーザがOKボタン、Yesボタン、あるいはNoボタンを押すまで、関数は返ってこない。ユーザが選択するまで処理をブロックする。これは同期処理。ところが、iOSである関数の中で、モーダル・ダイアログ(ビューコントローラ)を作っても、ダイアログを閉じるまでその場で待つことができない。代わりに、モーダル・ダイアログを作って、OKボタン、Yesボタン、Noボタンを押したというイベントを処理するハンドラを仕掛ける。だから、あるメッセージを出して、ユーザのYes/Noの選択を待って何かの処理をしようと思ったら、ダイアログを出す関数、と、結果を処理する関数の、最低ふたつの関数を作る必要がある。メッセージを表示してから、選択に応じた処理をするまでの間に、何か別のことが起こる可能性もある。これは非同期処理。プログラムを書く立場からすると、同期処理のMessageBoxが可能であれば、その方が簡単だ。

ディレクトリ内のファイル一覧を取得するためだけに非同期処理を要求するのは、明らかにやりすぎだ。なんでこんな馬鹿な仕様にしたんだ?と、思った。言いたいことは、わかるような気はする。クラウド上のファイル一覧を取ってくる処理をブロッキングすると、ユーザーインターフェースが割と長時間停止するではないか、と言いたいんだと思う。だが、それはプログラマーの選択だ。ユーザーには悪いけど少し待ってもらうか、あるいは、非同期処理でインターフェースの停止を防ぐか、または、別スレッドを立てて処理するというやりかたもある。

その後、試行錯誤の結果、非同期処理を仕掛けて、イベントループを回すと、処理が進むことを発見する。ファイル一覧の取得が完了するまでイベントループを回してやればいい。実際、この方法で、一応、なんとかなった。同様の解決は、以下のURLでも提案されていたけど、この情報に気がついたのは、自力でこのやり方を見つけた後だった。

http://stackoverflow.com/questions/19917466/how-to-wait-for-an-iasyncaction

ただ、この方法には大きな問題がある。イベントループを回すから、リエントランスが発生する。ファイルの一覧が欲しいとき、というのは、多分、ユーザがボタンを押したタイミングとか、何かアクションを起こしたタイミングだと思う。だから、イベントハンドラ内から、ファイル一覧取得関数を呼ぶ可能性が高い。イベントハンドラ内でイベントループを回すと、まだひとつのイベントのハンドリングが終わっていないのに、次のハンドラが呼ばれてしまう危険がある。この方法は良くない。

納得できなかったのは、C#では、awaitというキーワードで、同期処理が終わるのを待つことできるとなっている。awaitキーワードに対応する関数として、C++の方では、Concurrency::task<>::wait() という関数があるはずなのだが、使おうとするとinvalid parameterの例外が発生する。ダメじゃん。と、思ったのだが、下のリンクで、

https://social.msdn.microsoft.com/Forums/sqlserver/en-US/02299772-22a2-451a-9ae7-72dad6d3396c/ctaskwait-in-a-catchblock-throws-exception?forum=wpdevelop

どうも、UIスレッドがどうのこうのという議論になっている。

そういえば、前もなんか似たようなことがあったような。なんだったっけ?思い出せないが、特定の機能がUIスレッドでは実行できなくて困ったような。ああ、そうそう。フォントだ。フォントを描画するクラスがUIスレッドで作れなかったんだっけ?記憶をたどると、そうだったような気がする。でも、最終的にビットマップにできないという何の約にも立たないクラスだった。その代わりにWin2Dが使えることがわかった、だった気がする。そうだった。UWPの一部の機能はメインスレッドで実行できないんだった。と、思い出したので、ひょっとして実は別スレッドで実行すればいいだけか?と、思った。

ものは試し。std::threadを使って、別スレッドで実行してみたところ、普通にwait()が使える。なんだ、それだけのことだったのか。ファイル一覧が欲しいとき、まず、std::threadを使って、別スレッドでファイル一覧を取得する。別スレッドではwait()を使うことができるから、同期的にファイルの一覧の取得ができる。メインスレッドは、std::thread::join()を使うと、別スレッドの処理が終わるまで待つから、メインスレッド側でもファイル一覧の取得完了まで待つことができる。

多分、この方法を使えば、他の非同期なAPI関数も同期的に実行できるようになると思う。 悪いけど、僕のプログラムでは、ファイル一覧を取得する間はユーザには待ってもらうよ。0.1秒もかからないと思うんで。

以下は実験用に書いて成功したコード。

uwpdirlist.h

#ifndef UWP_DIRLIST_IS_INCLUDED
#define UWP_DIRLIST_IS_INCLUDED

#include <vector>
#include <string>

class YsUwpFileList
{
public:
    class File
    {
    public:
        std::basic_string <wchar_t> nm;
        bool isDir;
    };
    std::vector <File> fileList;

private:
    bool res;

public:
    bool MakeFileList(const wchar_t fulPath[]);
private:
    void RunInThread(const wchar_t fulPath[]);
};

#endif

uwpfilelist.cpp

#include "uwpdirlist.h"
#include <thread>

#include <ppltasks.h> // To use Concurrency namespace

using namespace Windows::Storage;
using namespace Windows::Foundation::Collections;  // To use IVectorView
using namespace Concurrency;                       // To use create_task

bool YsUwpFileList::MakeFileList(const wchar_t fulPath[])
{
    std::thread t(&YsUwpFileList::RunInThread,this,fulPath);
    t.join();
    return this->res;
}
void YsUwpFileList::RunInThread(const wchar_t fulPath[])
{
    this->fileList.clear();
    this->res=true;
    Platform::String^ uwpPath=ref new Platform::String(fulPath);

    auto getFolderTask=create_task(Windows::Storage::StorageFolder::GetFolderFromPathAsync(uwpPath));
    getFolderTask.wait();
    try
    {
        auto folder=getFolderTask.get();
        auto getItemsTask=create_task(folder->GetItemsAsync());
        getItemsTask.wait();
        auto itemList=getItemsTask.get();

        for(int i=0; i<itemList->Size; ++i)
        {
            auto itm=itemList->GetAt(i);
            bool isDir=false;
            if((itm->Attributes&FileAttributes::Directory)==FileAttributes::Directory)
            {
                isDir=true;
            }
            std::basic_string <wchar_t> fn=itm->Name->Data();

            File newFile;
            fileList.push_back(newFile);
            fileList.back().nm=fn;
            fileList.back().isDir=isDir;
        }
    }
    catch(Platform::COMException^)
    {
        this->res=false;
    }
}

一応、コンパイルしてアプリケーションとして実行できる形式の例題を下からダウンロードできる。起動後、自身のディレクトリ内のファイルの一覧をウィンドウに表示する。

[例題]

コンパイルするには、CMake 3.8-rc0 以上が必要。CMakeを実行するには、ビルドディレクトリに移動して、コマンドから、

cmake <sourceDir> -G "Visual Studio 14 2015" -DCMAKE_SYSTEM_NAME=WindowsStore "-DCMAKE_SYSTEM_VERSION=10.0"

このようにタイプ。できたProject.slnでコンパイルして実行できる。なお、CMakeLists.txtの中から、OpenGL ES 2.0とWin2DのNuGetパッケージをダウンロードするので、ネットにつながってないと利用できない。 なお、-DCMAKE_SYSTEM_VERSION=10.0をダブルクオートでくくらないと、Power Shellから実行するとき文句が出る。「.」になんか特別な意味があるのかな。まだPower Shellいまいち仲良くなれてない。いや、でも、その前のコマンドプロンプトがあまりにもひどかったから、既にPower Shellに全面的に移行したけど。

Comments are welcome.  Send E-Mail to: 

Back to http://www.ysflight.com