Sat, 21 Dec 2002 20:12:54 JST / hina.di
powered by tds-1.3.0
<issei@issei.org>
RAM を Windows2000 マシンに取られて、身動きできず。
現・ 東京証券取引所 理事長って「あの」土田正顕氏だったのか。そりゃ、ダメだわ。
午睡をとって頭が冴えたところで、ATL のソースコードを読む。追うのは <atlwin.h> で定義されているウィンドウ関係のクラス。このうち、一般に アプリケーション開発者が利用するのは CWindow と CWindowImpl の二つ。
関連するクラスの継承関係を抜き出しすと、左図のようになっている。
CWindow はウィンドウをクラス化したもので HWND に対する軽いラッパクラス。 図では省略したが、CWindow にはWHND に関係する Win32 API に対応するメソッ ドが、いろいろ定義されている。
Microsoft のドキュメントによると、CWindowImpl は独自のメッセージ処 理動作を定義できるか点が CWindows と異なる、となっている。たしかに コードを追ってみると、CWindow は単なる HWND のラッパクラスであり、 単体では独自のウィンドウクラス *1を登録する能力はない。したがって固有のウィンドウプロシージャを持つことが 不可能で、メッセージ処理動作もそのままではカスタマイズできない。
一方 CWindowImpl では独自のメッセージ処理動作を定義できる。これを示して いるのが CMessageMap の継承。CMessageMap クラスは純粋仮想関数のみから成る抽象クラス *2だが、そこで宣言されている純粋仮想関数 ProcessWindowMessage() が、 メッセージ処理を行なう関数へのインターフェースとなっている。
CWindowImpl の派生クラスで、仮想関数 ProcessWindowMessage() を override すれば、独自のメッセージ処理が実現できる。
実際には、ATL を利用する開発者が ProcessWindowMessage() を直接 override することは稀で、マクロ BEBIN_MSG_MAP(), END_MSG_MAP() などを使って、間接 的に行うことになる。
キーは ::SetWindowLong() と CWndProcThunk クラス。待て次号。
上のクラス図を書くのに使ったアプリケーション (デモ版) ですが、あの程度の 小さなクラス図を書くだけで、メモリを 50MB も使うという。 これだから Pure Java アプリケーションってやつは……。
GLOBAL は、ソースファイルのパス名に空白文字が含まれていると gtags.el と組み 合わせて使えないのね。gctags, gtags.el あたりに手を入れて対応。
CWindowImpl の派生クラスでウィンドウを作成するときに使われる、ウィンド ウクラスとウィンドウプロシージャ。CWindowImpl::Create() で登録されてい る。
// ウィンドウクラスの登録
ATOM atom = T::GetWndClassInfo().Register(&m_pfnSuperWindowProc);
dwStyle = T::GetWndStyle(dwStyle);
dwExStyle = T::GetWndExStyle(dwExStyle);
// ウィンドウの作成
return CWindowImplBaseT< TBase, TWinTraits >::Create(hWndParent, rcPos, szWindowName,
dwStyle, dwExStyle, nID, atom, lpCreateParam);
GetWndClassInfo() は CWndClassInfo& を返す static メンバ関数。 これはマクロ DECLARE_WND_CLASS(), DECLARE_WND_CLASS_EX(), DECLARE_WND_SUPERCLASS() を介して定義される。
#define DECLARE_WND_CLASS(WndClassName) \
static CWndClassInfo& GetWndClassInfo() \
{ \
static CWndClassInfo wc = \
{ \
{ sizeof(WNDCLASSEX), CS_HREDRAW | CS_VREDRAW | CS_DBLCLKS, StartWindowProc, \
0, 0, NULL, NULL, NULL, (HBRUSH)(COLOR_WINDOW + 1), NULL, WndClassName, NULL }, \
NULL, NULL, IDC_ARROW, TRUE, 0, _T("") \
}; \
return wc; \
}
CWndClassInfo の定義は <atlwin.h> にあるが、この3番目の要素がウィンド ウプロシージャ。つまり CWindowImpl の派生クラスで Create() を呼び出し てウィンドウを作成すると StartWindowProc() なる関数がウィンドウプロシー ジャとして登録されることになる。
StartWindowProc() は CWindowImplBaseT<CWindow, CControlWinTraits> クラ スで定義された static メンバ関数。StartWindowProc() が呼ばれるのは、ウィ ンドウが作成されてメッセージ処理が開始されてからなので、ひとまず脇に置 く。先に CWindowImplBaseT<CWindow, CControlWinTraits>::Create() を読む。
CWindowImplBaseT<CWindow, CControlWinTraits>::Create() は通常のメンバ 関数。その中身は、エラー処理を省くと次のようになっている。
Create(HWND hWndParent, RECT& rcPos, LPCTSTR szWindowName,
DWORD dwStyle, DWORD dwExStyle, UINT nID, ATOM atom, LPVOID lpCreateParam)
{
_Module.AddCreateWndData(&m_thunk.cd, this);
if(nID == 0 && (dwStyle & WS_CHILD))
nID = (UINT)this;
HWND hWnd = ::CreateWindowEx(dwExStyle, (LPCTSTR)MAKELONG(atom, 0), szWindowName,
dwStyle, rcPos.left, rcPos.top, rcPos.right - rcPos.left,
rcPos.bottom - rcPos.top, hWndParent, (HMENU)nID,
_Module.GetModuleInstance(), lpCreateParam);
return hWnd;
}
_Module は CComModule クラスのグローバルインスタンス。 CComModule::AddCreateWndData() は <atlbase.h> で定義されている。
void AddCreateWndData(_AtlCreateWndData* pData, void* pObject)
{
AtlModuleAddCreateWndData(this, pData, pObject);
}
...
ATLINLINE ATLAPI_(void)
AtlModuleAddCreateWndData(_ATL_MODULE* pM, _AtlCreateWndData* pData, void* pObject)
{
pData->m_pThis = pObject;
pData->m_dwThreadID = ::GetCurrentThreadId();
::EnterCriticalSection(&pM->m_csWindowCreate);
pData->m_pNext = pM->m_pCreateWndList;
pM->m_pCreateWndList = pData;
::LeaveCriticalSection(&pM->m_csWindowCreate);
}
AddCreateWndData() は _Module.m_pCreateWndList から始まる連結リストに m_thunk.cd を繋いでいる。データ構造を図にすると、次のようになる。

pData->m_pThis (== m_thunk.cd.m_pThis) は CWindowImpl クラス (あるいは その派生クラス) のインスタンスをさす this ポインタが渡ってきている。個々 の m_thunk.cd はクラス内にスコープが限られるオブジェクトだが、グローバ ルインスタンス _Module からポインタを使ってリンクすることでグローバル に辿れるようになる。
_Module はスレッド間で共有されるグローバルなオブジェクトであるため、デー タ挿入の際には ::EnterCriticalSection(), ::LeaveCriticalSection() を使っ て排他制御している点に注意。
静的配列のように「数に制限があるデータ構造」や、エラー処理などに手間の かかる動的メモリ確保を使わず、事前に確保したメモリ領域をうまく使って、 各オブジェクトを管理している点が巧妙。
ここまでの準備が済んだら、いよいよ Win32 API の ::CreateWindowEx() を 呼び出してウィンドウを作成する。
::CreateWindowEx() でウィンドウを作成すると、そのウィンドウ宛のメッセー ジが、OS からのウィンドウプロシージャ呼び出しという形で配送される。
先に見た通り CWindowImpl::Create() で作成されたウィンドウのウィンドウ プロシージャは CWindowImplBaseT<CWindow, CControlWinTraits> クラスの static メンバ関数 StartWindowProc()。デバッグ用の記述を除去すると、次 の通り。
StartWindowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
CWindowImplBaseT< TBase, TWinTraits >* pThis =
(CWindowImplBaseT< TBase, TWinTraits >*)_Module.ExtractCreateWndData();
pThis->m_hWnd = hWnd;
pThis->m_thunk.Init(pThis->GetWindowProc(), pThis);
WNDPROC pProc = (WNDPROC)&(pThis->m_thunk.thunk);
::SetWindowLong(hWnd, GWL_WNDPROC, (LONG)pProc);
return pProc(hWnd, uMsg, wParam, lParam);
}
まず CComModule::ExtractCreateWndData() から。 CComModule::AddCreateWndData() と同様に <atlbase.h> で定義されている。
void* ExtractCreateWndData()
{
return AtlModuleExtractCreateWndData(this);
}
...
ATLINLINE ATLAPI_(void*) AtlModuleExtractCreateWndData(_ATL_MODULE* pM)
{
void* pv = NULL;
::EnterCriticalSection(&pM->m_csWindowCreate);
_AtlCreateWndData* pEntry = pM->m_pCreateWndList;
if(pEntry != NULL)
{
DWORD dwThreadID = ::GetCurrentThreadId();
_AtlCreateWndData* pPrev = NULL;
while(pEntry != NULL)
{
if(pEntry->m_dwThreadID == dwThreadID)
{
if(pPrev == NULL)
pM->m_pCreateWndList = pEntry->m_pNext;
else
pPrev->m_pNext = pEntry->m_pNext;
pv = pEntry->m_pThis;
break;
}
pPrev = pEntry;
pEntry = pEntry->m_pNext;
}
}
::LeaveCriticalSection(&pM->m_csWindowCreate);
return pv;
}
典型的な線形検索のコード。_Module.m_pCreateWndList から始まる連結リス トから、スレッドID が一致するエントリを検索。そこに記録しておいた CWindowImpl クラス、あるいはその派生クラスのインスタンスを指すポインタ (this) を取得している *1。
毎回、この手順を踏んで CWindowImpl のインスタンスへのポインタ (this) を 取得し、インスタンス固有のメッセージ処理を実装した仮想関数 ProcessWindowMessage() を呼び出せば、ウィンドウプロシージャにメンバ関 数を使うことが可能であることは分かる。 しかし ATL では、メッセージ処理の効率を上げるために、もう一工夫凝らし ている。
再びStartWindowProc() に戻ると、メンバ変数 m_hWnd にウィンドウハンドル を記録した後で m_thunk.Init() を呼び出している。これは何か?
pThis->m_thunk.Init(pThis->GetWindowProc(), pThis);
CWndProcThunk は <altwin.h> て定義されているが、x86 プロセッサ関連の部 分のみ抜粋して示すと次の通り *2。
#pragma pack(push,1)
struct _WndProcThunk
{
DWORD m_mov; // mov dword ptr [esp+0x4], pThis (esp+0x4 is hWnd)
DWORD m_this; //
BYTE m_jmp; // jmp WndProc
DWORD m_relproc; // relative jmp
};
#pragma pack(pop)
class CWndProcThunk
{
public:
union
{
_AtlCreateWndData cd;
_WndProcThunk thunk;
};
void Init(WNDPROC proc, void* pThis)
{
thunk.m_mov = 0x042444C7; //C7 44 24 0C
thunk.m_this = (DWORD)pThis;
thunk.m_jmp = 0xe9;
thunk.m_relproc = (int)proc - ((int)this+sizeof(_WndProcThunk));
// write block from data cache and
// flush from instruction cache
FlushInstructionCache(GetCurrentProcess(), &thunk, sizeof(thunk));
}
メンバ変数 m_thunk.thunk に値を設定している。 struct WndProcThunk はアライメントが 1 に設定されているので、パディン グは一切入らない。結果的に m_thunk.thunk には、先頭から順に
C7 44 24 04 XX XX XX XX E9 YY YY YY YY
というバイト列が設定されることになる。ここで XX XX XX XX は CWindowImpl インスタンスのポインタ、YY YY YY YY は CWindowImplBaseT<CWindow,CControlWinTraits>::WindowProc() への相対アドレス。したがって m_thunk.thunk から始まるバイト列は、イン ストラクションコードとして解釈すると次のアセンブラ命令に等しくなる。
mov dword ptr[esp + 0x4], pThis jmp CWindowImplBaseT<CWindow,CControlWinTraits>::WindowProc()
dword ptr[esp + 0x4] は m_thunk.thunk が指す関数の第一引数。次のよう に関数を呼び出すと hWnd を pThis で上書きした上で WindowProc() を呼び 出すことになる。もともとの hWnd は失われるがメンバ変数 pThis->m_hWnd に格納してあるため問題ない。
(*m_thunk.thunk)(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
StartWindowProc() に戻り、次のコードを見てみる。
WNDPROC pProc = (WNDPROC)&(pThis->m_thunk.thunk);
::SetWindowLong(hWnd, GWL_WNDPROC, (LONG)pProc);
Win32 API ::SetWindowLong() を呼び出して、ウィンドウプロシージャを先ほ ど作成したコード片で置き換えている。これにより、次のメッセージからは、 WindowProc() (ただし第一引数はウィンドウハンドルではなく、CWindowImpl のインスタンスに置き換えられる) が呼び出されることになる。
最後に、今回のメッセージを処理するために m_thunk.thunk のコード片を直 接実行して、制御を WindowProc() に移す。
次回に続く
Microsoft 製アプリケーションを見ると、アプリケーション固有のデータは My Document ではなく Application Data *1に置き、移動ユーザプロファイルを使ってユーザプロファイル全体を共有する ことを想定しているように思えます。
ただし、移動ユーザプロファイルを使うためには要 Windows NT/2000 Server。
細かいところで問題が出る。
Perl for UNIX では open(FH, "xxx |") すると sh を起動して xxx を実行、 その出力をファイルハンドルに関連付けるが、Win32 では当然 sh は存在しな い。Perl 内部で処理しているみたいだけど、空白文字やシングルクオートの 扱いが sh と違う。
とりあえず CVS リポジトリのルートディレクトリ名に空白を含まないように DOS 互換ファイル名を使い、open() に含まれるシングルクオートを全部消し て対処する。
CVS リポジトリを、スペースを含んだディレクトリの下に置くほうが悪いっ て? ……そうかも。
「ポップンの大冒険」を拾ってきてみたんだけど、なんかコードがダメダメに 見える。
LPSTR gPath; gPath = "popn.exe"; GetCurrentDirectory(n, gPath);
そりゃ、落ちるよ。
ct=GetTickCount();
while ( 1 ){
if ( abs(GetTickCount()-ct)>30 ){
break;
}
}
busy loop がダメダメだし、OnPaint でタイマ管理するのも設計がマズすぎ。
昔からベーマガの投稿プログラムって、こういうレベルでしたっけ?
最近、良質なゲームのソースコード読みたい病に罹患しているので、おすすめ があれば教えて頂きたく *1。ジャンルは問わず、プラットホームは Win32 ということで。
「
前3冊は、いずれも Microsoft PRESS の出版物。いわゆる教科書の類。
ATL, COM 本も探したが、以前にリストアップしておいた書籍が見つからなかっ たので、購入せず。ATL はソースコード読めば分かるので本が無くても構わな いけど、COM は基礎概念と方法論を押さえておかないと、時間を無駄にしそう。