
A TThread.Suspend eljárás igazából rosszul van elnevezve. A helyes név így hangzik: TThread.RandomlyHangTheProgram. Nem vicc, ezt a helyzet. A legjobb, amit tehetünk, ha egyáltalán nem használjuk, de talán még jobb, ha el is felejtjük.
A Suspend ugyanis megállítja a szálat, akármit is csinál az éppen. Fogva tart egy fájlt? Várólistára mindenkit. Belépett egy critical sectionbe? Nincs több belépés. Képzeljük csak el azt, ha a szál éppen memóriát foglalt. A memóriakezelő is osztott erőforrás, critical section védi a szálak versengése ellen. Ha a szál éppen fogva tartja a critical sectiont amikor a Suspend meghívódik, a memóriakezelő ettől kezdve zárva van minden arrajáró számára. Egy egyszerű szövegművelet is lehetlenné válik, ha ahhoz memóriafoglalás vagy felszabadítás kellene. Ha pedig éppen az a szál próbál memóriát foglalni, amelyik a Suspendet meghívta, és majd később a Resume-ot hívná, kialakul a csuka fogta róka esete, amit csak úgy hívunk: deadlock.
Az olvasóra bíznánk annak végiggondolását, hogy mi történik, ha meghívjuk a win32 API TerminateThread eljárását.
Ma nyomozni fogunk.
Bevezető. A COM egyik alapfunkciója az úgynevezett late binding, azaz hogy meg tudjuk hívni egy objektum valamely metódusát névvel. Olyan ez, mintha Delphiben tudnánk ilyet írni:
DoThisForMe('ShowMessage', ['Hello, World']);
Ugyanez más nyelveken előfordul, például a legtöbb SQL implementációban van valami "EXECUTE IMMEDIATE" vagy "EXEC" vagy hasonló parancs.
A COM is tud ilyet, úgy hívják a metódust, hogy Invoke, és az IDispatch interface tagja. Fejléce a dokumentáció szerint az alábbi.
HRESULT IDispatch::Invoke(
DISPID dispidMember,
REFIID riid,
LCID lcid
unsigned short wFlags,
DISPPARAMS FAR* pdispparams,
VARIANT FAR* pvarResult,
EXCEPINFO FAR* pexcepinfo,
unsigned int FAR* puArgErr,
)
A dispidMember egy szám, a meghívandó eljárás vagy függvény azonosítója. Azonosítót kaphatunk a névből az IDispatch interface GetIDsOfNames eljárásával. A paramétereket egy struktúrában kell átadni, melynek neve DISPPARAMS. Nem egyszerű, nem is lehetetlen, ám most ezt nem ismertetjük. A végeredményt pedig a pvarResult-ban kapjuk.
Amire most szeretnénk kiemelten felhívni a figyelmet, az az lcid paraméter. Az lcid a Locale Identifier rövidítése, és azt adja meg, hogy milyen lokalizációs beállításokat használ a hívó. Azaz mi a tizedeselválasztó, mi a dátumformátum, mi a nyelv, stb. Minek ez?
Mivel az IDispatch elsősorban scripting céllal készült, feltételezhető, hogy programozásban kevésbé jártas felhasználók kezébe is kerülhet. Ezért, vagy másért, lehetőséget akartak adni arra, hogy az egyes metódusok lokalizált paramétereket kaphassanak. Kiváló példa erre az Excel Range objektumának NumberFormat adattagja. Ha az Invoke-nak átadott lcid 1033 (English-US), akkor az ezreselválasztó a vessző, a tizedeselválasztó pedig a pont. Ha viszont az lcid 1038 (Hungarian), akkor az ezreselválasztó a szóköz, a tizedeselválasztó pedig a vessző.
Az IDispatch interface nem való arra, hogy magunk hivogassuk. A paraméterek összeállítása sokkal bonyolultabb, mint a megfelelő interface-t használni, és direktben hívni meg a kívánt metódust. Delphiben egy igen egyszerű módszer áll rendelkezésünkre az IDispatch hívására, és ez a variant típus.
var
MyIntf: IDispatch;
MyVar: variant;
...
MyVar := MyIntf;
Myvar.Method(Param1, Param2);
A Delphi ilyenkor némi "compiler magic" alkalmazásával valójában egy GetIDsOfNames hívást generál a "Method" szövegre, majd a kapott azonosítóval meghívja az Invoke-ot, mégpedig két paraméterrel.
Most jön a feladvány.
A fentiek értelmében azt szeretnénk, ha az lcid 1033 lenne, hogy a NumberFormat beállítást egyszerűen el tudjuk végezni (pl "#,##0.00" értékre). Csakhogy a variant módszer esetén nem látszik arra lehetőség, hogy beállítsuk. Akkor mit fog a Delphi odaírni? Mi lesz a lcid paraméter értéke?
Ennek kiderítésére a help természetesen nem alkalmas. Nyomozni szükséges. Kezdjük a nyomozást azzal, hogy a compiler magic mögé nézünk. Ehhez tegyünk breakpointot a Method hívás sorára, és futtassuk a progamot. Amikor a breakpointon leáll, nyissuk ki a CPU ablakot.
A paraméterek beállítása után meghívódik egy rutin, ami a VariantManager nevű valaminek a része. Hol lehet ez a fura lény? Mivel nem sok unitot esélyes, könnyen végig is nézhetjük őket. De ez a funkcionalitás a Delphi nyelv alapvető része, hiszen bármikor írhatok ilyesmit, akkor is, ha egyetlen unitot sem használok. Célszerű ezért a keresést a system.pas-ban kezdeni.
A system.pas-ban keresve meg is találjuk a keresett állatfajtát:
type
TVariantManager = record
VarClear: procedure(var V : Variant);
VarCopy: procedure(var Dest: Variant; const Source: Variant);
...
DispInvoke: procedure(Dest: PVarData; const Source: TVarData;
CallDesc: PCallDesc; Params: Pointer); cdecl;
...
end;
var
VariantManager: TVariantManager;
Ez tehát egy record, aminek adattagjai eljárások címei. Az ilyesmit akkor szokta használni a Borland, ha azt akarja, hogy a definíció a system.pas-ba kerüljön, de a tényleges implementációt majd másik unit fogja megadni. Hasonló módon működik a memóriakezelő is. Nézzük csak, hogyan is inicializálja a recordot.
Az initialization részben ráakadunk az InitVariantManager eljárás meghívására. Ennek tartalma:
P := @VariantManager;
for I := 0 to (SizeOf(VariantManager) div SizeOf(Pointer))-1 do
P[I] := @VariantSystemUndefinedError;
VariantManager.VarClear := @VariantSystemDefaultVarClear;
Na ez nem sok. Úgy látszik, törlésen kívül semmi egyéb nincs kitöltve. Tényleg valaki más tölti ki. Keressünk a Delphi Source mappájában levő fájlokban "VariantManager" kifejezést. Az egyetlen találat, és egyben a következő állomásunk a variants unit.
Keressük a hivatkozást a VariantManager-re. Ezt találjuk az inicializációban:
GVariantManager.VarClear := @_VarClear;
GVariantManager.VarCopy := @_VarCopy;
...
GVariantManager.DispInvoke := @_DispInvoke;
...
SetVariantManager(GVariantManager);
A GVariantManager várakozásunknak megfelelően egy TVariantManager. A fentiek szerint tehát keressük a _DispInvoke eljárást, az lesz a mi barátunk.
procedure _DispInvoke(Dest: PVarData; const Source: TVarData;
CallDesc: PCallDesc; Params: Pointer); cdecl;
var
LSourceType: TVarType;
LSourceHandler: TCustomVariantType;
begin
LSourceType := Source.VType and varTypeMask;
if Assigned(Dest) then
_VarClear(Dest^);
if LSourceType < CFirstUserType then
VarDispProc(PVariant(Dest), Variant(Source), CallDesc, @Params)
else if FindCustomVariantType(LSourceType, LSourceHandler) then
LSourceHandler.DispInvoke(TVarData(Dest^), Source, CallDesc, @Params)
else
VarInvalidOp;
end;
Az elején megállapítjuk a Source paraméter (ez nyilván a variant, amire hívtuk) típusát. A maszkolás azért kell, mert a típusok kívül a VType tárolja azt, hogy tömbről van-e szó, és egyéb paramétereket. Ezután a Dest paraméter törlése következik, akármi is legyen az. Majd egy döntés a Source paraméter típusa alapján. Sem a help, sem a TVarData definíciója nem ad segítséget, hogy melyik if ág tartozik ránk. Gyanús, hogy itt az úgynevezett "Custom Variant type" támogatással futottunk össze. További utalást a CFirstUserType definíciója ad.
{ TCustomVariantType support }
{ Currently we have reserve room for 1791 ($6FF) custom types. But, since the
first sixteen are reserved, we actually only have room for 1775 ($6EF) types. }
const
CMaxNumberOfCustomVarTypes = $06FF;
CMinVarType = $0100;
CMaxVarType = CMinVarType + CMaxNumberOfCustomVarTypes;
CIncVarType = $000F;
CFirstUserType = CMinVarType + CIncVarType;
CInvalidCustomVariantType: TCustomVariantType = TCustomVariantType($FFFFFFFF);
Igen. Tehát a standard típusok kódjai CFirstUserType alattiak, így a mi utunk a VarDispProc irányába visz, hiszen az IDispatch egy standard típus.
type
TVarDispProc = procedure (Dest: PVariant; const Source: Variant;
CallDesc: PCallDesc; Params: Pointer); cdecl;
var
VarDispProc: TVarDispProc;
Hopp. Ismét egy eljárást tartalmazó változó. Valakinek ki kell tölteni. Keressünk rá! Az initialization részben bukkan fel ismét.
VarDispProc := @_DispInvokeError;
procedure _DispInvokeError;
asm
MOV AL,System.reVarDispatch
JMP System.Error
end;
Ajjaj. Ez rossz jel. Az itt megadott eljárás csak egy alapértelmezett csonk, hibaüzenetet generál. Akkor tehát valaki más tölti ki az igazi VarDispProc-ot. Keressünk rá a Source mappában. A ComObj unit az egyetlen idevágó találat. Nézzük csak!
initialization
...
VarDispProc := @VarDispInvoke;
Na ez már valami. Nézzük az eljárást.
procedure VarDispInvoke(Result: PVariant; const Instance: Variant;
CallDesc: PCallDesc; Params: Pointer); cdecl;
procedure RaiseException;
begin
raise EOleError.CreateRes(@SVarNotObject);
end;
var
Dispatch: Pointer;
DispIDs: array[0..MaxDispArgs - 1] of Integer;
begin
if (CallDesc^.ArgCount) > MaxDispArgs then raise EOleError.CreateRes(@STooManyParams);
if TVarData(Instance).VType = varDispatch then
Dispatch := TVarData(Instance).VDispatch
else if TVarData(Instance).VType = (varDispatch or varByRef) then
Dispatch := Pointer(TVarData(Instance).VPointer^)
else RaiseException;
GetIDsOfNames(IDispatch(Dispatch), @CallDesc^.ArgTypes[CallDesc^.ArgCount],
CallDesc^.NamedArgCount + 1, @DispIDs);
if Result <> nil then VarClear(Result^);
DispatchInvoke(IDispatch(Dispatch), CallDesc, @DispIDs, Params, Result);
end;
Haladunk! Láthatóan az eljárás megkezdi a variáns feldolgozását, megvizsgálja a paramétereket (amiket valamilyen varázslatos módon a Delphi eddigre szépen összecsomagolt neki), valamint kiveszi az IDispatch interface-t. Annak GetIDsOfNames metódusával megszerzi a hívandó azonosítót. Végül a DispatchInvoke következik. Megjegyzésre érdemes a RaiseException eljárás, amit egyetlen helyről hív. Ez egy tipikus példa arra, hogy mit nem szabad eljárásba tenni. Ha a tartalmát képező egyetlen sort bemásolták volna a hívás helyére, érthetőbb és rövidebb lenne a kód. A másik észrevétel, hogy a GetIDsOf Names eljárást felesleges minden alkalommal meghívni, elég lenne minden névre egyszer. De ez talán megbocsátható egyszerüsítés.
Na de fordítsuk figyelmünket a DispatchInvoke eljárás felé. Itt lesz a lényeg! Az eljárás igen hosszú, ennek oka az, hogy valóban megkezdi az érdemi munkát, és elkezdi összeállítani az Invoke komplex adatszerkezeteit. Hosszasan követve a hullámzó szélű kódot végül az alábbi részletre bukkanunk.
Status := Dispatch.Invoke(DispID, GUID_NULL, 0, InvKind, DispParams,
Result, @ExcepInfo, nil);
Megvan! Melyik is az lcid paraméter? Ha az olvasó visszalapoz, megláthatja, ha nem, elmondjuk: a harmadik. Ami itt nulla. Nem globális változó, nem valami trükkös módon szedi elő a paraméterlistából, nem 1033, hanem egyszerűen nulla. Egyáltalán, mit jelent a nulla lcid? Segít a dokumentáció, mégpedig ez itt.
0x0000 Neutral locale LOCALE_NEUTRAL Language corresponds to the neutral locale. Your applications generally do not use this identifier. Instead, they use either LOCALE_SYSTEM_DEFAULT or LOCALE_USER_DEFAULT.
Vonjuk le a tanulságokat.
MyCells.NumberFormat := '#' + ThousandSeparator +
'##0' + DecimalSeparator + '00';
Igazán kényelmes. A célunkat ugyan nem értük el, de ma is sokat tanultunk a Delphi lelkivilágáról.
Van olyan, hogy az ember balszerencséjére a windows saját eszköztárához kell nyúljon, ezzel elhagyva a delphi többé-kevésbé védett és biztonságos világát. Az egyik nehezebben érthető feladat az, amikor az adott rendszerfunkció egy szöveget akar visszaadni, amihez nekünk kell buffert szolgáltatni. Legyen a példánk a GetCurrentDirectory. Ennek fejléce a win32 programmer's reference szerint
DWORD GetCurrentDirectory(
DWORD nBufferLength, // size, in characters, of directory buffer
LPTSTR lpBuffer // address of buffer for current directory
);
A delphi szerint (windows.pas):
function GetCurrentDirectory(nBufferLength: DWORD;
lpBuffer: PChar): DWORD; stdcall;
A dokumentáció az is elmondja, hogy az lpBuffer egy általunk lefoglalt memóriaterületre mutasson, oda fogjuk megkapni a kért adatot. Fontos, hogy elég hely legyen a teljes mappanévnek, valamint egy záró nullának (a C nyelv szokásos szövegtípusa olyan, hogy a végét egy nulla karakter jelzi, tehát egyel több hely kell, mint ahány karakter van). Ha az átadott buffer túl kicsi, a dokumentáció szerint a visszatérési érték megmutatja, hogy mekkora hely kellett volna legalább. Mégpedig nem azt, hogy hány karaktert fogunk kapni, hanem hogy mekkora buffert kell adni, azaz a lezáró nulla is benne van már.
Nyilván nekünk az lenne a célunk, hogy a végeredmény egy string változóban legyen:
function GetCurrentDirectory: string;
Hogy fogjunk hozzá? Alább bemutatunk pár szokásos megoldást, aztán majd elemezzük.
Az egyik megoldás a karaktertömbös buffer, delphire bízott konverzióval.
function GetCurrentDirectory: string;
var
Buffer: array[0..MAX_PATH] of char;
Len: integer;
begin
Len := Windows.GetCurrentDirectory(SizeOf(Buffer), @Buffer);
if (Len = 0) or (Len > SizeOf(Buffer)-1)
then Result := ''
else Result := Buffer;
end;
Mi itt a lényeg? Az a bizonyos memória, amit a windows kért tőlünk, egy lokális változó lesz, egy karaktertömb. Ennek méretét MAX_PATH-nak választottuk, abban reménykedve, hogy nem lesz hosszabb. Mivel megkapjuk a szükséges méretet, erre tudunk vizsgálni, tehát ha ez a helyzet, üres visszatérési értékkel jelezzük, hogy baj van. (Hamar feladtuk, de most lusták vagyunk.) A Len=0 teszt talán elég felesleges, mivel ez a hívás nem nagyon tud hibára futni. De sose lehet tudni, jobb a biztonság. Ha minden rendben volt, string típusra konvertáljuk a karaktertömböt, amihez nem kell csinálni semmit, a delphi automatikusan megcsinálja értékadáskor. Tehát az értékadás valójában egy új string létrehozását, és a karakterek átmásolását jelenti. Honnan tudja a delphi, hogy milyen hosszú legyen a szöveg? A véget jelző nullát keresi. Tulajdonképpen itt nem karaktertömb - string konverzió történik, hanem PChar - string konverzió, de a delphi előzékenyen odagondolja helyettünk a @-ot, mintha ezt írtuk volna
else Result := PChar(@Buffer);
A második módszer hasonló, csak most nem bízzuk a delphire, hogy a hosszt meghatározza, hanem segítünk neki:
else SetString(Result, Buffer, Len);
A függvény többi része változatlan. A SetString első paramétere a string változó, amit be akarunk állítani. A második paraméter a buffer, ahonnan a karaktereket vegye, és a harmadik adja meg, hogy hány karaktert másoljon. Ezzel a módszerrel egy hosszabb szöveg egy részéből is csinálhatunk stringet, nem kell a végére lezáró nulla. Megjegyzendő, hogy a SetString második paramétere valójában PChar lenne, de a delphi itt is előzékenyen odaképzeli a referenciaképzést.
Az első módszer előnye, hogy érthetőbb, nincs szükség a feleslegesnek tűnő SetString-re. Nem előny, nem hátrány, csak különbség, hogy az első megoldás a lezáró nullától függ, a második pedig nem. A másodikkal részeket is kimásolhatunk, vagy akár null-karaktereket is tartalmazhat a szöveg. Ez utóbbi elég ritka dolog a windowsban, de azért előfordul itt-ott.
Mindkét módszernek hátránya azonban, hogy ha a végeredmény mégiscsak hosszabb lenne, mint MAX_PATH, akkor kudarcot vallanak. Ezen segít a dinamikus memóriafoglalás.
function GetCurrentDirectory: string;
var
Buffer: PChar;
Len: integer;
begin
Len := Windows.GetCurrentDirectory(0, nil);
Buffer := AllocMem(Len);
Len := Windows.GetCurrentDirectory(Len, Buffer);
if (Len = 0)
then Result := ''
else Result := Buffer;
FreeMem(Buffer);
end;
Kis furcsaság, hogy meghívjuk a GetCurrentDirectory-t nulla mérettel és buffer nélkül. Ez arra szolgál, hogy kicsikarjuk belőle a valódi bufferméretet. A második hívás az igazi, de akkorra már teljes méretében legyártott bufferrel várjuk. Figyelem! A lezáró nullára ilyenkor már nem kell gondolni (nincs Len+1), mivel a rutin nem a szöveg hosszát adja vissza ugyebár, hanem a szükséges bufferméretet. Bátor (botor) módon nem használunk try-finally-t, mert azt ígérték nekünk, hogy a windows rendszerfunkciók nem dobálnak exceptiont, ha helyes paramétereket kapnak, márpedig most azt kaptak. Nem hiba try-finally-t használni, csak tessék, csak tessék.
Vannak, akik nem és nem szeretik a dinamikus memóriafoglalást. Nekik is mutatunk valamit. A string típus kiváló buffernek is, és mivel úgyis ez a végcél, egyből az eredménybe kérjük le az adatot.
function GetCurrentDirectory: string;
var
Len: integer;
begin
Len := Windows.GetCurrentDirectory(0, nil);
SetLength(Result, Len - 1);
Len := Windows.GetCurrentDirectory(Len, PChar(Result));
if (Len = 0) then Result := '';
end;
Nos, ez igazán szép egyszerű. A SetLength egy olyan eljárás, ami a szöveg hosszát erőszakkal beállítja a megadott értékre. A tartalom véletlenszerű lesz. Eközben arról is gondoskodik, hogy a szöveg referenciaszámlálója egy legyen, azaz ne osztozzunk másokkal az értéken (ugye mindenki tudja, hogy működik az AnsiString?). A hossz Len-1, hiszen a kapott érték a buffer hossza, ami egy db lezáró nullával több, mint az adat. Ez pont jó lesz, mert a delphi előzékenyen biztosít helyet a lezáró nullának. Mivel tehát a tartalomra kizárólagos jogokkal rendelkezünk, bátran felkérhetjük a windowst, hogy egyből oda tegye a végeredményt. Ehhez nem kell más, mint a stringet bután PCharrá castolni, hisz a string típus lényegében az, egy pointer, ami a szövegre mutat. Ha a korábbi megoldásoknál kiírtuk volna a try-finally-t, ide akkor sem került volna. Mivel a string típust a delphi menedzseli helyettünk, a memóriafelszabadítással nem kell foglalkozni hiba esetén sem.
Ez utóbb megoldás a legegyszerűbb, de megvan az a baja, hogy érteni kell, mit csinál az ember. Általában sem szerencsés dolog a typecast, és általában sem szerencsés dolog az AnsiString típussal játszani, de a kettő együtt meg kimondottan életveszélyes. Ráadásul itt nemcsak használjuk, de még módosítjuk is az értékét. Mindazonáltal, aki érti, hogy mi miért történik, használhatja ezt a megoldást, mert ez nem trükk, hanem dokumentáltan és biztosan működik. De ha valaki nem érzi magát biztonságban, térjen bátran vissza a bufferes megoldáshoz, nincsen azzal sem semmi baj.
P.S.: Nem véletlen, hogy teljesítményszempontokat sehol sem említettem. Azért nem, mert ilyen rendszerhívásokkal nem szoktunk tucat megabájtokat mozgatni ciklusmagban. Azt a párszáz bájtot ide-oda tologatni pedig nem idő, ilyesmit az ember nem optimalizál.
Régebben említettem az Anonymous Pipe nevű állatfajtát, ami kiválóan alkalmas a saját magunk által létrehozott alkalmazásokkal való kommunikációra. Most azt fogjuk megnézni, hogy azért nem annyira kiválóan.
Kicsit nézzük át, hogy kell velük bánni.
Elsőként a pipe-ot létre kell hozni, erre szolgál a CreatePipe eljárás, ami ad nekünk két handle-t, az egyik a bejárat, a másik a kijárat. A többi paraméterrel most nem foglalkozunk.
if not CreatePipe(hRead, hWrite, nil, 0) then ... handle error
Az olvasás ReadFile-lal történik a hRead handle segítségével, példa:
var
Buf: array[0..127] of char;
...
if not ReadFile(hRead, Buf, SizeOf(Buf), BytesRead, nil) then begin
case GetLastError of
ERROR_BROKEN_PIPE: ... other side closed
...
end;
end;
...
Külön figyelmet kell fordítani arra, hogy az ERROR_BROKEN_PIPE hibakód szituációtól függően nem biztos, hogy hiba. Akkor keletkezik, ha a másik oldal befejezettnek tekinti a kommunikációt, és lezárja a pipe-ot. Ugyancsak figyelni kell arra, hogy a harmadik paraméter nem az olvasandó adat mennyiségét jelzi, csak a maximum adatmennyiséget. A hívás kevesebb adattal is visszatér, amelyről a BytesRead érték ad tájékoztatást. Ugyanakkor a hívás sosem tér vissza nulla adattal. Ha nincs kiolvasható adat, az eljárás nem tér vissza, hanem megvárja, amig legalább egy bájtnyi adat kerül a pipe-ba, és azzal tér vissza. Táblázatosan így lehetne a viselkedését összefoglalni:
| Helyzet | Tevékenység |
| Nincs adat | Vár, amig adat érkezik |
| Kevesebb adat van, mint a Buf méret | A rendelkezésre álló adatot adja vissza, a BytesRead tájékoztat a mennyiségről |
| Több adat van, mint a Buf mérete | Buf mennyiségű adatot ad vissza, a többi a pipe-ban marad későbbi kiolvasásra. |
Szerintem ez butaság, feleslegesen bonyolult. Én úgy csináltam volna, hogy ami adat éppen van, annyit ad vissza, legyen az akár nulla is.
Írni a pipe-ba a WriteFile eljárással lehet, mégpedig a hWrite felhasználásával, például:
var S: string;
...
if not WriteFile(hWrite, S[1], Length(S), BytesWritten, nil) then
... handle error
Érdekesség, hogy a ReadFile-hoz hasonlóan itt is van BytesWritten, de csak azért, mert a WriteFile más eszközökhöz is való. Pipe esetén soha nem fordul elő, hogy a BytesWritten ne legyen azonos az irandó bájtok számával. Ami viszont előfordulhat, hogy a pipe belső puffere megtelik. Ilyenkor a hívás várakoztatva van, amig nem szabadul fel elegendő hely. A viselkedést az alábbi táblázat szemlélteti:
| Helyzet | Tevékenység |
| A pipe belső pufferébe nem fér el az adat | Várazkozik, amig felszabadul elég hely |
| A pipe belső pufferében van elég hely | Az adat a pufferbe kerül, és az eljárás visszatér |
Akkor most adjunk magunknak feladatot, és járjunk utána, hogy lehet megvalósítani. Legyen az a cél, hogy használjuk a pipe-ot non-blocking módban, tehát akkor olvassunk, amikor van rajta adat, és akkor írjuk, amikor tudja fogadni. Amúgy meg foglalkozzunk valamely egyéb tennivalónkkal. Elsőként döntsük el egy pipe-ról, hogy van-e olvasható adat rajta.
Ha visszaemlékszünk a ReadFile táblázatára, látjuk, hogy a feladat azzal nem oldható meg. Ugyanis ha nincs adat, ezt az eljárás nem közli velünk, hanem megvárja, míg lesz.
Van a ReadFile eljárásnak egy olyan szogáltatása, hogy Overlapped IO. Ebbe most nem fogunk belemenni túl részletesen, a lényege, hogy a műveletet nem végzi el azonnal, hanem a háttérben csinálja, és szól, ha elkészül. Sajnos csak akkor használható, ha a fájlt eleve a megfelelő módon hoztuk létre (ez kreténség, de ők tudják). A pipe létrehozásánál azonban nincs ilyen lehetőség. Ez zsákutca.
Van egy ReadFileEx API eljárás is. Az átvesz paraméterként egy callback rutint, amit meghív, ha kész van. De ez is csak Overlapped IO-val működik. Újabb zsákutca.
A WaitForSingleObject eljárás egy sor dologra tud várakozni, hátha tud pipe-ra is. Ez elsőre furán hangzik, hisz épp a várakozást akartuk elkerülni. Ám a WaitForSingleObject átvesz egy paramétert, hogy mennyit várakozzon, és ez lehet 0 is. Ilyenkor nevével ellentétben nem vár, hanem csak jelzi, hogy amire várnia kellett volna, az még nem áll rendelkezésre. Lássuk csak, mit ír a dokumentáció, mire lehet várakozni:
Change notification ... Console input ... Event ... Mutex ... Semaphore ... Timer ...In some circumstances, you can specify a handle of a file, named pipe, or communications device as a synchronization object in lpHandles. However, their use for this purpose is discouraged.
Discouraged. Hm. Ez nem hangzik túl jól, ráadásul az anonymous pipe-ot nem is említi, sem itt, sem a felsorolásban. Egyáltalán, mi az, hogy disocuraged? Valamit vagy lehet használni, vagy nem, vagy csak bizonyos körülmények között. Olyan nincsen, hogy discouraged. Azért csak próbáljuk ki:
case WaitForSingleObject(hRead, 0) of WAIT_OBJECT_0: ... data to read? WAIT_TIMEOUT: ... no data? WAIT_FAIL: ... can't use on pipes? end;
Azt tapasztaljuk, hogy az eredmény minden esetben WAIT_OBJECT_0, akár van adat, akár nincsen. Pech. Így sem jutottunk előre. A feladat triviálisan egyszerűnek tűnik, de most úgy fest, hogy egyáltalán nincs megoldás.
Nézzük, mit mond a Google. Legyen a keresőkifejezés mondjuk anonymous pipe data available. Ha elég türelmesek vagyunk, eljutunk egy érdekes WinAPI ejáráshoz: PeekNamedPipe. Na de mi köze ennek a mi problémánkhoz, amikor mi anonymous pipe-ot haszálunk? Nézzük csak a leírást:
hNamedPipeA handle to the pipe. This parameter can be a handle to a named pipe instance, as returned by the CreateNamedPipe or CreateFile function, or it can be a handle to the read end of an anonymous pipe, as returned by the CreatePipe function. The handle must have GENERIC_READ access to the pipe.
Aha. Logikus, a függvény neve teljesen világosan mutatja, hogy ez named pipe-okra vonatkozik, és nincs is rá semmi utalás a CreatePipe leírásánál, de valójában anonymous pipe-okhoz is ugyanolyan jó. Ravasz. Ezzel a tudásunkkal már könnyű a dolgunk:
var TotalBytesAvail: cardinal; if not PeekNamedPipe(hRead, nil, 0, nil, @TotalBytesAvail, nil) then ... handle error if TotalBytesAvail > 0 then ... there is data to read
Ejha, ez nem volt könnyű. De végülis sikerült számos kalandon keresztül eljutni a célhoz. Az alkotó megpihen, ám szörnyű gondolat hasít belé: na de hogyan kell vajon aszinkron módon írni a pipe-ba?
Az a csodálatos öröm ért, hogy betekintést nyerhettem egy programba, amit egy itt meg nem nevezett cég készített belső használatra. A szerzők nem gyakorlott programozók voltak, sőt, igazából nem is programozók, csak hát magad uram, ha szolgád nincs. Jónéhány megoldásuk ide kívánkozik sajnos.
A mai példánk egy ordas nagy hiba, amit hatalmas szerencsével úsztak meg. (A program ugyanis évek óta éles környezetben fut.)
Figyeljük csak meg az alábbi (nem kimásolt: reprodukált) példát.
function ZZSomefunc(params): PChar; var Chars: array[0..1023] of char; begin ... stuff Chars array with a null terminated string Result := Chars; end;
Profik tudják, mi a baj ezzel. Kezdőknek fogalmam sincs, hogy magyarázzam. Először is egy apróság. A delphi elég rendes, és számos "értelmes" műveletet elvégez helyettünk. Például simán átadhatunk egy string helyébe PChar értéket, a delphi az eljáráshívás idejére készít egy átmeneti string változót, abba bemásolja a szöveget, és azt adja át paraméterként. Vagy egy másik kedvesség a rekord-pointerek esetén az automatikus dereference:
var RectPtr: PRect; ... RectPtr.Left := 0; // instead of RectPtr^.Left
A fenti eljárás utolsó sora is egy ilyen kényelmi funkció. A Result típusa PChar, a tömbé array of char, mégis megengedi a delphi az értékadást, mégpedig az alábbi értelemben:
Result := @Chars;
Tehát az eredménybe a tömbre mutató pointer kerül. A PChar nem gondoskodik arról, hogy a karaktereknek hely foglaltassék. A PChar egy teljesen buta pointer, semmivel nem több, mint az alábbi típus:
type TByteArray = array[0..MaxInt-1] of byte; PByte = ^TByteArray; TCharArray = array[0..MaxInt-1] of char; PChar = ^TCharArray;
A PChar csak egy mutató, ami mutathat bármire:
var MyChars: PChar; S: string; Chars: array[0..10] of char; begin S := 'hello'; MyChars := PChar(S); // points to "hello" MyChars := @S[2]; // points to "ello" MyChars := @Chars; // now points to chars array MyChars := @Chars[5]; // now points to the middle of chars array MyChars := nil; // points to nowhere MyChars := PChar(177); // points to an illegal position end;
Ennek számos következménye van. Vegyük például az alábbi programrészletet:
var Chars: array[0..1023] of char; CSTR1, CSTR2: PChar; begin // stuff something in Chars CSTR1 := Chars; // stuff something else in Chars CSTR2 := Chars; SomeProc(CSTR1, CSTR2); // same string twice! end;
A fenti részlet nem azt csinálja, amit képzeletbeli írója gondolhatott. A két CSTR nevű változóban azonos érték lesz, mindkettő ugyanarra a tömbre mutat. Ennek értéke kezdetben az első valami volt, majd átírtuk egy másik valamire, és végül erre a másik valamire mutató pointereket adtuk át az eljárásnak.
Most térjünk vissza a hibás példára. Ha a PChar nem foglalkozik a karakterekkel, akkor hol vannak azok? A lokális változókat a delphi a veremben hozza létre. A verem célja az eljárások paraméterei és a visszatérési cím tárolása, de szinte minden fordító felhasználja a lokális változók vagy más segédváltozók tárolására is. Mégpedig azért, mert a veremben foglalni memóriát ingyen van, míg az igazi memóriafoglalás egy bonyolult művelet.
Hogy egy eljáráshívás mennyi helyet foglal le a veremből, azt nem könnyű megtudni. Függ a hívási konvenciótól (stdcall, cdecl, stb), a paraméterek számától, attól, hogy az optimalizáció be van-e kapcsolva, a fordító verziójától és még sokminden mástól is.
Anélkül, hogy részletekbe belemennénk, annyit mondhatunk, hogy egy eljárás a visszatérése után a saját lokális változóit a veremből "törli" ugyan, de ez a törlés ugyanolyan felületes, mint egy lemez gyorsformázása: az adat maga ott marad, amig a következő arrajáró át nem írja. Tehát ha szerencsénk van, a fenti "módszer" működőképes is lehet, előfordulhat, hogy a veremben a vadak martalékául hagyott szövegünket senki nem bántja a - remélhetőleg közeli - felhasználásig. Ám az is lehet, hogy bántja.
Mi a megoldás?
Általános megoldás nincs. Legjobb lenne elkerülni a PChar-t, de sajnos az nem fog menni, ha ez egy dll-ben implementált függvény, hiszen akkor nem használhatunk string-et.
Mondhatnánk, hogy dinamikusan kell foglalni a memóriát:
function ZZSomefunc(params): PChar; begin Result := AllocMem(...); ... stuff Result^ with a null terminated string end;
De akkor felmerül a kérdés, hogy ki fogja felszabadítani? Esetleg a hívó fél dolga lehetne, de ez egyrészt nem szép, mert akkor egyet kell érteniük a memóriafoglalás/szabadítás részleteiről, másrészt nem biztos, hogy lehetséges, hiszen ha a hívó fél egy másik program, dll, stb, az nem tud hozzáférni a mi memóriánkhoz. Persze arra is van lehetőség, hogy adjunk lehetőséget a hívó oldalnak a felszabadításra:
procedure ZZFreePChar(P: PChar); begin FreeMem(P); end;
Bizonyos esetekben előfordulhat, hogy a szövegek nem dinamikusan állnak elő, hanem eleve léteznek. Ilyenkor nem kell aggódnunk a felszabadítás miatt, a saját tárolt szövegpéldányunkra adunk egy pointert:
function ZZGetWord(Index: integer): PChar; begin ... check if index is OK Result := @Words[Index]; end;
De ha a mi esetünk nem ilyen, ez az út nem járható. Az általános és szokványos megoldás az, hogy a hívó fél foglalja le a memóriát, és átadja az eljárásnak:
function ZZSomefunc(params; Buf: PChar; BufLength: cardinal): cardinal;
begin
Result := the length;
if Result < BufLength then
... stuff Buf^ with a null terminated string
end;
end;
Persze ilyenkor meg az a baj, hogy mekkora legyen a lefoglalt memória, és mi történjen, ha nem elég nagy. Ezért az ilyen eljárások legtöbbje visszaadja, hogy mekkora az adat. Ha belefért az adott területre, akkor oda is lett másolva, ha nem, akkor újra kell hívni, immár elegendően nagy adatterülettel.
Még egy utolsó apróság. Hogy működhetett éveken át a fenti hibás megoldás? A titok az, hogy a verem fordítva működik, felülről lefele. Tehát a szöveg a végétől kezdett erodálódni, és mivel jócskán volt biztonsági ráhagyás, a tönkretett terület sosem érte el magát az adatot. Ez egy csapda: csak egyszer kerüljön egy veremzabáló eljárás a feldolgozás lépései közé, és máris kész a felderíthetetlen hiba. Néhány felhasználó meg nem érti, hogy miért működik kiválóan a programja bekapcsolt optimalizációval, anélkül meg nem mindig.
Rengetegszer fordul elő kezdő programozóknál (meg nem annyira kezdőknél) az alábbihoz hasonló kérdés.
PostMessage(WM_USER, PChar(S), 0) API hívással akarok egy string típusú változót elküldeni, de a túloldalra már nem érkezik meg, vagy hibát ad.
Vagy egy másik gyakori eset.
Egy rekordot akarok fájlba írni, de a szöveges részek nem jól töltődnek be, és gyakran elszáll.
type
TMyData = packed record
Name: string;
Age: byte;
...
end;
A hiba nyilvánvaló, a string típus valójában csak egy pointer. A fenti példákban a pointert használtuk fel, de a valódi tartalom nem került a célhelyre.
Miért lehet delphiben ilyen könnyen hibázni? Miért kellene érdekelje a felhasználót a string típus megvalósítása, miért nem szól a Delphi, hogy ezt nem lehet? A fordító dolga ismerni az adat típusát, és az előírt műveletet vagy értelmesen hajtani végre, vagy pedig hibát jelezni. Elvégre úgynevezett erősen típusos nyelvről van szó.
Csakhogy ez a fenti példákban azért nem történik, mert mi magunk közöltük a fordítóval, hogy ne tegye. A fenti nyelvi struktúrák típustalanok! A typecast és barátai éppen arra valók, hogy egyes típusokat ellenőrizetlen módon adjunk el valami más típusnak. Az ilyesfajta trükkök nemkívánatosak egy korrekt, magasszintű nyelvben.
Na de akkor mi a megoldás? A fenti példák nem a hacker magazinból valók, ilyenekkel találkozni Borland által írt példaprogramokban, vagy akár a dokumentációban is. Igen, sajnos a Delphi számos esetben kényszerít minket, hogy a szigorú típusosságot feladjuk. Ezért Delphi környezetben nem áll meg az alapigazság, hogy ne ássuk alá a típusosságot. Viszont célszerű felismerni az ilyen alantas helyzeteket, és különös óvatossággal mozogni az aknamezőn.
A teljesség igénye nélkül a Delphibe rejtett nehezen megkerülhető típustalanságok:
Direkt typecast, PChar(Str)
Elsősorban WIndows API hívásokhoz, vagy más interface híváshoz használatos. Sok baja van: az egyik, hogy nem szabad olyan eljárásnak átadni, ami módosítja a tartalmát (illetve szabad, de akkor elő kell készíteni rá). A másik, hogy olyan eljárásanak sem szabad átadni, ami később akarja azt felhasználni, amikorra esetleg a tartalma már nem elérhető. A harmadik meg az, hogy a process falain kívül nem érvényes, tehát nem szabad átadni olyan eljárásnak, ami másik processt hívhat. Utóbbi kettőre példa a PostMessage.
Típus nélküli paraméter (var Buf) vagy (const Buf)
A típus nélküli paraméterek nem tudják, hogy az átadott értéket hogy kell használni. Nekik a string egy négy bájtos adat. Előfordulási helyeik többek között:
A windows eseménynaplójába bárki írhat diagnosztikailag fontos eseményeket. Az eseményeknek forrása (a naplózó program) és azon belül kódja van, ami egy szám. Azonban a kódok szöveges feloldása is meg van oldva, hogy a nézegetőprogramban könnyebb legyen a dolgunk. Ehhez be lehet regisztrálni egy dll vagy exe fájlt, annak a resource-ában keresi majd a messages szekció alatt a szöveget, ami az egyes kódokhoz tartozik. Ennek az az értelme, hogy a resource nyelvfüggő lehet, tehát az eseménynapló többnyelvűsítése ezzel meg van oldva.
A szöveg tartalmazhat %1, %2, ... paramétereket. A Microsoft gondosan a lelkünkre köti, hogy paraméternek ne használjunk szavakat, mondatrészeket, mivel a paramétereket nem fordítja le senki. Ha az üzenet szövegét így akarjuk felépíteni:
22 "Error %1 configuration file"
hogy majd aztán "reading" illetve "writing" paraméterekkel helyettesítsük, akkor egy esetleges magyarítás után ezt kapjuk:
"Hiba a konfigurációs fájl writing közben"
Minderről bővebb információ itt.
A Sybase fejlesztői minden bizonnyal nem tudták hiánytalanul befogadni ezt a tudást, ugyanis az alábbi eseményleírásokat definiálták az egyik termékükhöz (a teljes listát közlöm):
1 "%1: %2"
2 "%1"
Korábbi ígéretünkhöz híven bemutatjuk az osztott memória alapú ICP technikát. A példa abszolút erőltetett lesz, de arra jó, hogy magát a módszert megfigyeljük. Elkészítjük a prímszámtesztelő program egy olyan változatát, ami egyáltalán nem foglalkozik az üzenetekkel, viszont mindent, amit csinál, nyilvánossá tesz egy osztott memórián. Készítünk továbbá egy másik programot, ami ezt rendszeresen kiolvassa, és megjeleníti.
Hogy a két fél biztosan egyetértsen az osztott memória struktúrájában, valamint praktikus okokból is, az osztott memória implementálását egy külön unitként írjuk meg, amit mindkét program használni fog. Ez a unit a status.pas. Kezdésnek definiáljuk magát az adattartalmat.
type
TPrimeStatus = integer;
const
psUndefined = 0;
psNew = 1;
psError = 2;
psWorking = 3;
psIsPrime = 4;
psIsNotPrime = 5;
psExited = 6;
type
TSharedData = packed record
Status: TPrimeStatus;
Number: Int64;
LastDivisor: Int64;
MaxDivisor: Int64;
end;
Fontos a packed kulcsszó, hogy a fordító verziójától és egyéb tényezőktől ne függjön az adatmezők tényleges helye. Ha nem használnánk, a fordító nem használt helyekkel úgy egészítené ki a rekordot, ahogy azt a jelenlegi hardverek szeretni szokták, például 32 bites érték 4-gyel osztható címre kerüljön. Amint látjuk, a számítás során használt valamennyi érték az osztott memóriára fog kerülni. A Status mező lehetne enumáricó is, de sajnos olyan WinAPI függvényeknek fogjuk átadni, amik integer értéket várnak, és a typecast otrombaságát célszerű elkerülni, ha lehet. Az osztott memóriát a SharedData globális változón keresztül érjük el.
var SharedData: ^TSharedData;
A unit saját magát inicializálja a program indulásakor. Egy igazi programban esetleg jobban kézivezérelhető megoldást választunk.
var
hMapping: THandle;
initialization
hMapping := CreateFileMapping($FFFFFFFF, nil, PAGE_READWRITE, 0,
SizeOf(TPrimeStatus), 'PrimeGen_ShMem_3D68414E-6A3E-4CB7-B5AE-66A408EBFC2E');
if hMapping = INVALID_HANDLE_VALUE then
InitError := GetLastError
else begin
SharedData := MapViewOfFile(hMapping, FILE_MAP_WRITE, 0, 0, SizeOf(TPrimeStatus));
if SharedData = nil then
InitError := GetLastError;
end;
end.
A CreateFileMapping első paramétere a fájl handle lenne. Az FFFF FFFF érték azt jelzi, hogy a swap fájlból foglaljon nekünk egy darabot, nem kívánunk valódi fájlt használni. Az utolsó paraméter a név. A név megadására különös figyelmet kell fordítani, nehogy véletlenül olyan nevet használjunk, amit más is. Ezért a "ShMem" illetve "MyMapping" nem szerencsés nevek. A legbiztosabb, ha egy GUID értéket illesztünk a név végére, ezzel gyakorlatilag kizárjuk a véletlen egybeesést. Delphiben könnyen generálhatunk GUID értéket, csak meg kell nyomni a Shift-Ctrl-G billentyűkombinációt, és a szövegbe illesztődik egy GUID.
Miután az inicializáció lefutott, az osztott memória elérhető az alábbi módon.
SharedData.LastDivisor := SharedData.LastDivisor + 1;
Mivel az osztott memória ugyaolyan memória, mint amiben a lokális változók vagy a dinamikusan foglalt adatok (class, stb) vannak, nyugodtan használhatjuk is ugyanolyan módon, nincs semmilyen teljesítménybeli vonzata. Ezért a program nem csak publikálás céljára fogja az osztott memóriát használni, hanem ez lesz a tényleges munkaterület. Ez ám az igazi nyíltság!
Eddig egyszerű volt, most jön a érdekesebb része. Ki kell találnunk valami protokolt, amivel biztosítjuk, hogy a teljesen konkurrens elérés nem fogja összezavarni a programjainkat. Minden egyidőben zajlik ugyanis, tehát elképzelhető, hogy a monitorprogram éppen akkor akar majd olvasni, amikor mi az írási művelet kellős közepén vagyunk. A jelen esetben szerencsénk van, mert szinte semmi védelemre nem lesz szükség. De azért csak vegyük sorra.
A status mező egy felsorolás. Nem szeretnénk, ha esetleg nem valós érték keletkezhetne benne akár időlegesen is. Ezért a status mezőt írását védetté tesszük az InterlockedExchange API függvény segítségével. Az InterlockedExchange úgy változtatja meg egy 32 bites változó értékét, hogy eközben más ahhoz nem férhet hozzá, amennyiben szintén Interlocked függvényeket használ. A kiolvasás egy kicsit trükkös, mivel olyan nincs, hogy InterlockedRead. Helyette az InterlockedCompareExchange használható, ami megfelelő paraméterezés mellett semmi mást nem csinál, csak visszaadja a jelenlegi értéket. A beírás tehát így néz ki:
InterlockedExchange(SharedData.Status, psIsPrime);
A kiolvasás pedig:
case InterlockedCompareExchange(SharedData.Status, 0, 0) of ...
Egész pontosan csak ez lenne, ha valami megtévedt elme nem definiálta volna InterlockedCompareExchange függvény paramétereit pointer típusúnak. Így rákényszerülünk némi stílusrombolásra:
case TPrimeStatus(InterlockedCompareExchange(PPointer(@SharedData.Status)^,
pointer(psUndefined), pointer(psUndefined))) of
Problémás a 64 bites értékek védelme. Az Interlocked függvényeknek van 64 bites változata, azaz csak lesz, mégpedig a Vistán. Addig is egy másik lehetséges út volna, ha a módosítást és az olvasást egy Mutex védené. A Mutex megszerzése azonban lassú, így semmiképpen nem célszerű minden egyes írási művelet előtt elvégezni, tehát adagokban kellene a számítást végezni. Egy másik opció lenne egy viszonylag új módszer, a zárolásmentes (lock free) hozzáférés, ami röviden annyiból áll, hogy az adatelérést mindenféle védelem nélkül megvalósítjuk, majd ellenőrizzük, hogy időközben elrontották-e azt. Ha elrontották, kezdjük a műveletet előről. Ha nem rontották el, akkor pedig a művelet sikerült. Ezt persze könnyebb mondani, mint megcsinálni, de megcsinálni sem lehetetlen.
Ami ellen védekezünk, az tehát például az alábbi. Legyen egy Int64 értékünk, amit egyel szeretnénk növelni. Legyen továbbá a pillanatnyi érték FFFF FFFF. Ennek növelése időben így néz ki:
| felső 32 bit | alsó 32 bit | |
| kezdeti állapot | 0000 0000 | FFFF FFFF |
| növeljük egyel, balról kezdve | 0000 0000 | 0000 0000 |
| és a maradékot átvisszük | 0000 0001 | 0000 0000 |
Ha pont a második sornak megfelelő állapotban kapjuk el az értéket, akkor nullát fogunk kapni. De bekövetkezhet ennél még furcsább eredmény is. Ha például időben először az alsó 32 bitet olvassuk ki, majd kicsit később a felsőt, előfordulhat, hogy az olvasott érték 1 FFFF FFFF, tehát a régi és az új érték kevercse. Ilyen kevert értékekre kell felkészülnünk az monitorozó programunkban.
Mi azonban mégsem fogunk védekezni! Mégpedig azért nem, mert az olvasó oldalon semmi komolyra nem használjuk az olvasott értékeket, csak megjelenítjük a felhasználó számára. Azon ritka esetekben, amikor összekeveredik a kezünk alatt az előző és a jelenlegi szám, a kiolvasott érték valótlan lesz, amit azonban éppolyan stabilan át tudunk alakítani szöveggé, és a képernyőn megjeleníteni. Tehát a veszély csak annyi, hogy bizonyos esetekben inkorrekt szám jelenhet meg a kijelzőn. Ez számunkra most megfizethető ár.
Egy veszély azonban leselkedik ránk. Mivel szándékunkban áll százalékban is megjeleníteni az elvégzett munkát, osztani kell a maximális osztóval, ami elvileg soha nem lehet nulla, de a fentiek alapján nem kizárható, hogy mégis nullát olvassunk ki. Ezért a nullával osztás elkerülése végett a százalékot csak akkor számoljuk újra, ha a MaxDivisor nem nulla. Ehhez másolatot kell készítenünk az értékről, mert egymás után többször kiolvasva más és más értéket kaphatunk.
MaxDiv := SharedData.MaxDivisor;
if MaxDiv <> 0 then
PercentLabel.Caption :=
FormatFloat('0%', SharedData.LastDivisor / MaxDiv * 100);
Az összes többi értéket vadul kiolvassuk.
NumberEdit.Text := IntToStr(SharedData.Number); TestingEdit.Text := IntToStr(SharedData.LastDivisor);
Mindezt egy közönséges Timer vezérli. Több szót nem érdemel, forrás itt.
Folytatjuk a programok közötti párbeszéd lehetőségeiről szóló értekezést. Ezúttal három ritkábban használt, de a maga módján igen hatékony módszert ismertetünk.
COM / DCOM
Viszonylag bonyolult módszer, de adott esetben mégis erre eshet a választás. A bonyolultság onnan adódik, COM szervert kell írni hozzá, ami nem első féléves anyag. Továbbá a COM egyszerűnek van ugyan szánva, de valójában elég sok csapdát rejt, ha alapos tudás nélkül akarjuk használni.
A COM integráns része az úgynezevett marshalling, ami nem más, mint az adat eljuttatása egyik alkalmazásból a másikba. Ehhez type library, vagy sajátkezűleg kivitelezett marshalling kell. Előbbi regisztrációs és teljesítményvonzattal jár, utóbbi többletmunkával.
A megvalósítás például lehet például olyan, hogy az egyik program implementál egy COM osztályt, és regisztrálja magát mint local server. A másik program létrehoz egy példányt az osztályból, és annak megfelelő interface-ét lekéri. Annak eljárásait hívva adatokat cserélhet a másik programban levő osztállyal.
Előnyei:
anonymous pipe
E ponton eredetileg az alábbi hülyeség állt. Sajnos igen hosszú ideig, a javítás dátuma 2007.10.26. Bocsánat.
Az anonymous pipe egy egyszerű cső, amibe az egyik oldalon beöntjük az adatokat, a másik oldalon pedig ki lehet venni. Egy windows pipe valójában két csőből áll, egyik illetve másik irányba menőből. Mondhatjuk úgy is, hogy a vonal full duplex. Pipe-ot CreatePipe API hívással állíthatunk elő. A CreatePipe készít nekünk két handle-t, az egyik a bejövő, a másik a kimenő irányú csőé. Fájlműveletekkel (WriteFile, ReadFile) tudunk adatot írni vagy olvasni.
Ehelyett a valóság az, hogy a CreatePipe egy egyirányű csövet készít, és annak író, illetve olvasó végét adja vissza. Ha full duplex adatátvitelt akarunk, akkor két csövet kell készíteni, egyet odafele, egyet visszafele.
Trükkös kérdés az anomymous pipe használatánál, hogy hogyan juttatjuk el a handle-t a másik programba. Tudniillik minden programnak saját handle készlete van, csak a window handle olyan speciális, hogy minden program számára elérhető. A pipe handle az alkalmazás sajátja, nem adhatja simán át. De még ha átadhatná is, akkor sem világos, hogy hogyan tegyük ezt. Az első problémára megoldás lehet a DuplicateHandle API hívás, ami pont arra szolgál, hogy egy handle-t átadhassunk egy másik programnak. Illetve tulajdonképpen csak előállít egy handle-t, ami a másik programban használható, de az odajuttatásról magunknak kell gondoskodni. Az eljuttatást megoldhatjuk például üzenetben. Ez persze nem éppen egy elegáns megoldás, hiszen ha már úgyis üzenünk, megoldhatnánk az adatátvitelt is üzenetek segítségével.
Van viszont egy nagyon kellemes felhasználása a pipe-oknak. Ez pedig a saját magunk által indított programokkal (child process) való kapcsolattartás. Amikor CreateProcess hívással egy új programot indítunk el, módunkban áll az összes saját handle-t elérhetővé tenni az új programnak (bInheritHandles), tehát a DuplicateHandle nem is kell. A handle átadása pedig egyszerű command line paraméterként megvalósulhat. Különleges értelmet kap a pipe console alkalmazások esetén, ahol is módunkban áll a program standard inputját és outputját egy egy pipe-ba irányítani (STARTUPINFO.hStdXXXX), és azon keresztül irányítani azt. Ami azt illeti, a pipe kimondottan ilyen esetekre lett kitalálva.
Előnyei:
named pipe
Nevében ez is pipe, de egy egészen más állatfajta, mint a névtelen párja. Ugyan ez is cső, és ez is adatok full duplex továbbítását oldja meg, de nagyságrenddel komolyabb funkcionalitást lehet vele megvalósítani. Elsőként kiemelendő, hogy ez a pipe névvel rendelkezik, aminek segítségével a programok könnyen felvehetik a kapcsolatot. Másodszor a szerver oldal egyidőben több klienssel is tarthatja a kapcsolatot ugyanazon pipe-on keresztül (hasonlóan a TCPIP server socket-hez). És nem utolsósorban a named pipe képes távoli gépeket is összekötni. Bonyolult szerveroldali, és nem túl egyszerű kliens oldali megvalósíthatósága miatt ritkán használják, többnyire komoly célra, például adatbázisszerver és -kliens közötti kommunikációra.
Előnyei:
A windows programok különálló címterekben dolgoznak. Mindegyik azt hiheti, hogy egyedül rendelkezik a számítógép felett, noha lehetőségei korlátozottak, de amihez hozzáfér, az csak az övé. Ez kellemes állapot, mindaddig, amig nem éppen az a feladat, hogy két program kommunikáljon egymással. Szerencsére az alkalmazások közötti falon számos átjárási lehetőség van, még több is, mint amennyit itt felsorolunk. Ezeket hívják IPC-nek (Interprocess Communication).
Memory Mapped File - Shared Memory - osztott memória
Volt már szó róla a memory mapped file kapcsán. Tulajdonképpen egy közös memória jön létre, amit bármely program olvashat vagy írhat. A problémák is ebből adódnak: alaposan meg kell szervezni, hogy mikor ki mit csinál, különben vészes összekeveredés lehet. Gondoljunk csak arra, hogy ha az egyik program éppen adatot ír a memóriába, az adat félkész, amikor eljön a taszkváltás ideje. A másik program ezt kiolvassa, és hogy mit fog csinálni, ha meglátja, azt csak a programozó tudhatja, de leginkább ő sem. Az összhang megvalósítására egyrészt a józan eszünket használhatjuk, másrészt rendelkezésünkre áll direkt erre a célra rendszeresített eszköztár, név szerint a Mutex és az Event (nem a delphi esemény, emezt CreateEvent API hívással lehet létrehozni).
Az osztott memória tipikus akkor, ha egy monitor-program számára tesszük nyilvánossá pl. egy service alkalmazás tevékenységének részleteit. De természetesen adatforgalmat is lehet rajta bonyolítani, némi plusz munkával.
Előnyei:
Üzenet, WM_COPYDATA
Az ablakok, függetlenül attól, hogy melyik program tulajdonolja őket, mindenki számára láthatók. És bár nem mindent csinálhatunk az idegen ablakokkal, üzenetet tudunk nekik küldeni. A windowsban külön üzenettartomány van fenntartva az ilyen nem-rendszer üzeneteknek, ez a WM_APP - WM_APP+$3FFF tartomány, amit bármire használhatunk. Vigyázni kell azonban, mert ilyen üzeneteket sohasem szabad olyan ablaknak elküldeni, amit nem mi készítettünk, csak ha a készítő felhatalmazott rá. Mivel a tartomány szabad, elképzelhető, hogy ugyanarra az üzenetre (mondjuk WM_APP + 2) két ablak egészen másképp reagál. Csak oda küldjünk WM_APP üzeneteket, ahova az szánva lett. Van még egy módszer, amivel az üzenetek kódjainak egyediségét biztosíthatjuk: ez a RegisterWindowMessage.
Két szokványos megoldás van arra, hogy a felek egymásra találjanak. Az egyikben a kezdeményező fél egy szórt üzenetet küld el (HWND_BROADCAST), amit minden (felső szintű) ablak megkap. Ez az üzenettípus delphiben az Application.OnMessage eseménnyel, vagy AllocateHWnd-vel készített ablakkal kapható el. Ebben az üzenetben a kezdeményező elküldi a saját ablakának handle-jét. Ez az ablak lehet bármely form (Handle property), vagy egy AllocateHWnd-vel készített dedikált üzenőablak. A megszólított fél az üzenetre reagál, és visszaküldi a megadott ablaknak a saját handle-jét. Ettől kezdve a kommunikáció direktben megy.
A másik módszerben a passzív fél egy bizonyos feliratú vagy osztálynevű ablakot hoz létre, és a kezdeményező fél azt keresi meg FindWindow hívással. Ezután célzottan ennek az ablaknak küldi a kezdeményező üzenetet, egyébként minden úgy történik, mint az előbb.
Vigyázat! Szórt üzenetben sohasem küldünk WM_APP típusú üzenetet, hiszen nem tudjuk, hogy a többi ablak mivel reagál arra (lemez formázása, adatbázis törlése, stb.). Ilyenkor regisztrált üzenetet kell használni. Ha formot akarunk üzenet fogadására használni, a message kulcsszóval kell üzenetkezelő eljárást definiálni. Ez azonban nem működik regisztrált üzeneteknél. Emiatt is, és általában is szerencsés az AllocateHWnd használata.
Egy üzenetben két integer nagyságú érték vihető át. További korlátozott információtartalma lehet magának az üzenetnek is, azaz használhatunk különböző funkciókra más és más üzenetet, de ezt nem érdemes nagyon szaporítani. Ha ennél több adatot kell átvinni, arra szolgál egy speciális üzenet, a WM_COPYDATA. Különlegessége az, hogy az egyik paramétere egy pointer (nem egészen, de most egyszerüsítünk), ami egy memóriaterületre mutat. A windows gondoskodik arról, hogy a memóriaterületet a másik alkalmazásba átmásolja az üzenet feldolgozásának idejére. WM_COPYDATA üzenetet soha nem küldünk szórt üzenetben, hasonlóan a WM_APP üzenetekhez!
Előnyei:
nem sok van, én hirtelen egyet sem tudnék mondani. ennek ellenére igen elterjedt módszer.
TCPIP
A megoldás kézenfekvő, TCPIP kapcsolatot nem csak távoli gépek között, hanem egy gépen belül is használhatunk. Az egyik program nyit egy server socketet, a másik pedig egy client sockettel kapcsolódik hozzá. Erről bővebbet nem is érdemes mondani.
Előnyei:
Legközelebbre marad további három módszer: a COM, a named pipe és az anonymous pipe.
Ha az ember programozó, és még rigolyás is, gyakran lát nem éppen elmés megoldásokat programokban. Ezeket kipellengérezzük, és ha kedvet érzünk, esetleg közlünk egy vagy több helyes megfejtést is.
RSS
balinto 2006