或曰

……と、ある人は言った。

-- 2001年4月上旬 --

Sat, 21 Dec 2002 20:12:54 JST / hina.di
powered by tds-1.3.0
<issei@issei.org>



検索式: [検索方法]

表示件数: 表示形式: ソート:

[最新版] << == >> [一覧]

2001/4/1(Sun)

FreeBSD-CURRENT マシン

RAM を Windows2000 マシンに取られて、身動きできず。

東証理事長

現・ 東京証券取引所 理事長って「あの」土田正顕氏だったのか。そりゃ、ダメだわ。

購入予定

ATL 探索 (1)

午睡をとって頭が冴えたところで、ATL のソースコードを読む。追うのは <atlwin.h> で定義されているウィンドウ関係のクラス。このうち、一般に アプリケーション開発者が利用するのは CWindow と CWindowImpl の二つ。

ATL Window 関連クラス

関連するクラスの継承関係を抜き出しすと、左図のようになっている。

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() などを使って、間接 的に行うことになる。

_ 問題

  1. ウィンドウプロシージャと ProcessWindowMessage() は、どのように結びつけられるのか。
  2. メンバ関数 ProcessWindowMessage() にアクセスするためには、インスタンスを指すポインタ (this) が必要。しかし ::RegistrClassEx() で登録できるウィ ンドウプロシージャは、メンバ関数ではない通常の関数に制限されている。 それではウィンドウプロシージャ経由で、いつ、誰が this を取得しているのか?

キーは ::SetWindowLong() と CWndProcThunk クラス。待て次号。

_ MagicDraw 4.1

上のクラス図を書くのに使ったアプリケーション (デモ版) ですが、あの程度の 小さなクラス図を書くだけで、メモリを 50MB も使うという。 これだから Pure Java アプリケーションってやつは……。


*1:ここで言っているウィンドウクラスは Win32 API RegisterClassEx() で登録す るクラスのこと。C++ のクラスではない。
*2:Java だと abstract class ではなく interface に相当。


2001/4/2(Mon)

Global for Win32

GLOBAL は、ソースファイルのパス名に空白文字が含まれていると gtags.el と組み 合わせて使えないのね。gctags, gtags.el あたりに手を入れて対応。


2001/4/3(Tue)

ATL 探索 (2)

_ 初期ウィンドウプロシージャの登録

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() を読む。

_ CWimdowImpl::this の保存

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 を繋いでいる。データ構造を図にすると、次のようになる。

m_pNext
次のデータへのリンク
m_dwThreadID
スレッドID値 (Key)
m_pThis
CWindowImpl インスタンスへのポインタ (Value)

pData->m_pThis (== m_thunk.cd.m_pThis) は CWindowImpl クラス (あるいは その派生クラス) のインスタンスをさす this ポインタが渡ってきている。個々 の m_thunk.cd はクラス内にスコープが限られるオブジェクトだが、グローバ ルインスタンス _Module からポインタを使ってリンクすることでグローバル に辿れるようになる。

_Module はスレッド間で共有されるグローバルなオブジェクトであるため、デー タ挿入の際には ::EnterCriticalSection(), ::LeaveCriticalSection() を使っ て排他制御している点に注意。

静的配列のように「数に制限があるデータ構造」や、エラー処理などに手間の かかる動的メモリ確保を使わず、事前に確保したメモリ領域をうまく使って、 各オブジェクトを管理している点が巧妙。

ここまでの準備が済んだら、いよいよ Win32 API の ::CreateWindowEx() を 呼び出してウィンドウを作成する。

_ ウィンドウプロシージャの動作 (1) this の取得

::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 では、メッセージ処理の効率を上げるために、もう一工夫凝らし ている。

_ ウィンドウプロシージャの動作 (2) ウィンドウプロシージャの動的再構成

再び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() に移す。

_ さらに

次回に続く


*1:さらに、当該エントリを連結リストから切り離す処理も行っている。
*2:些細なことだけど thunk.m_mov = 0x04244C7 の行のコメントが間違ってる。 正しくは C7 44 24 0C ではなく C7 44 24 04 ですね。


2001/4/4(Wed)

Re: 雑記

Microsoft 製アプリケーションを見ると、アプリケーション固有のデータは My Document ではなく Application Data *1に置き、移動ユーザプロファイルを使ってユーザプロファイル全体を共有する ことを想定しているように思えます。

ただし、移動ユーザプロファイルを使うためには要 Windows NT/2000 Server。


*1:Windows2000 Professional だと、デフォルトで C:\Documents and Settings\%USER%Application Data に作られる。 隠しフォルダ。

CVSWeb.cgi with Apache on Win32

細かいところで問題が出る。

Perl for UNIX では open(FH, "xxx |") すると sh を起動して xxx を実行、 その出力をファイルハンドルに関連付けるが、Win32 では当然 sh は存在しな い。Perl 内部で処理しているみたいだけど、空白文字やシングルクオートの 扱いが sh と違う。

とりあえず CVS リポジトリのルートディレクトリ名に空白を含まないように DOS 互換ファイル名を使い、open() に含まれるシングルクオートを全部消し て対処する。

CVS リポジトリを、スペースを含んだディレクトリの下に置くほうが悪いっ て? ……そうかも。

Re: ベーマガ

「ポップンの大冒険」を拾ってきてみたんだけど、なんかコードがダメダメに 見える。

LPSTR gPath;

gPath = "popn.exe";
GetCurrentDirectory(n, gPath);

そりゃ、落ちるよ。

    ct=GetTickCount();
    while ( 1 ){
        if ( abs(GetTickCount()-ct)>30 ){
            break;
        }
    }

busy loop がダメダメだし、OnPaint でタイマ管理するのも設計がマズすぎ。

_ うーむ

昔からベーマガの投稿プログラムって、こういうレベルでしたっけ?

_ 募集

最近、良質なゲームのソースコード読みたい病に罹患しているので、おすすめ があれば教えて頂きたく *1。ジャンルは問わず、プラットホームは Win32 ということで。


*1:読む時間を取れるのは、先のことになりそうだけど。


2001/4/6(Fri)

お買い物

_[Comic] コミックス

セント・はいぱあ警備隊」は、テンションが異常に高い。読むと ハイ になれること 請け合い。

_[Book] 書籍

前3冊は、いずれも Microsoft PRESS の出版物。いわゆる教科書の類。

ATL, COM 本も探したが、以前にリストアップしておいた書籍が見つからなかっ たので、購入せず。ATL はソースコード読めば分かるので本が無くても構わな いけど、COM は基礎概念と方法論を押さえておかないと、時間を無駄にしそう。

読書記録

今月のZ氏文庫から。


以上、5日分です。

[最新版] [一覧] << == >>

Copyright © 2000 - 2001
Issei Suzuki <issei@issei.org>