Këto ditë, ndërsa jam duke i shtuar aftësinë "Control Container" programit që po krijoj, m'u desh të eksploroj disa nga file-t e ATL-së, dhe ndër të tjera "zbulova" një teknikë mjaft interesante që e justifikon plotësisht emrin "arti i programimit", ndaj dhe vendosa ta ndaj me të tjerët "zbulimin" tim.
Thuajse të gjithë programet që krijohen sot strukturohen në formën e objekteve. Nga ana tjetër pjesa dërrmuese e programeve për Windows janë të tipit GUI, pra komunikojnë me përdoruesit përmes "dritareve". E gjitha kjo do të thotë se thuajse në ç'do program lind nevoja për të shoqëruar një objekt me një dritare.
Natyrisht të gjithë ata që përdorin Delphi-n, C++ Builder, MFC, Visual Basic apo ndonjë platformë tjetër për të krijuar programet e tyre, nuk e perceptojnë fare nevojën për të shoqëruar objektet me dritaret, pasi të gjitha platformat e mësipërme (dhe të tjera) e realizojnë automatikisht këtë.
Sidoqoftë, ndonëse njohja e teknikës që do përshkruaj nuk është e domosdoshme (për më tepër ATL-ja e ofron të gatshëm implementimin e saj), njohja e saj mund të rezultojë e dobishme në situata të tjera, pasi kjo teknikë në vetvete ka të bëjë me gjenerimin e instruksioneve "fluturimthi" (on the fly) dhe shpeshherë njihet me emrin "thunk".
Por fillimisht po përshkruaj 3 teknikat "standarte" për të shoqëruar objektet me dritaret, pasi në këtë mënyrë ka për të dalë më mirë në pah se pse ATL-ja nuk ka "preferuar" asnjë prej tyre, por ka zgjedhur këtë mënyrë "të pazakontë" të të bërit të gjërave.
Mënyra I ka të bëjë me rezervinin e hapësirës së nevojshme në WNDCLASS : : cbWndExtra, dhe përdorimin e kësaj hapësire për të ruajtur pointer-in në objektin që duam të shoqërojmë me një dritare të caktuar. Gjithësesi, nëse dritaren nuk e kemi krijuar ne, nuk kemi asnjë kontroll mbi cbWndExtra, prandaj dhe kjo mënyrë ka përdorim të kufizuar - e rendita thjeshtë për të thënë se ekziston edhe kjo mënyrë.
Mënyra II
Kodi:
// Shoqërojmë objektin me dritaren:
// hwnd është Handle i një dritare
// pObj është një pointer në objektin që duam ta shoqërojmë me këtë dritare
SetWindowLongPtr (
hwnd,
GWLP_USERDATA, // indeksi
(LONG_PTR)pObj
);
// Në implementimin e WindowProc ne mund ta "marrim" pointerin në objektin tonë në këtë mënyrë:
LRESULT CALLBACK WindowProc (HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
// CObjekt përfaqëson klasën e këtij objekti:
CObjekt *pObj = (CObjekt*)GetWindowLongPtr (hwnd, GWLP_USERDATA);
if (pObj)
{
// përdorim objektin:
pObj->Funksion();
pObj->Data = ...
};
};
Problemi kryesor me këtë teknikë ka të bëjë me faktin se ç'do dritare ka vetëm një "slot" (hapësirë) GWLP_USERDATA, dhe ndërsa ky në vetvete mund të mos përbëjë një problem, problemi lind kur dritares tonë mund t'i bëjë "subclass" dikush tjetër i cili gjithashtu vendos të përdorë GWLP_USERDATA duke mbishkruar pointerin tonë, ose anasjelltas, kur ne i bëjmë "subclass" dritares së dikujt tjetër duke mbishkruar informacionin që programi tjetër ka shoqëruar me këtë dritare (duke përdorur GWLP_USERDATA).
Mënyra III
Për shkak të problematikës që ekziston në mënyrën II, Windows-i vë në dispozicion një mekanizëm tjetër: "Window Properties"
Kodi:
// Shoqërojmë objektin me dritaren:
SetProp (
hwnd,
"Pointeri_Im", // emri i properti-së
(HANDLE)pObj
);
// Në implementimin e WindowProc ne mund ta "marrim" pointerin në objektin tonë në këtë mënyrë:
LRESULT CALLBACK WindowProc (HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
// CObjekt përfaqëson klasën e këtij objekti:
CObjekt *pObj = (CObjekt*)GetProp (hwnd, "Pointeri_Im");
if (pObj)
{
// përdorim objektin:
pObj->Funksion();
pObj->Data = ...
};
};
Kjo mënyrë na lejon të shoqërojmë "informacion" me një dritare pa pasur frikë nga ndonjë "konflikt" i mundshëm (për aq kohë sa emri që zgjedhim si property (psh. "Pointeri_Im") nuk koincidon të përdoret edhe nga dikush tjetër).
Sidoqoftë edhe kjo mënyrë me sa duket e ka një problem: "nuk është aq e shpejtë sa do t'ja donte zemra ATL-së".
Mënyra IV (the ATL way)
Të gjitha dritaret e një klase (WNDCLASS(EX)) kanë të përbashkët një funksion të vetëm për procesimin e mesazheve (WndProc). Ndonëse implementimin e këtij funksioni e ofrojmë ne, ky funksion ka për t'u thirrur për të procesuar mesazhet e të gjitha dritareve që kemi krijuar, për pasojë nga brendësia e tij ne s'mund ta dimë nëse filan mesazhi i takon filan dritare (që në programin tonë përfaqësohet nga një objekt i caktuar C++).
Në implementimin e ATL-së të WindowProc më bëri përshtypje type-cast i çuditshëm:
LRESULT CALLBACK WindowProc (HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
// CObjekt përfaqëson klasën e këtij objekti:
CObjekt *pObj = (CObjekt*)hwnd;
Me një fjalë ATL-ja "s'e vriste fare mendjen" as për GetWindowLongPtr(hwnd, ...) dhe as për GetProp(hwnd, ...) por thjeshtë i bënte një type-cast hwnd-së në (CObjekt*).
Të gjitha interpretimet hipotetike që munda t'i bëj këtij instruksioni rezultonin absurde, mbasi si mundet vallë një "handle in a window" (objekt që krijohet dhe menaxhohet nga sistemi i operimit) të mund të trajtohet (përmes type-casting) si një pointer në një objekt që e kam krijuar unë (adresën e të cilit OS-ja s'ka nga ta dijë)?
Pasi u enda për njëfarë kohe mes konstrukeve të ATL-së shpjegimi u bë i qartë:
ATL-ja krijon "flutirimthi" një funksion unik për ç'do dritare, dhe ky funksion bën vetëm 2 gjëra: mbishkruan argumentin hwnd me pObj dhe thërret funksionin origjinal WindowProc. Kështuqë në brendësi të WindowProc hwnd përfaqëson realisht pObj ndaj dhe konstrukti i mësipërm është krejt i rregullt.
Kodi:
/////////////////////////////////////////////////////////////////////////////
// Thunks for __stdcall member functions
#if defined(_M_IX86)
#pragma pack(push,1)
struct _stdcallthunk
{
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
void Init(DWORD_PTR proc, void* pThis)
{
m_mov = 0x042444C7; //C7 44 24 0C
m_this = PtrToUlong(pThis);
m_jmp = 0xe9;
m_relproc = DWORD((INT_PTR)proc - ((INT_PTR)this+sizeof(_stdcallthunk)));
// write block from data cache and
// flush from instruction cache
FlushInstructionCache(GetCurrentProcess(), this, sizeof(_stdcallthunk));
}
//some thunks will dynamically allocate the memory for the code
void* GetCodeAddress()
{
return this;
}
};
#pragma pack(pop)
#elif defined (_M_ALPHA)
// For ALPHA we will stick the this pointer into a0, which is where
// the HWND is. However, we don't actually need the HWND so this is OK.
Fragmenti mësipër është kopjuar direkt nga implementimi i ATL-së. Siç vihet re ekzistojnë implementime për të gjitha familjet e proçesorëve (unë kam kopjuar vetëm implementimin për x86 (#if defined (_M_IX86)) si dhe fillimin e implementimit për alpha (#elif definde (_M_ALPHA))).
Normalisht ne i përdorim strukturat për të rezervuar një "bllok memorje" për disa variabla që kanë lidhje me njëri-tjetrin, pra për të ruajtur "data" në këtë memorje.
ATL-ja nga ana tjetër, me anë të strukturës _stdcallthunk rezervon memorje për të ruajtur një "funksion".
Konkrethisht 4 "fushat" (variablat) e vetëm të kësaj strukture përfaqësojnë (dhe kanë për t'u inicjuar me) numra që më pas kanë për t'ju dhënë procesorit t'i ekzekutojë (duke i konsideruar si instruksione makine)
8 byte-t e parë të kësaj strukture (2 DWORD-et: m_mov, m_this) përfaqësojnë instruksionin mov (në këtë rast instruksioni mov është 8 byte, por ka raste kur ky instruksion është vetëm 1 byte) i cili mbishkruan memorjen në [esp + 0x4] (në [esp] gjendet adresa e instruksionit që e thirri këtë funksion, ndërsa në [esp + 4] ndodhet argumenti i parë, që i bie të jetë hwnd) me vlerën e pointerit në objekt (vlerë që është ruajtur në fushën m_this).
Mbas instruksionit të parë (8 byte-t e mov) vjen instruksioni i dytë 5 byte-sh jmp (1 byte për m_jmp + 4 byte për m_relproc). Ky instruksion bën një jump (goto) m_relproc byte nga pozicioni aktual, "kapërcim" që ka për ta "degdisur" procesorin ekzaktësisht në fillim të funksionit WindowProc().
Meqënëse alternativa e ATL-së angazhon ekzekutimin e vetëm 2 instruksioneve ekstra (mov + jmp) kjo është në absolut mënyra më e shpejtë mes mënyrave të paraqitura mësipër, të cilat angazhonin thirrjen e një funksioni (GetWindowLongPtr(), GetProp()) që në ç'do rast përfshin ekzekutimin e shumë më tepër instruksioneve.
Vlerat e fushave të kësaj strukture (që në fakt përfaqëson code) iniciohen në funksionin Init () i cili merr 2 argumenta: adresën e WindowProc origjinal, si dhe pointerin në objekt. Mbasi inicion strukturën (gjeneron code) ky funksion thërret FlushInstrucionCache() për t'i bërë update cache të procesorit.
Krijoni Kontakt