Delphi minus VCL

21.09.2007 02:32 | blackhole_srnec

Niekedy je pouzitie VCL neziaduce pripadne nemozne preto trochu skusim priblizit pouzitie Delphi priamo s Win API. Programy budu pokial mozno kratke a male (aj po kompilacii).
O Delphi s pouzitim VCL / CLX uz pojednava clanok od Lukas.229 (za co mu dakujem lebo ma nevedomky nakopol aby som sa konecne rozhybal).

Ak budete chciet skusat kompilovat tak odporucam nainstalovat:

 a) nejake starsie Delphi (pouzivam 6) staci minimalna instalacia
 b) Delphi10Lite - pohladat na nete
 c) Free Pascal (fpc)
 d) Dev Pascal

Este na uvod, budem sa snazit pouzivat nazorne ukazky ktore budu nie vzdy "najcistejsie" z programatorskeho hladiska, ide o nazornost. Ako doplnok odporucam mat otvoreny nejaky pokec k Win API (msdn).

Zacneme takou blbinou co som davno pisal na postrasenie bfu. Nebudeme vytvarat zatial ziadne okna iba pouzijeme Notepad ako miesto na pisanie (class EDIT). Pytajte sa na co chcete, ked bude treba vysvetlim aj co je handle :-)

Tu je kod:

program wintask;
{$D TASK Helper}
//{$MODE DELPHI} //povolit pre fpc
{$APPTYPE GUI}
uses windows;
const crlf = #13#10;
var
    S  : String;
    cdx,
    spis,
    tdw,
    sbh,
    tmh : dword;
procedure sendd;
begin
    move(s,tdw,4);
    sendmessage(sbh, $000c, 0, tdw); //WM_SETTEXT
end;
procedure bychar(vst : string);
var xsp : integer;
begin
    for xsp := 1 to length(vst)do
    begin
      s := s + vst[xsp];
      sendd;
      sleep(30); //bolo 100
      if vst[xsp] = #10 then messagebeep(0);
    end;
end;
begin
//    sleep(240000); //na zaciatku pocka, nech je trocha srandy
    SystemParametersInfo(97, 1, @spis, 0); //nastav akoze bezi screen saver, nefunguje pod NT (zakaze CTRL+ALT+DEL)
    if paramstr(1) = '' then winexec('NOTEPAD.EXE',0)
    else winexec(pchar(paramstr(1)),0);
    sleep(250);
    tmh := findwindow('Notepad',nil);
    sbh := GetWindow(tmh, 5);  //GW_CHILD
    enablewindow(tmh,false);
    setwindowtext(tmh,'Important Msg 4U');
    setwindowpos(tmh,hwnd_topmost,100,100,500,300,swp_showwindow);
    bychar('**********************************************************' + crlf + crlf);
    sleep(1000);
    bychar('Hallo L''Uzer' + crlf + crlf);
    bychar('I am now here. For next days I will live in your computer.' + crlf);
    bychar('Do not be afraid, I am YOUR friend! Don''t panic!.' + crlf);
    bychar('Everything is under control. Control. c''ntrol. cool. trol.' + crlf + crlf);
    bychar('                                              Au revoir...' + crlf + crlf);
    for cdx := 1 to 58 do
    begin
        s := s + '*';
        sendd;
        sleep(50);
    end;
    enablewindow(tmh,true);
//    sendmessage(tmh, $0010, 0, 0); //wm_close
    SystemParametersInfo(97, 0, @spis, 0); //vid hore
end.

Preskocte uvod a podme na hlavne telo:
winexec('NOTEPAD.EXE',0) spustime notepad starou funkciou
sleep(250); pockame aby mal cas vytvorit okno (250ms)
tmh := findwindow('Notepad',nil); pohladame okno (class "Notepad" o caption sa nestarame aby to bolo language neutral) a ulozime handle do premenej tmh
sbh := GetWindow(tmh, 5); pohladame potomka okna tmh (EDIT - nase miesto na hranie) a handle dame do premenej sbh
enablewindow(tmh,false); zakazeme focus parrent okna (aj vsetkych child)
setwindowtext(tmh,'Important Msg 4U');
nastavime caption aplikacie (viditelne v taskbare)
setwindowpos(tmh,hwnd_topmost,100,100,500,300,swp_showwindow);
presunieme okno Notepad-u, mmenime velkost, nastavime topmost

teraz si popiseme co robi procedura sendd;

procedure sendd;
begin
    move(s,tdw,4);
    sendmessage(sbh, $000c, 0, tdw); //WM_SETTEXT
end;

move(s,tdw,4); do premenej tdw ulozime adresu retazca s
sendmessage(sbh, $000c, 0, tdw); posleme oknu sbh (EDIT) spravu WM_SETTEXT (nastav oblast pamate tdw ako text okna)

riadok sendmessage(tmh, $0010, 0, 0); zatvori okno (kedze sme ziadny subor neotvarali a nepisali pomocou klavesnice tak Notepad nepinda a netreba pouzit "hrubu silu"

co sa deje v bychar by malo byt kazdemu jasne.

Tak poskusajte a napiste co treba vysvetlit, nabuduce pojdeme dalej....

tu je poll ako chcete aby serial pokracoval , tak hlasujte

    • Re: Delphi minus VCL 25.09.2007 | 11:02
      ziman   Návštevník

      Zaujímavý článok, ale mám pár formálnych výhrad/návrhov:
      1) Používaj zmysluplné názvy premenných a funkcií, najmä ak ten kód píšeš pre druhých ľudí. Iteračné premenné sa volávajú i, nie xsp. Funkciu bychar by som premenoval napríklad na addStringToNotepad, funkciu sendd na setNotepadText s jedným parametrom text typu String, atď.

      Potrebuješ vysvetľovať, a teda potrebuješ čitateľné a na pohľad pochopiteľné názvy (imho je to rovnako dôležité aj v normálnom kóde); z pohľadu na sendd, cdx, tdw, sbh, tmh nemám ani poňatia, čo to je.

      2) Konverziu zo Stringu na pointer/DWORD by si imho mal pouzivat nativnu Delphiovsku (tj. argument := DWORD(PChar(text));), tá s move() len kopíruje bajty z pamäti. AFAIK sa v dokumentácii nikde nepíše, že prvé štyri bajty Stringu sú pointrom na daný reťazec (zatiaľ čo PChar() je zdokumentovaná konverzia). Ja viem, že to funguje, ale je to imho prasačina a mali by sme uprednostňovať čisté varianty, ak existujú :) Okrem toho takáto konverzia je oveľa čitateľnejšia než move(), pretože na tom vidno, že to je konverzia (a sám vravíš, že o názornosť ti ide predovšetkým).

      Nenechaj sa ale odradiť, toto ber ako konštruktívnu kritiku, rád si prečítam ďalší diel.
      --
      On neni mrtev, on jenom spi!

      • Re: Delphi minus VCL 25.09.2007 | 11:37
        Avatar vid   Používateľ

        AFAIK sa v dokumentácii nikde nepíše, že prvé štyri bajty Stringu sú pointrom na daný reťazec (zatiaľ čo PChar() je zdokumentovaná konverzia). Ja viem, že to funguje, ale je to imho prasačina a mali by sme uprednostňovať čisté varianty, ak existujú

        zaujimave, ja som vzdy myslel ze delphi stringy obsahuju len dword v ktorom je dlzka stringu, a za tym samotne znaky. Ako to je?

        • Re: Delphi minus VCL 25.09.2007 | 12:35
          ziman   Návštevník

          Klasické Pascalovské stringy boli 256-bajtové polia, ktoré v nultom bajte obsahovali dĺžku reťazca (btw preto sa indexovali od jednotky) a mohli mať najviac 255 znakov. To je aj dôvod, prečo mohli, na rozdiel od C stringov v pohode obsahovať nulový znak.

          V Delphi to je AFAIK spravené tak, že String je pointer niekam do pamäte, pričom na pozícii zaciatok := PByte(retazec) je prvý znak reťazca, ale alokovaná pamäť má o štyri bajty viac, pretože na pozícii zaciatok - 4 je uložená 32bitová dĺžka reťazca (to však Delphi vie a podľa toho aj string dealokuje).

          retazec := 'HELLO_WORLD';
          { Length(retazec) = xxxx (bin) }
          xxxxHELLO_WORLD
              ^-- Pointer(retazec);

          To umožňuje mať v Stringu ľubovoľné binárne dáta a tiež aj takéto veci:

          {
              buffer: String;
              f: File;
              toRead, actuallyRead, actuallyWritten: Integer;
              socket: TSocket; { fiktívna trieda }
          }
          begin
            toRead := socket.availableBytes;
            SetLength(buffer, toRead);
            { procedure TSocket.read(var destination; bytes: Integer); }
            actuallyRead := socket.read(Pointer(buffer)^, toRead);
            BlockWrite(f, Pointer(buffer)^, actuallyRead, actuallyWritten);
            WriteLn('Read ', actuallyRead, ' bytes; written ', actuallyWritten, ' bytes.');
          end.

          Teda pracovať so Stringom ako s buffrom ľubovoľných dát s veľmi pohodlným automatickým pamäťovým manažmentom.

          Toto, na rozdiel od move(string, pointer, 4), až taká prasačina nie je (trochu áno), pretože sa jednak takéto kódy vyskytujú aj v helpe, jednak sa nespoliehajú na žiadne nezdokumentované predpoklady (String má presne taký istý binárny layout v pamäti ako Pointer, pointer ma 4 bajty, atď.), len na to, že String po natívnej Delphiovskej konverzii za Pointer (ktorá by nebola zavedená, ak by nemala zmysel; nie je samozrejmé, že String sa dá skonvertovať na Pointer) ukazuje na dáta, ktoré reťazec obsahuje.
          --
          On neni mrtev, on jenom spi!

          • Re: Delphi minus VCL 25.09.2007 | 14:08
            Avatar vid   Používateľ

            a ako je to s unicode stringami? a co ked chcem staticky string (nie alokovany), je to mozne? kam sa potom uklada dlzka buffra (max. velkost stringu)?

            • Re: Delphi minus VCL 25.09.2007 | 14:47
              ziman   Návštevník

              Ako je to s Unicode, neviem, to som nikdy neriešil (Delphi som opustil asi pred dvoma rokmi a vtedy všetko -- aspoň v mojom okruhu vnímania -- bolo v 8-bitovej cp1250).

              Statický string... no to je klasický String (dynamický by bol ^String). Akurát Delphi robí za teba v pozadí manažment pamäte, takže ťa vôbec nezaujíma, čo sa deje a pracuješ s tým ako s normálnym statickým stringom (aj keď áno, jeho dáta sú uložené na heape).

              Ak chceš 100% statický string (aby aj dáta mal uložené na stacku), tak použi ShortString (to je premenovaný old-fashion pascalovský string) alebo typ v štýle String[123];. Dĺžka stringu sa potom ukladá do nultého bajtu (čo kladie limit na dĺžku takýchto stringov 255 znakov). Ale takéto stringy nemajú oproti novým Stringom žiadne výhody.
              --
              On neni mrtev, on jenom spi!

            • unicode 26.09.2007 | 08:51
              Avatar blackhole_srnec   Používateľ

              Unicode je podporovane, potom sa stringy volaju double byte string, cize kazdy znak zabera 16 bitov, praca je skoro rovnaka ako s klasickym stringom. Neprijemna vec je ze vsetky vcl tcaption a ttext su ako single byte a musis robit konverziu, naviac nemozes mat v jednom nazve napr. cinstinu a azbuku. Velkost alokovaneho miesta potom strazi runtime (alebo aj nie, podla nastavenia kompilatora)

              +++
      • Re: Delphi minus VCL 25.09.2007 | 22:36
        Avatar blackhole_srnec   Používateľ

        Dik za comment, trochu ozrejmim ako sa veci maju. Tento clanok je viac-menej sonda pre mna ako ludia budu reagovat (ci maju vobec zaujem) na programovanie na nizsej urovni. Mam v umysle pisat aj o nestandartnych rieseniach pripadne nedokumentovanych resp. "nepohodlnych funkciach" (co MS zamerne schovava alebo priamo rusi) a potrebujem presondovat teren. (aby som vedel ci staci napisat "Handle okna" alebo aj vysvetlit ze to nieje ta vec na taske) Zareagoval si prvy a zatial jediny na urovni. Nech ta nematie ze som na to vybral pascal, ved vznikol ako skolsky jazyk :-)
        S commentom ohladom nazvov plne suhlasim, mea culpa.
        Ako priklad som vyhrabal stary zdrojak a neupravoval som ho. Vznikol este za cias delphi 1 alebo 2 a vtedy woodoo s pretypovanim moc nechodilo a neviem ci podobna vec ide v fpc. Sice to nazyvas prasacinou ale z pohladu asm je to najcistejsie riesenie :-) A vec je zdokumentovana, mozno nie v priamo v helpe k delphi ale urcite je. Nepozivam 4 bajty zo stringu, to len tak vyzera, pouzivam interny pointer pascalovskeho runtime (toto vyuziva aj garbage collector). Okrem toho je tam este dost zavazna vec, cakam kto sa ozve.

        +++
        • Re: Delphi minus VCL 26.09.2007 | 00:59
          ziman   Návštevník

          Skúsim ja :)

          Program môže zjavne brať ako parameter príkazového riadku aplikáciu, ktorú má spustiť, tá teda môže byť ľubovoľná, ale vo volaní FindWindow má natvrdo hardcodovaný 'Notepad'. (Aj keď, pravdupovediac, ako až taká funkčne kritická časť sa mi to nevidí a WinAPI som nevidel už cez dva roky, aby som hľadal chyby inde.)

          Btw, vo fpc Delphi kód behá bez akýchkoľvek problémov, Free Pascal Compiler plne podporuje Delphi dialekt opšnou -Sg.

          "Nepozivam 4 bajty zo stringu, to len tak vyzera,"
          Nemyslel som to tak, že 4B zo stringu (napr. 'Hell' z 'Hello World'), ale 4B z miesta v pamäti, ktoré je alokované pre Delphiovský typ String (tj. v reáli tam je "náhodou" pointer na samotný obsah stringu), a teda som chcel poukázať na to, že aj keď riadky 2 a 3 v nasledujúcom kúsku kódu robia to isté, dvojka sa mi vidí oveľa čistejšia, pretože jej syntax vystihuje sémantiku (prinajmenšom, ak tvrdíš, že je to zdokumentované).

          {
            ptr: PChar;
            str: String;
          }
          {1} str := 'Hello world';
          {2} ptr := PChar(str);
          {3} move(str, ptr, 4);

          Ale samozrejme, možno máš na to svoje dôvody (ak chceš plynulo prejsť na mov ecx 4, rep movsb :) ).

          Určite pokračuj, som zvedavý, čo máš ešte pre nás v rukáve. :)
          --
          On neni mrtev, on jenom spi!

          • Re: Delphi minus VCL 27.09.2007 | 16:36
            Avatar blackhole_srnec   Používateľ

            ee parameter on command line to nieje, odflakol som tam #0 na konci pascalovskeho stringu, takto by mohlo dojst k buffer overflow hlavne pri pouziti pointer operacii, tebou navrhovane riesene {2} je bezpecnejsie z tohto hladiska aj ked o dost pomalsie (pocet instrukcii) {co dnes programatorov netrapi :-( }

            +++
            • Re: Delphi minus VCL 28.09.2007 | 12:56
              ziman   Návštevník

              1. Práve preto, lebo nepoužívaš veci na to, na čo sú určené. Na konverziu zo Stringu na (char *) sa používa nie move() ale PChar(), ktorý ti všetky potrebné veci zaručí (aj tú nulu na konci stringu).

              2. Čím je PChar() pomalší od move()? String sa pri konverzii na PChar nemôže kopírovať, pretože PChar nie je manažovaný, a teda by ho nemal kto dealokovať. AFAIK sa to rieši tak, že String je ukončený neviditeľným znakom #0, pričom pri konverzii na PChar sa proste iba skopíruje adresa -- String je zároveň PCharom. Ak si napíšeš program, ktorý ti vypíše hodnotu výrazu Pointer(retazec) a hodnotu Pointer(PChar(retazec)), zistíš, že sú také isté.

              Keďže pointer má šírku slova, konverzia Stringu na PChar je takto len jedna mov inštrukcia, zatiaľ čo move() je určená na prenos ľubovoľne veľkých dát, a teda je tam celá tá haluz s rep movs[bw] a aritmetikou, koľko to treba vlastne skopírovať, čiže práve ten move() by som videl na (niekoľkonásobne) väčší počet inštrukcií (call, celá tá réžia s ebp, esp atď, vyzdvihnutie argumentov zo stacku, naplnenie eds, esi, es, edi, ecx, rep movsb/w, leave, ret), zatiaľ čo konverzia na pchar je prosté priradenie, ktoré v kóde reprezentuje jednu (možno pár, záleží od kontextu) inline mov inštrukciu.

              Treba si ale uvedomiť, že počet inštrukcií naozaj nikoho netrápi. Dôležitý je dobrý návrh programu, jeho čitateľnosť a udržiavateľnosť, nie predčasná násilná optimalizácia. Kritické časti sa potom môžu odhaliť profilerom a zoptimalizovať hoci aj na asm úrovni, pričom netreba nijako podceňovať kompilátor (kompilátory dokážu vyoptimalizovať strašné veci). Optimalizácia je fajn. Ale písať celý kód by default s tým, že popieraš jeho sémantiku a píšeš ho rovno "optimalizovaný", je blbosť -- jednak nedávaš možnosť kompilátoru, aby ti kód optimalizoval (a ty to sotva spravíš lepšie, kompilátor má oveľa viac možností pri generovaní kódu než ty pri jeho písaní), jednak nedávaš možnosť programátorovi, čo príde k tvojmu kódu po pol roku, aby ho pochopil (či už sebe alebo niekomu inému).
              --
              On neni mrtev, on jenom spi!

              • Re: Delphi minus VCL 28.09.2007 | 14:07
                Avatar blackhole_srnec   Používateľ

                Teraz nebavime o programe co robis "naozaj" ale o veci na ukazku tu ide o demonstraciu riesenia aj s poukazanim na chybny postup, najvasci problem ked ides priamo do api je uvedomit si (a stale mysliet) na dva rozlicne pristupy, tuna string vs. pchar resp. pole znakov ukoncene nulou. Chcel som ukazat v com je hacik pri pouzivani stringov, teoreticky mozes cely pascalovsky program pisat bez ich pouzitia a pracovat iba s pchar alebo si priamo alokovat miesto. Pi konverzii pchar(string) vytvara kopiu dat v pamati, ak mas proceduru ktora meni obsah buffra tak sa ti naspat v stringu neprejavi a ty nemozes pchar konverziu pouzit priamo.

                Pointer je dword ale na pocte instrukcii to pri 32 bitovom procaku nic dramaticky nemeni.

                Co sa tyka optimalizcie kompileru to je na dlhu diskusiu, mozeme sa bavit o pouziti register variabiles a podobnych lahodkach ale nepocitam ze niekto bude v pascale pisat nieco na sposob johna pripadne generovat duhove tabulky :-)

                Zatial si stale jediny kto na urovni reaguje tak neviem cim by som mal pokracovat, pripadne nieco navhni ale tak aby to nebol serial pre dvoch, to sa mozme radsej stretnut na pive.

                +++
                • Re: Delphi minus VCL 29.09.2007 | 10:18
                  ziman   Návštevník

                  Prepáč, ale neodpustím si. :)

                  1. Konverzia na PChar nemôže vytvárať kópiu, pretože PChar (na rozdiel od Stringu) nie je manažovaný, je to fakt iba pointer. Kópia sa musí vytvoriť explicitne pomocou StrPCopy. Rieši sa to tak, že String okrem metadát pred payloadom (ref count, length) obsahuje ešte jeden NUL byte za payloadom, a tak po konverzii PChar proste ukazuje na to isté miesto, kam String a aj tak je to korektne ukončený PChar (pretože String je zároveň aj NUL-terminated).

                  Ku Delphi zdrojákom samozrejme prístup nemám, ale kukni si v zdrojákoch Free Pascal Compilera súbor rtl/inc/astrings.inc, kde sú nakodené práve AnsiStringy a tam to krásne vidno.

                  > ak mas proceduru ktora meni obsah buffra tak sa ti naspat v stringu neprejavi a ty nemozes pchar konverziu pouzit priamo.
                  Skús preložiť tento program:

                  var
                    S: String;
                    P: PChar;
                  begin
                    S := 'Hello world';
                    P := PChar(S);
                    WriteLn('Pointer(S) = ', Integer(Pointer(S)));
                    WriteLn('Pointer(P) = ', Integer(Pointer(P)));
                    P[2] := 'X';
                    WriteLn('S = ', S);
                    WriteLn('P = ', P);
                  end.

                  a hádaj, čo vypíše. Testované Kylixom (čo by mal sharovať väčšinu zdrojáka s Delphi) a, samozrejme, Free Pascalom. Ak sa nad tým zamyslíš, dôjdeš k záveru, že iné riešenie za daných vlastností jazyka ani nie je možné a tieto experimenty to aj potvrdzujú.

                  2. Je rozdiel odniesť škatuľu k susedovi vlastnoručne (inštrukcia mov) a volať si na to kuriérsku službu s nákladiakom (funkcia move).

                  Ja viem, kritizovať je ľahké (sám by som sa na takýto článok nezmohol, pretože vo WinAPI nerobím), myšlienka je pekná a zaujímavá, akurát prevedenie utrpelo možno viac kritiky, než by si zaslúžilo... proste daj niečo, čo si mal pripravené, myslím, že to tuná ľudia celkom čítajú, snáď sa aj nejakí ozvú.

                  Pôvodne som tu nechcel nejako flejmovať, ale už sa asi stalo... Povedal som si, že sa na to nevykašlem a snažím sa tu teraz osvetliť súvislosti; nie však za každú cenu, pretože občas sa mi stáva, že namiesto dotiahnutia diskusie do konca sa druhá strana na všetko vybodne bez toho, aby sa vyjasnilo, kde boli nezhody. A to za to zvyčajne nestojí.

                  Toto by som teda považoval za svoj posledný príspevok do diskusie, pretože na tom, kde sú ako bajty rozhodené... vlastne až tak nezáleží a treba sa venovať užitočnejším veciam. :)
                  --
                  On neni mrtev, on jenom spi!

                  • Re: Delphi minus VCL 29.09.2007 | 17:36
                    Avatar blackhole_srnec   Používateľ

                    kazdopadne dik za "oponenturu" ak to tak mozem nazvat, bola k veci a to je hlavne, tu nejde o dokazovanie svojej "pravdy", pripadne ma kontaktuj na icq (v profile), to je viac interactive :-)

                    +++
        • Re: Delphi minus VCL 26.09.2007 | 01:12
          Avatar vid   Používateľ

          Mam v umysle pisat aj o [...] nedokumentovanych resp. "nepohodlnych funkciach" (co MS zamerne schovava alebo priamo rusi)
          ktore to su? daj par prikladov...

          ozaj, ako je to v pascale s garbage collectorom? Da sa v Delphi robit aj klasicky bez neho?

          • zrusene funkcie.... 26.09.2007 | 11:38
            Avatar blackhole_srnec   Používateľ

            napriklad getprocessflags, ta mala velmi velku vypovedaciu schopnost o tom ako odflakli okna, vraca totiz ci je process 16 bitovy, ci bezi ako servisa / driver a ine uzitocnosti. Dokonca ju uvadzali aj ako priklad vo Wintop z SDK/DDK kitu. Wo wine ju najdes. A potom vsetko co bolo uvolne pod settlement programom.

            S garbage collectorom som si ne na 100% isty, ak vypnes vsetko v kompilery tak by ho nemal pouzivat, ale nemam overene.

            +++