ShellExecuteExからなぜかWndProcが呼ばれる

[戻る]
tamachan 2014/03/07(金) 23:21:18
明らかに英語の意思疎通が私の能力では不可能/回答を得るのは困難ですのでこちらで再
度質問させて下さい。(http://stackoverflow.com/questions/22250904/wndproc-is-
called-from-in-shellexecuteex-fifth)
よろしくおねがいします。

ShellExecuteExからなぜかWndProcが呼ばれます。しかも何故かShellExecuteExが5回目
の時に限ってです。
これはShellExecuteExのバグなのでしょうか?それとも私のコードのバグなのでしょうか?
どうしても私だけでは判断がつかなかったため、こちらで質問させてもらいました。

ソース: https://code.google.com/p/jpgcutter/
MainDlg.h先頭の #define ENABLE_SHELLEXECUTEBUG を定義(コメントアウト)してください

再現環境:
Windows 8.1 x64 + VC2010

再現方法:
適当にjpgを20個ほどすばやく2回放り込んでください。(jpgは改変されます。支障の
ないファイルを使用してください)

現象:
OnIdleの5回めのShellExecuteExの呼び出し中にさらにWndProcのタイマーなどの処理が
呼ばれます。またその中のShellExecuteEx5回めで更にWndProcが呼ばれます。さら
に・・・(10回ほどループ)
デバッグでコンパイルしてTRACEを確認すると確認できます。

理想的には
+OnIdle
-OnIdle
+WM_TIMER
-WM_TIMER
...(ループ)

なのですが、
実際には・・・
+OnIdle1
 + WM_TIMER(スレッドID同じ)
   + WM_TIMER(スレッドID同じ)
     + WM_TIMER(スレッドID同じ)
     - WM_TIMER
   - WM_TIMER
 - WM_TIMER
-OnIdle1
といった感じになってしまいます。
同一スレッドのためMutexもCriticalSectionもセマフォも効かないため、グローバル変数
をカウンタにすることで回避しました。回避はできたのですが、なぜこのようなことが起
こるのか大変モヤモヤしています。
何かわかる方がいたらぜひ教えてください。よろしくお願いいたします。

tamachan 2014/03/07(金) 23:41:25
汚いソースですみません。少しでも足しになるように簡単にプログラムの説明をしておき
ます。

・jpgをjpegtran.exeで処理して、サイズを縮小するプログラムです。ファイルがドロッ
プされると処理開始します。

・処理(jpegtran.exe)は最大10個同時に動きます。起動はShellExecuteExを使って行われ
ます。

・処理はOnTimer(50ns)、またはOnIdleハンドラから呼び出されます。ConvProc()です。

・通常はCMessageLoopのRunからOnTimer, OnIdleともに呼び出されます。1スレッドなの
でOnIdleの終わる前にOnTimerが呼び出されるなんてことは無い・・・と理解していたの
ですが、呼び出されているようですorz

・TRACEを見ていると、ConvProc: グローバル変数カウンタ値, ThreadId=0x%08X、で同じ
スレッドIDのOnTimerがOnIdle途中(150行の"   7"で止まる)から呼び出されていくのがわ
かるかと思います。

tamachan 2014/03/07(金) 23:54:35
再補足です。
最初はOutPutDebugStringの出力が順序逆転してるのかなというのも疑いました。しかし
177行のグローバル変数の値が1〜10まで上昇していくので、それは無いと判断しました。

tamachan 2014/03/08(土) 02:20:40
MainDlg.hの173行でg_bDebugCount==1の条件ブレークしたら、ShellExecuteExから
DispatchMessageが呼ばれてWndProcが呼ばれているのが確認できました。
https://pbs.twimg.com/media/BiI_1zHCMAA5q8V.jpg:large

なぜ5回めだけ?どうやったら回避できるかなど、何か情報をお持ちの方いらっしゃいま
せんでしょうか?

ITO 2014/03/10(月) 18:48:50
うーーん、
回避方法は分かりませんが、

> 適当にjpgを20個ほどすばやく2回放り込んでください。
> (jpgは改変されます。支障のないファイルを使用してください)
これは、異常なソフトの使い方なのでは?
2回放り込まれても2回目は無視するとかしないとまずいと思います。
ファイル20個も多いのでは?

まず、
 クライアントの要求とtamachanさんのソフトの許容範囲を明確にしたほうがいいと思
います。

tamachan 2014/03/11(火) 03:28:31
えーと・・・。2回放り込んでも大丈夫なように書きました。

その後いろいろ動作テストして、1回で10個ほどのjpgを放り込むだけでも起こりま
す。テストしてないですがたぶん6個以上なら起こるんじゃないかと(ShellExecuteEx5
回目のコールがおかしいので)

ebitai 2014/03/12(水) 05:49:39
ShellExecuteEx は、DDE 通信などのために内部でメッセージループが回ります。

仲澤@失業者 2014/03/12(水) 11:09:42
スレッドの設計に関わらず。Windowsでは以下の要請があります。

1.ウインドウプロシージャでのメッセージに応答する関数群は
 リエントラントに設計されていなければならない。

2.又は、再入禁止措置が必要。

基本中の基本なのですが、この内容は大丈夫ですか。

tamachan 2014/03/12(水) 20:44:08
ebitaiさん、中澤さんアドバイスありがとうございました。

>ShellExecuteEx は、DDE 通信などのために内部でメッセージループが回ります。
なるほど。DDEは気づきませんでしたorz。

>1.ウインドウプロシージャでのメッセージに応答する関数群は
> リエントラントに設計されていなければならない。
これは本当ですか?できればMSDNかなにかの情報源を貼ってもらえると嬉しいです。
ということは、中澤さんは全メッセージハンドラに下記のようなコードを書いていらっ
しゃるということなんでしょうか?(下記コードだと同一スレッドから複数呼び出された
場合、一部メッセージが破棄されるので不完全ですが・・・)

int g_count = 0;//Global変数。同一スレッドから呼び出された時のため。

CMainDlg{
CCriticalSection m_cs; //メンバ変数
OnHandler(/* ... */)
{
  CCritSecLock lock(&m_cs);//ロック。別スレッドから呼び出された時のため
  if(g_count==0)
  {
    g_count++
    ...
    g_count--;
  }
}
}

tamachan 2014/03/12(水) 20:52:36
仲澤さんすみません。お名前の仲が中になっていました。訂正とともにお詫び申し上げます。

tamachan 2014/03/12(水) 21:54:09
よく考えたら上のコードだめですね。同一スレッドから再呼び出しされたらクリティカル
ロックが再発行されて、もしかしたら上書きまでされちゃうかも・・・
同一スレッド/別スレッド、両方からロックするって以外に難しい・・・

forty-five 2014/03/12(水) 22:44:20
度々横から失礼しますが、

> 中澤さんは全メッセージハンドラに下記のようなコードを書いていらっ
しゃるということなんでしょうか?

仲澤@失業者さんは
「ウインドウプロシージャは再入可能なように設計しなければならない。
どうしても再入されたくないなら自分で再入禁止措置をとらなければならない」
と言っている訳で、再入禁止措置をやらなければならないのは再入を禁止したいあなただ
けですよ。
ウインドウプロシージャは再入可能なように設計するのが普通で、
あなたが特異な要求をしているだけです。

まあ、ウィンドウプロシージャを再入禁止にするより、
::ShellExecuteEx() の代わりに ::CreateProcess() を使えば良さそうですけどね。

tamachan 2014/03/12(水) 23:03:53
forty-fiveさんアドバイスありがとうございました。

> あなたが特異な要求をしているだけです。
なんとなくですが・・・書いてること矛盾してません?
というのは現実的に、"再入可能なように設計する" ≒ "再入禁止措置" なような気がする
んです。
ごくごく一部の単純な、すごーく短いハンドラならともかく・・・普通にソフト作る場合
のほとんどのハンドラは再入禁止措置しか私には解が思いつきません・・・orz

>::ShellExecuteEx() の代わりに ::CreateProcess() を使えば良さそうですけどね。
前にコマンドライン引数の最大文字数の制限で、深層の日本語名ファイルが失敗するなん
てことがあって、できればDDE使いたい/引数の引数がLPTSTR(constが無い!!)という2
つのなんとな〜くな理由でShellExecuteExを使ってました。

tamachan 2014/03/12(水) 23:48:10
[[解決]]
原因がわかって解決策を見つけられたため解決チェックしました。
数々の有り難いアドバイスいただき、大変有難うございました。

解決策案
 1. ShellExecuteExをCreateProccess/Exで置き換える
 2. 別スレッド(ShellExecuteEx起動専用)に処理を置き換える(ファイルリストを渡し
たり、GUIへ完了通知したりするのが非常に面倒そうですが・・・orz)
 3. jpegtran.exeを結合してしまう。関数化する。(ShellExecuteExを使わない)

仲澤@失業者 2014/03/13(木) 18:16:56
全ての関数がリエントラントであることが要求されるわけではありません。
特にcallbackといわれる、システムから呼ばれる関数は、リエントラント性が
強く要求されます。

最近はPCが早くなったので、ほとんど問題になることはありませんが、
Windows2.1 Windows 3.0、3.1のSDKには、このことが明記されていました。
現在この件についてのM$さんのズバリといった公式な文書は見つかりませんでしたが、
原理的に簡単に類推できます。

最も想像しやすい例としては、ウインドウコールバックで、何らかの
メッセージを処理中に、自身にSendMessage()する例が挙げられます。

想像しにくい、但し厳然とした事実としては、WM_PAINTメッセージの
処理中に、BeginPaint()を実行すると(普通そうなりますし、MFCも
そうなってますが)、BeginPaint()から制御が「戻る前」にWM_ERASEBKGND
メッセージが、自身に届きます。
また、このことは、BeginPaint()のマニュアルに明記されています。

興味があったら試してみてください。

IT 2014/03/13(木) 19:52:45
> なんとなくですが・・・書いてること矛盾してません?
> というのは現実的に、"再入可能なように設計する" ≒ "再入禁止措置"
> なような気がするんです。
> ごくごく一部の単純な、すごーく短いハンドラならともかく・・・
> 普通にソフト作る場合のほとんどのハンドラは再入禁止措置しか私には
> 解が思いつきません・・・orz

うーーむ、
もともと、ShellExecuteExはexeファイルを何回も実行させる目的では作られていない
と思います。

本来の使い方は、エクセル・ワードみたいにDDE 通信が可能なアプリケーションを
一つ立ち上げて、DDE 通信を行いながら何かを行う。なのかなぁーと思います。

>ShellExecuteExをCreateProccess/Exで置き換える
これは正解だと思います。
ただ、ハンドル等はメインスレッドで管理するのがいいと思います。

ITO 2014/03/13(木) 19:53:26
すみません、ITOです。

tamachan 2014/03/13(木) 21:47:54
仲澤さん、ITOさんいろいろ解説ありがとうございます。

> BeginPaint()から制御が「戻る前」にWM_ERASEBKGNDメッセージが、自身に届きます。
私は知らなかったですw。参考になります。
ただこれはWM_ERASEBKGNDしか届かないのでは?やはりShellExecuteExがDDE以外のメッ
セージを処理するのはなんか変(バグ)じゃないのかな〜と私はちょっと思います・・・

ーーー
あと解決策なんですが1つ書き忘れていました。今回は別スレッドを立ち上げることは無
いためほぼシングルスレッドみたいなもので、現在のグローバル変数のカウンタ方式でも
一応解決します。
ただWM_DROPFILESにはカウンタのフィルタをかけてないので、極稀にWM_DROPFILESが
ShellExecuteから呼ばれるとダメかも(←不思議なのはこれが発生しないのです。かなり
回数テストしましたが、WM_TIMERしか通過していないように見えます。厳密にテストした
わけじゃないので自信がないのですが・・・)

tamachan 2014/03/13(木) 21:56:28
ここまで書いてみてもしかしてSetTimerで使っているIDが重複しているのでは?と思い、
IDを1200 -> 12000に変えたら直るんじゃと試してみたけどやっぱりダメでしたw

tamachan 2014/03/13(木) 22:20:18
あと1つ注釈しておくと、ShellExecuteEx(5回め)で必ず発生するとのことでしたが、こ
れは間違いでした。厳密にいうと私のPCでは必ず5回目に発生します。なのでみなさんの
高速なPCだと8回目だったり発生しないこともあるかも。SetTimerの値を10やもっと小さ
くすれば発生するかと思います。

古いスレッドには追加発言できません。