ラベル Win32 の投稿を表示しています。 すべての投稿を表示
ラベル Win32 の投稿を表示しています。 すべての投稿を表示

2016年10月31日月曜日

FindFirstFile にルートパスを指定した場合の挙動について

FindFirstFile(FindFirstFileEx) 関数の lpFileName にルートパスは指定できません。
MSDN でそのように記載されています。
FindFirstFile 関数
FindFirstFile の lpFileName パラメータでは、最後に円記号(\)を付けるかどうかにかかわりなく、ルートディレクトリを指定することはできません。




指定できないのはわかりましたが、実際に指定してしまった場合にどうなるのか検証したところ、厄介な挙動だったので備忘録として残しておきます。

検証にしようしたコードはこちら。



これを実行するとこのような出力になります。

カレントディレクトリがルートパスでない場合

このように INVALID_HANDLE ではなく Find が成功し、しかもなぜかカレントディレクトリ名が取得できます。


これが、カレントディレクトリがルートパスだった場合

期待?通り INVALID_HANDLE が返ります。


ちなみに、FindFirstFileExTest がコメントアウトされていますが、FindFirstFileEx でも同じ挙動を示しました。
(Windows 10 HOME version 1607 build 14393.321)
古い OS での検証はできていないので、もしかしたら挙動が異なるかもしれません。

2012年8月22日水曜日

strcpy_s が失敗したとき

今回は、「CRTセキュリティ強化」のことについてです。

早速本題です。
strcpy_s を使っている人がどれだけいるかわかりませんが、少なくはないと思います。
私もそのひとりです。
今回は、この strcpy_s が失敗したときについてです。

失敗したときの挙動
strcpy_s 関数ですが、第二引数に「コピー先の文字列バッファのサイズ」を指定すると思います。
指定サイズを超えてコピーが行われた場合、戻り値にエラー値が返ってきます。

と、思っていたのですが実際の挙動は少し違っていました。

MSDN より引用
strDestination または strSource が null ポインタの場合、またはコピー先文字列が小さすぎる場合は、「パラメータの検証」に説明されているように、無効なパラメータ ハンドラが呼び出されます。実行の継続が許可された場合、これらの関数は EINVAL を返し、errno を EINVAL に設定します。

どうやら、「無効なパラメータ ハンドラ」というものが呼ばれるようです。

試してみた
ということで、以下のコードで試してみました。

#include <gtest/gtest.h>

int main(int, char**)
{
    const char a[] ="test";
    char b[2];
    ASSERT_EQ(0, strcpy_s(b, a));
    return 0;
}

デバッグビルドで実行すると


で、中止 or 無視 を選択するとプログラムが終了。


リリースビルドの場合、


このように WerFault.exe が実行される。
「プログラムを終了します」を選択すると文字通りプログラムが終了します。
この点が少し問題で、この時失敗も返さず、例外も投げず終了していくので、
クラッシュレポートを出力するようなプログラムの場合、何もできずに終了してしまいます。

gtest の対応は?
上の検証コードが gtest のアサーションのみ利用しているのは理由があって、
gtest は上の挙動と異なるからです。

#include <gtest/gtest.h>

int main(int, char**)
{
    ::testing::InitGoogleTest(&argc, argv);
    return RUN_ALL_TESTS();
}
TEST(CRT, strcpy_s)
{
    const char a[] ="test";
    char b[2];
    ASSERT_EQ(0, strcpy_s(b, a));
}

このコードをリリースビルドで実行すると、

なにごともなく終了していきます。

gtest では、
SetErrorMode(SEM_FAILCRITICALERRORS | SEM_NOALIGNMENTFAULTEXCEPT |
                 SEM_NOGPFAULTERRORBOX | SEM_NOOPENFILEERRORBOX);
のように設定され、システムの処理が行われないようにしているからです。

XML が出力されない
さて、ここで困ったことがあります。
この状態では、テスト結果の XML が出力されないのです。
XML が出力されていない時点で「何らか」の失敗があったことはわかりますが、
できることなら XML が出力されて欲しいものです。

XML 出力されるようにしてみる
「無効なパラメータ ハンドラ」を設定します。
それには、_set_invalid_parameter_handler 関数を使用します。
こちらの第一引数にハンドラの関数ポインタを渡します。

void _invalid_parameter(
   const wchar_t * expression,  // 引数式
   const wchar_t * function,    // CRT 関数の関数名
   const wchar_t * file,        // CRT ソースファイル名
   unsigned int line,           // CRT ソースファイルの行
   uintptr_t pReserved
);

そして、このハンドラで例外を投げるようにします。(例外の内容はなんでもいいです。)
あとは、RUN_ALL_TESTS の前で _set_invalid_parameter_handler を読んで登録するだけです。

まとめたコードがこちら
#include <gtest/gtest.h>

static void OnInvalidParameter(const wchar_t * , const wchar_t *
    , const wchar_t * , unsigned int , uintptr_t)
{
    throw std::invalid_argument("invalid parameter error");
}
int main(int argc, char* argv[])
{
    ::testing::InitGoogleTest(&argc, argv);

    _set_invalid_parameter_handler(OnInvalidParameter);

    return RUN_ALL_TESTS();
}
TEST(CRT, strcpy_s)
{
    const char a[] ="test";
    char b[2];
    ASSERT_EQ(0, strcpy_s(b, a));
}

実行すると、失敗がレポートされ最後まで実行されます。


最後に
今回のケースで問題になることって、極稀だと思います。
ただ私の場合、身近なところで実際に問題になったことがあって、
それからこのことを初めて知りました。
プログラムが終了するパターンの一つなので、知っておいてソンはないと思います。

以上。

2012年5月28日月曜日

リソース(.rc) で単語の共通化

リソースを書いていて、後々変更するかもしれないけど暫定の名前を使うことありますよね?
それがたくさんあると、後で変更するときに少しメンドクサイですよね?

置換すれば一瞬だって?

そうだね!

でも、ここでは別のアプローチをしてみたいと思います。

プリプロセッサがあるじゃない
リソースファイル(.rc) でもマクロが使えます。
マクロで単語を共通化しちゃいましょう。

Win32 アプリケーションのテンプレートプロジェクトで実験してみましょう。
//Microsoft Visual C++ で生成されたリソース スクリプトです。
//
#include "resource.h"

#include <boost/preprocessor/cat.hpp>
#include <boost/preprocessor/stringize.hpp>

#define SOFTWARE_NAME ソフト名

// ~~~~~~ 略 ~~~~~~~~

IDC_RESTEST MENU
BEGIN
    POPUP "ファイル(&F)"
    BEGIN
        MENUITEM BOOST_PP_STRINGIZE(BOOST_PP_CAT(SOFTWARE_NAME,の終了(&X))), IDM_EXIT
    END
    POPUP "ヘルプ(&H)"
    BEGIN
        MENUITEM BOOST_PP_STRINGIZE(BOOST_PP_CAT(SOFTWARE_NAME,のバージョン情報(&A)...)), IDM_ABOUT
    END
END


※[プロジェクトのプロパティ] - [リソース] - [追加のインクルード ディレクトリ] にパスを追加しましょう

これをビルドし、実行するとこうなります。

おわり。

追記
半角スペースを入れるには、 「\x20」とする必要があります。
ちょっと不恰好ですね…

2012年1月9日月曜日

ExpandEnvironmentStrings の問題

自分のための備忘録です。

Windows7 と XP で ExpandEnvironmentStrings の挙動が違うもよう。
基本的に XP での動作が怪しい感じ。

ExpandEnvironmentStrings についてはこんな感じ。
DWORD ExpandEnvironmentStrings(
LPCTSTR lpSrc, // 環境変数を表す文字列へのポインタ
LPTSTR lpDst, // 展開後の環境変数を表す文字列へのポインタ
DWORD nSize // 展開後の文字列の最大文字数
);

戻り値
関数が成功すると、展開後の文字列を受け取るバッファに格納された文字数が返ります。
指定したバッファのサイズより展開後の文字列数の方が大きいときは、展開後の文字列を
保持するために必要なバッファのサイズが返ります。


日本語を含む文字列の展開
char* hoge()
{
    DWORD len = ExpandEnvironmentStringsA( "%USERPROFILE%\\デスクトップ", NULL, 0 );
    char* str = new char [len];
    ExpandEnvironmentStringsA( "%USERPROFILE%\\デスクトップ", str, len );
    return str;
}
1回目の ExpandEnvironmentStringsA で必要なサイズ(文字数)を取得して、
2回目の ExpandEnvironmentStringsA 動的確保したバッファに読み込みます。

使い方に関して、一見問題なさそうですが、
Windows XP では、len に想定外の値が返ってきます。
実際に必要な文字数より大きい値であれば、必要な文字列が取得できるのですが(余分なメモリは確保されてしまうが)小さい値になる場合もあるようで、この場合文字列が切り捨てられてしまいます。

対策としては、余分にバッファを確保するか、ExpandEnvironmentStringsW を使うといったところでしょうか…

非常に長い文字列の展開
日本語を含む文字列の展開については、ExpandEnvironmentStringsW を使うことにして対処しました。
しかし、ExpandEnvironmentStringsW にも問題がありました。
TEST(EnvironmentStrings, LongLongString)
{
    wchar_t str[0x10000];
    // 適当な文字列を作る
    for( int i=0; i < 0x10000; ++i )
    {
        str[i] = L'0' + i%10;
    }
    str[0x10000-1] = L'\0';
    DWORD len = ExpandEnvironmentStringsW( str, NULL, 0 );
    ASSERT_EQ( 0x10000, len );
}
上記コードは、Windows7 では成功します。 しかし、Windows XP では len に 0x7FFF が返ってきて、失敗してしまいます。 どうやら、XP では 0x8000 以上の文字列は展開できないようです… そんなに長い文字列を展開することがあるのか、という気もしますが、 最近あったからこれを書いているわけで… 対策としては、事前に分解して長い文字列の展開をしないように工夫するしかないですね…
追記
  • 2012/01/22
    TEST(EnvironmentStrings, LongLongString) のコードに間違いがあったので修正しました。