著作一覧 |
久々にC++でCOMのコンポーネントを書いたらわからない現象に出会ってしょうがないので回避したが、原因はなんだろう?
まず、それは2つのATLを利用したクラスのインターフェイスだ(以下うろ覚えで書くので一部ATLの継承リストなどは適当)。
class CComponent : public CCom..., (ATLの長い継承リスト) { ... } class CDialog : public CAxDialogImpl{ }
タイマーを利用してCComponentの関数を呼び出したい。そこでCDialogにポインタと関数を記憶させることにする。
typedef void (CComponent::*callback)(LPVOID); class CDialog .... { public: ... enum { CALLBACK_EVENT = 101 }; void RegisterComponent(CComponent* p) { m_component = p; } bool RegisterDelayedFunc(CALLBACK_EVENT, UINT msec, callback fnc, LPVOID param) { if (m_callback) return false; // busy m_callback = callback; m_param = param; SetTimer(msec); return true; } ... LRESULT TimerProc(WPARAM timerId, LPARAM lparam, bool& bhandled) { if (timerId == CALLBACK_EVENT) { (m_component->*m_callback)(m_param); m_callback = NULL; } bhandled = true; return 0; } private: callback m_callback; LPVOID m_param; CComponent* m_component; };
上の例では面倒だから1関数しか登録できないようにしてみた。
で、CComponent側はこんな風に使おうとした。
HRESULT FinalConstruct() { Application->GetDialog()->RegisterComponent(this); ... return S_OK; } HRESULT STDMETHODCALL AsyncFoo(BSTR instr, LONG* retval) { if (!retval) return E_POINTER; Application->GetDialog()->TimerProc(10, &CComponent::Foo, instr); return S_OK; } void Foo(LPVOID param) { BSTR b = reinterpret_cast(param); ... SysFreeString(b); FireAsyncResultEvent(resultValue); // using Connection Point } ... };
すると壊れる壊れる。CDialogの特定メンバーがつるりとNULLになる。
まず疑うのは、RegisterComponentで与えたthisが、CComponentではなく異なるvtblの可能性だ。が、関数プロトタイプから正しいポインタが与えられることを期待できると思うのだ(と書いて、これが怪しいなぁという気もする。FinalConstructはATLの基底クラスの1つが呼び出すから、そのクラスのvtblになっている可能性がある)。
というか、ブレークポイントを入れれば、正しくFooがコールバックされる。したがってthisが異なるというのはあまり考えられない。(追記:でも仮想関数ではないからブレークポイントが効くのは当然だった。この時インスタンス変数を見ていないために問題に気づいていないだけとか?)
データ破壊に対するブレークポイントを仕掛けても、止まらない(がクリアされているのは事実)。これは困った。が、代わりに全然関係ないCCompoentのSTDMETHODのreturnで、esp破壊を検出する。はて? typedefから言ってもどこにも矛盾は無いはずだが。
データ破壊ブレークポイントが効かないと常に轢き逃げ後の状態でしか捕まえられない。謎過ぎる。
時間切れだ。
というわけで、クッションを置くようにした。するときれいに解決するわけだが、なぜだ? (Win32APIの多くがコールバックをstaticにしているから考え付いた回避方法だが、本当に回避できているのだろうかという疑問も)
typedef void (*callback)(CComponent*, LPVOID); class CDialog .... { public: ... enum { CALLBACK_EVENT = 101 }; void RegisterComponent(CComponent* p) { m_component = p; } bool RegisterDelayedFunc(CALLBACK_EVENT, UINT msec, callback fnc, LPVOID param) { if (m_callback) return false; // busy m_callback = callback; m_param = param; SetTimer(msec); return true; } ... LRESULT TimerProc(WPARAM timerId, LPARAM lparam, bool& bhandled) { if (timerId == CALLBACK_EVENT) { (*m_callback)(m_component, m_param); m_callback = NULL; } bhandled = true; return 0; } private: callback m_callback; LPVOID m_param; CComponent* m_component; };
と変えて呼び出し側も変える。
HRESULT STDMETHODCALL AsyncFoo(BSTR instr, LONG* retval); void Foo(LPVOID param) { BSTR b = reinterpret_cast(param); ... SysFreeString(b); FireAsyncResultEvent(resultValue); // using Connection Point } ... }; ... # cpp file static AsyncFooCallback(CComponent* p, LPVOID param) { p->Foo(param); } HRESULT STDMETHODCALL CComponent::AsyncFoo(BSTR instr, LONG* retval) { if (!retval) return E_POINTER; Application->GetDialog()->TimerProc(10, &AsyncFooCallback, instr); return S_OK; }
わからん。
追記:ふと気付いたがスレッドをまたがっている気がしてきた(にしてはHWNDが有効だから(WM_TIMERを取れているから)違うかなというか、COMのマーシャルが必要な話ではないし)。素直にHWNDをNULLにしたSetTimerを直接呼ぶべきだったのではなかろうか(その場合、いやでもインスタンス関数は呼べないから同じような形にせざるを得ないけど)。
ジェズイットを見習え |
的外れかもしれませんが、AFX_MANAGE_STATEマクロを試してみると良いかもしれません。<br><br>AFX_MANAGE_STATE (の記事およびリンク先の関連情報)<br>https://msdn.microsoft.com/ja-jp/library/ba9d5yh5(v=vs.120).aspx<br><br>以下は古い記事ですが。<br><br>ATL でダイアログベースのコントロールを作成する (の最後の説明の部分)<br>http://www.koutou-software.net/junk/howto-atldlg.html<br><br>[vcpp 00048954] Re: _AFXDLLとAFX_MANAGE_STATEマクロ (の考察)<br>http://vcpp-ml.ldblog.jp/archives/1175798.html
どうもありがとうございます。確認してみます!