De programmastructuur in Windows

johanok / at / gmail adres

Voor het ontwerp van een windowsprogramma (in dit voorbeeld 32 bits) heeft een programmeur de keuze uit meerdere programmeertalen. De verschillende programmeeromgevingen hebben elk hun eigen syntax. In een broncode zal iedereen op het zicht kunnen zien of het gaat om de C-omgeving (C, C++ of C#), basic (Visual studio VB, VBA, VBScript), Delphi/Pascal (Delphi, Lazarus) of assembler (NASM, MASM, GoASM of FASM).

De verschillen in syntax tussen deze groepen zijn erg duidelijk. Op forums van de ene taal staat soms commentaar op de andere taal. Ik wens mij niet in te laten met deze (hoofdzakelijk zinloze) discussies. Het gaat hier eerder om de overeenkomsten dan om de verschillen. Een programma dat voor Windows geschreven wordt heeft namelijk een vaste structuur die telkens terugkeert en duidelijk zichtbaar is, over de grenzen van de programmeeromgevingen heen.


Als ik snel een programma moet klaarhebben, gebruik ik een recente versie van Delphi, voluit, met de VCL. Dat maakt grotere exe's aan, maar ontwikkelt zeer snel en deze manier van werken is veelzijdig. Deze RAD-ontwikkelomgeving is niet te kloppen in snelheid qua ontwikkelen van applicaties.

Persoonlijk zou ik trager zijn in C++, C# of zelfs in VB. Maar dat is vermoedelijk bij iedereen verschillend volgens ervaring.

Als het programma eenvoudig is qua werking en structuur (en er is weinig tijdsdruk op de ontwikkeling), dan ga ik er eerst op af met "Delphi zonder VCL". Liefst een oudere versie als Delphi 3. Dit om een sneller en veel kleiner programma te bekomen. Als het om een of andere reden nog te groot is, dan herschrijf ik het voor TCC of voor FASM. In TCC win ik in grootte (veel kleiner uitvoerbaar programma), maar niet in snelheid, toch niet in vergelijking met Delphi 3. In FASM vooral als de code zeer snel moet lopen, of als de exe zeer klein moet zijn. Maar die situatie komt men zelden tegen.

Voor andere "tussendoorklusjes" is VBS (los op zichzelf) of javascript of VBscript (of beiden) in samenwerking met html soms handig. En uiteindelijk lijken al de programmeeromgevingen op elkaar. Dat beetje verschil in syntax is geen probleem. Om uit een datebase een deelbestand uit te trekken en naar een csv-bestand te kopiŽren, VBS is daat prima geschikt voor. Ik zou er niet over denken om dit in assembler te proberen. Veel te omslachtig.

Welke programmeertaal er ook gebruikt wordt, uiteindelijk wordt een groot aantal commando's door de compiler omgezet in Windows API-calls. Dat zijn de "application programming interfaces", door Microsoft gedocumenteerd op de MSDN-site. Een programmeertaal in een "visual omgeving" (Delphi in gewone modus, C++ of VB in Visual studio), toont die API-calls (en de structuur) meestal niet, de compiler verbergt dit voor de programmeur (en maakt dan meteen een veel grotere exe aan).
Om de gelijkenis aan te tonen bij het aanmaken van een windowsprogramma, wordt hier een minimaal windowsprogramma gemaakt. In dit geval een windowsprogramma dat enkel een venster toont. Uiteraard keren telkens dezelfde API commando's terug.

De foutenverwerking (als het registreren of aanmaken van een venster niet lukt bvb.) zijn uit deze voorbeelden uitgelaten, om het overzichtelijk te houden. En met zo weinig code is er weinig dat kan mislukken uiteraard.



Eerste voorbeeld: Delphi.

Volgend stukje code moet in een bestand "test.dpr".

program test;    // Bestand moet dus test.dpr heten, anders werkt het niet
                         //===========================================
                         //||        gebruikte bibliotheken         ||
                         //===========================================
uses
  Windows, Messages;
                         //===========================================
                         //||         declaratie variabelen         ||
                         //===========================================  
var
  WindowClass: TWndClass;
  Msg        : TMsg;     //===========================================
                         //||          berichtendispatching         ||
                         //===========================================
						 
function WindowMainProc(hWnd,Msg,wParam,lParam:Integer):Integer; stdcall;
begin
  if Msg = WM_DESTROY then PostQuitMessage(0);
  Result := DefWindowProc(hWnd, Msg, wParam, lParam);
end;                     //===========================================
                         //||    venster creŽren en berichtenlus    ||
                         //===========================================
begin
  WindowClass.lpszClassName:= 'KiesMaar';
  WindowClass.hCursor := LoadCursor (0, IDC_ARROW);
  WindowClass.lpfnWndProc  :=  @WindowMainProc;
  RegisterClass(WindowClass);
  CreateWindow(WindowClass.lpszClassName,
               'Tekst in Titelbalk',
               WS_TILEDWINDOW or WS_VISIBLE,
               20, 40, 410, 246, 0, 0, hInstance, nil);
  while GetMessage(Msg, 0, 0, 0) do
    DispatchMessage(Msg);
end.
Resultaat:

Dergelijk bestand kan met het windows kladblok aangemaakt worden. Opslaan als test.dpr - vervolgens kan dit in Delphi geopend worden door er op te dubbelklikken. Bij het opstarten van deze code toont dit een leeg windows-venstertje. Grootte van de exe in Delphi XE: 27.648 bytes. In Delphi 3: 15.872 bytes. Als in Delphi XE een leeg venster gecompileerd wordt op de gewone manier (met VCL en met een .pas bestand er bij), dan is de exe 915.456 bytes groot. Maar zelfs 15872 bytes is erg groot voor een leeg venster, zoals te merken is in de volgende programmeeromgevingen.




Nu in C. De volgende code is gemaakt voor de Tiny C compiler - TCC. Een beetje verouderd - ondersteunt ANSI en geen unicode, maar dat doet hier niet terzake. Overigens is TCC gratis. Het belangrijkste voordeel van TCC is dat het direct een exe aanmaakt, zonder dat een linker nodig is. Het is mogelijk om de code hieronder op te slaan als bvb. "test.c", en dan vanuit een commando-prompt naar dezelfde folder te gaan, en het commando
"tcc test.c" in te typen. De exe wordt dan in ťťn keer aangemaakt. Of "tcc test.c -run", dan start het gemaakte programma direct op (maar zonder de exe op te slaan). De folder van tcc.exe moet wel in het PATH van Windows vermeld zijn om de opdrachtprompt correct te laten werken. Of de broncode test.c wordt even in de folder van tcc.exe gezet.
Wat ook kan: een windows explorer openen en naar de folder van het bronbestand "test.c" gaan, en dit bronbestand verslepen (drag & drop) en loslaten op tcc.exe. Dat maakt direct de nieuwe exe aan (in de folder van het bronbestand). Maar op die manier loop je foutmeldingen mis als er fouten in het bronbestand staan, in een commandoprompt niet.

Bestand "test.c"

                         //===========================================
                         //||        gebruikte bibliotheken         ||
                         //===========================================
#include <windows.h>
                         //===========================================
                         //||         declaratie variabelen         ||
                         //===========================================
WNDCLASS   WindowClass;
MSG        msg;
HINSTANCE  hInst;        //===========================================
                         //||          berichtendispatching         ||
                         //===========================================
						 
LRESULT CALLBACK WindowMainProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam){	
  switch (message) {
    case WM_DESTROY: { PostQuitMessage(0);  break; }
    default: return DefWindowProc(hwnd, message, wParam, lParam);
  }
}                        //===========================================
                         //||    venster creŽren en berichtenlus    ||
                         //===========================================
						 
int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine,	int nCmdShow)
{
  ZeroMemory(&WindowClass, sizeof WindowClass);
  WindowClass.hCursor = LoadCursor (NULL, IDC_ARROW);
  WindowClass.lpszClassName = "KiesMaar";
  WindowClass.lpfnWndProc   = (WNDPROC)WindowMainProc;
  RegisterClass(&WindowClass);
  CreateWindow(WindowClass.lpszClassName,
               "Tekst in Titelbalk",
               WS_TILEDWINDOW | WS_VISIBLE,
               20, 40, 410, 246, 0, 0, hInst, 0);
  while (GetMessage(&msg, NULL, 0, 0) > 0)
    DispatchMessage(&msg);
  return msg.wParam;
}
Resultaat:

De exe is 2.560 bytes groot - maar doet exact hetzelfde als de vorige code. In TCC is een programma verder ontwikkelen (met functies en velden en knoppen) niet optimaal, wegens het verouderde uitzicht (pre windows XP, met hoekige knoppen). Maar het kan wel, en alles werkt wel.




Nu in ASM. Hier is gekozen voor FASM: de gratis "flat assembler". Ook FASM kan in 1 keer een exe aanmaken zonder linker. Als je FASM downloadt, is er zelfs een Windows IDE bij, FASMW.EXE - van daaruit kan de ingetikte (of geplakte) code direct uitgevoerd worden - net zoals in Delphi. Of diehards kunnen fasm.exe opstarten vanuit een opdrachtprompt, mits te letten op de PATH instellingen.

Bestand "test.asm"

format PE GUI 4.0
entry start              ;===========================================
                         ;||        gebruikte bibliotheken         ||
                         ;===========================================
include 'win32w.inc'

section '.idata' import data readable writeable
  library kernel32, 'KERNEL32.DLL', user32, 'USER32.DLL'
    include 'api\kernel32.inc'
    include 'api\user32.inc'
                         ;===========================================
                         ;||         declaratie variabelen         ||
                         ;===========================================	

section '.data' data readable writeable
  lpszClassName   TCHAR 'KiesMaar', 0
  titelbalk       TCHAR 'Tekst in Titelbalk', 0
  WindowClass     WNDCLASS 0, WindowMainProc, 0, 0, NULL, NULL, NULL, COLOR_BTNFACE + 1, NULL, lpszClassName
  msg             MSG

section '.text' code readable executable
                         ;===========================================
                         ;||          berichtendispatching         ||
                         ;===========================================

proc WindowMainProc uses ebx esi edi, hwnd, msg, wparam, lparam
  cmp     [msg], WM_DESTROY
  je      .wmdestroy
  invoke  DefWindowProc, [hwnd], [msg], [wparam], [lparam]
  jmp     .finish
  .wmdestroy:
     invoke  PostQuitMessage, 0
  .finish:
     ret
endp                     ;===========================================
                         ;||    venster creŽren en berichtenlus    ||
                         ;===========================================
start:
  invoke  GetModuleHandle, 0
  mov     [WindowClass.hInstance], eax
  invoke  LoadCursor, 0, IDC_ARROW
  mov     [WindowClass.hCursor], eax
  invoke  RegisterClass, WindowClass
  invoke  CreateWindowEx, 0,\
          lpszClassName,\
          titelbalk,\
          WS_TILEDWINDOW or WS_VISIBLE,\
          20, 40, 410, 246, 0, 0, [WindowClass.hInstance], NULL
msg_loop:
  invoke  GetMessage,msg, NULL, 0, 0
  or      eax, eax
  jz      end_loop
  invoke  DispatchMessage, msg
  jmp     msg_loop
end_loop:
  invoke  ExitProcess, [msg.wParam]  
  
Deze exe is uiteraard nog kleiner: 2.048 bytes, en doet weer eens 100% hetzelfde als de twee vorige codes. Het mooie aan FASM is dat in dezelfde broncode de "resources" kunnen gezet worden die de runtime themes (windows styles) gebruiken. ANSI of unicode kan gebruikt worden. En met deze compiler kan evengoed een 64-bits programma aangemaakt worden. Bijvoorbeeld het 64-bits register "rax" moet dan aangesproken worden in plaats van (in het voorbeeld hierboven) het 32-bits register "eax".

Bij het uitvoeren van deze code doet de AVG virusscanner moeilijk, en meldt die een of ander "cryptic"-trojan. Vreemd, voor een programma dat eigenlijk geen uitvoerbare code bevat. Als de sectie '.idata' en de sectie '.data' van plaats verwisseld wordt (Let op de "i" - uiteraard de bijbehorende lijnen er onder ook - de werking van het programma blijft dan hetzelfde), dan blijft AVG stil.

Ook bij Delphi XE laat de virusscanner van AVG steken vallen: een bestand "leeg.dpr" met daarin de code:

program leeg;
begin
end.
geeft bij het starten (aanmaken en opstarten van de exe) onmiddellijk een melding van een trojan. Ze kunnen moeilijk beweren dat dit "programma" schadelijke code bevat. De research van AVG zou wel wat grondiger mogen gebeuren, want een programmeur botst meerdere keren op dergelijke idiote meldingen. Ditzelfde "lege programma" geeft geen waarschuwing van AVG in Delphi 3.



johanok / at / gmail adres

Verder uitgewerkt: een Windows-programma in FASM
Snellere functies in Delphi
Numlock problemen Windows 8 en 10
Kleinere exe in Delphi zonder de VCL