Alle meine Programme sind auf Robustheit ausgelegt. Ein Programm ist robust, wenn es ohne zu murren und ohne Installiererei einfach läuft (und läuft und läuft). Auf möglichst jeder Zielplattform. (Das gilt sinngemäß auch für Webseiten!) Wichtig ist dabei die Unabhängigkeit von nicht-immer-vorhandenen Zusatzbibliotheken. Etwa den rechts gelisteten, manchen geplagten Anwender sattsam bekannten, mit Updates nervenden Softwarebomben.
Es gibt nur noch recht wenige Ausführungen anderswo, mit Win32 zu programmieren. Der typische Anfänger beginnt mit .NET und C-sharp.
Was die Fachliteratur als „Einfachstes Windows-Programm“ so anbietet ist schlichtweg abschreckend! Hier sind die wirklich einfachen Programme.
#include <stdio.h> int main() { printf("Hallo Welt!\n"); return 0; }
Wer kennt das nicht? (Quelle: Kerninghan+Ritchie: C-Programmierung)
Mit den richtigen Projekteinstellungen entsteht ein Konsolenprogramm, welches „Hallo Welt“ ausspuckt und mit ca. 32 Kilobyte ziemlich fett ist. Hier den Speck zu entfernen ist für Anfänger etwas knifflig, daher werde ich im folgenden nicht darauf eingehen. Außerdem sieht es nicht wie ein Windows-Programm aus. Sondern wie ein Fremdkörper.
#include <windows.h> int WinMain(HINSTANCE, HINSTANCE, LPTSTR, int) { return MessageBox(0,"Hallo Welt!","Nr. 2",0); }
Das ist ein einfaches Windows-Programm!
Beinahe selbsterklärend erscheint ein (kleines) Fenster in der Mitte des Bildschirms mit dem Text „Hallo Welt!“ und einem „OK“-Knopf zum Beenden des Programms.
Mit etwa 20 Kilobyte ist es aber immer noch Bloatware. Diese lassen sich ganz einfach entfernen, siehe nächstes Beispiel.
#include <windows.h> EXTERN_C int WinMainCRTStartup() { return MessageBox(0,"Hallo Welt!","Nr. 3",MB_OK); }
Dieses Programm macht genau dasselbe wie Nr. 2, die EXE-Datei ist mit den richtigen Projekteinstellungen nur noch rund 2 Kilobyte groß. Kleiner geht's nicht, die für Windows erforderlichen Verwaltungsinformationen im EXE-Header kosten ihren Fixpreis. Der eigentliche Kode und die Stringkonstanten bedürfen nur weniger als 100 Bytes und würde in den kleinsten Mikrocontroller passen: An der Intel-Architektur liegt's jedenfalls nicht.
Vergleicht man die drei Beispiele mit dem Dependency Walker, wird man feststellen, dass dieses genau eine externe Abhängigkeit aufweist, nämlich nach USER32:MessageBoxA. Die Bloatware vorher ruft noch Dutzende anderer Funktionen auf, die überhaupt nicht benötigt werden.
Schließlich der Ausgangspunkt für 95 % aller praktischen Programmieraufgaben.
Programme die wie Dialoge aussehen sind das Butterbrot des Programmierers.
Der Erfolg von VisualBasic und Delphi bestätigt dies.
Denn oftmals sind es die kleinen Probleme,
die ein kleines Programm benötigen.
Sogar „große“ Programme mit größenveränderlichem Hauptfenster lassen sich
ganz ohne CreateWindow()
und RegisterClass()
schreiben,
nur ist's dann nicht mehr so vorteilhaft.
#include <windows.h> static INT_PTR WINAPI MainDlgProc(HWND Wnd, UINT msg, WPARAM wParam, LPARAM lParam) { switch (msg) { case WM_INITDIALOG: { }return TRUE; case WM_COMMAND: switch (wParam) { case MAKELONG(IDCANCEL,BN_CLICKED): EndDialog(Wnd,wParam); break; }break; } return FALSE; } EXTERN_C void WinMainCRTStartup() { ExitProcess(DialogBox(0,MAKEINTRESOURCE(100),0,MainDlgProc)); }
Wie man am Quelltext „sieht“, fehlt jegliche Ausgabe, etwa Zeichenketten. Diese befinden sich nunmehr in einer Ressource: (Maus darüberhalten für Erklärungen!)
1 24 "manifest" 100 DIALOG 0,0,220,100 STYLE DS_3DLOOK|DS_NOFAILCREATE|DS_CENTER|WS_MINIMIZEBOX|WS_VISIBLE|WS_CAPTION|WS_SYSMENU CAPTION "Nr. 4" FONT 8, "Helv" { CTEXT "Hallo Welt!",-1,10,10,200,8 DEFPUSHBUTTON "Ende",2,60,50,100,14 }
So eine Ressourcendatei (*.rc) sieht aus wie C, ist aber kein C, und wird vom Resource Compiler (rc.exe) entweder in eine Binärressource (*.res) oder gleich in die EXE-Datei eingebunden. Ressourcen sind (u.a. für Dialoge) standardisiert und mehrsprachig, sie können auch nachträglich mit geeigneten Editoren korrigiert, übersetzt und hinzugefügt werden. So hat der Kunde die Möglichkeit der Anpassung ohne Quelltext! Mit .NET oder Qt geht das nicht.
Die EXE-Datei ist nun größer geworden (4 Kilobyte), weil eine weitere „Section“ (die Ressource nämlich) mit hineingewandert ist. Dafür ist dieses Programm nun beliebig ausbaufähig.
Wie man sieht, kommt man für lange Zeit
um RegisterClass()
und CreateWindow()
herum!
Wer so etwas als „Erstes Windows-Programm“ anbietet,
will wohl nur davon abschrecken.
(Schade, Herr Petzold.)
Das gute alte MSVC6 aus dem Jahr 1998 (1 CD) ist bei mir immer noch die beste Grundlage, um derartig kleine Programme zu schreiben. Dazu passt am besten die Online-Hilfe „MSDN April 2001“. Bei mir auf 3 CDs. Leider macht die Installation Probleme ab Windows XP:
Was man mit MSVC6 (und allen C++-Dialekten vor C++11) nicht tun sollte
ist die Verwendung von std::vector
mit richtigen Objekten!
Dadurch, dass der Raubkopierkonstruktor und std::vector::emplace()
fehlt,
wird das Einbetten von Handles und deren automatischer Destruktoraufruf
zu einer elend windigen Angelegenheit.
Man tut besser daran, keine Objekte sondern nur einfache Datentypen hineinzutun.
Bei Strukturen, die Handles enthalten, muss man diese zu Fuß freigeben.
Oder man tut Zeiger auf Objekte in den Vektor.
(Bei virtuellen Objekten geht das ohnehin nur so.)
Nun, das macht das Iterieren umständlicher. Weil die Iteratoren nun Zeiger auf Zeiger sind.
Fast alle meiner Win32-Programme sind ohne Laufzeitbibliothek geschrieben. Die nachfolgende Übersicht gibt einige Hinweise auf die Unterschiede der Programmierung.
Prinzipiell gibt es da zwei Wege:
Ganz ohne Laufzeitbibliothek | Verwendung der msvcrt.dll | |
Vorteile: |
|
|
Nachteile: |
|
|
Vorgehen: |
|
|
Dem Leser sollte Win32-Programmierung bekannt sein, wenn nicht, dann bitte den „Petzold“ lesen!
Ich bin mit Windows-Programmierung in Pascal groß geworden, und habe Objektbibliotheken zu hassen gelernt, weil sie eben nur so tun, als ob sie Komplexität verbergen könnten. Deswegen wird der Leser bei mir kein einziges MFC-Programm finden. Auch wenn es nach außen so (modern) aussehen möge, schon die Dateigröße verrät, dass es kein MFC sein kann.
Generelle Unterschiede:
mit Laufzeitbibliothek | ohne Laufzeitbibliothek | |
---|---|---|
Minimale Kodegröße | 20 KB | 2,5 KB |
Kopfdateien | #include <stdafx.h> oder ähnliches | #define WIN32_LEAN_AND_MEAN #include <windows.h> // Windows eben #include <windowsx.h> // Nützliche Makros #include <shlwapi.h> // String-Funktionen #include <comdlg32.h> // Datei-öffnen-Dialog #include <comctl32.h> // erweiterte Dialogelemente ... |
Eintrittspunkt | WinMain
| WinMainCRTStartup
|
Kommandozeilen-Argumente | Argument von WinMain , Programm-Name entfernt
Unicode-Problem! GetCommandLine wird empfohlen
| GetCommandLine benutzen, mit PathGetArgs
zerstückeln
|
Programmende | return retval
| ExitProcess(retval)
Nur return killt nicht die durch GetOpenFileName erzeugten Threads!
|
Compiler-Schalter | Projekt-Einstellungen funktionieren, die vorcompilierten Header machen aber meistens nur Ärger |
|
Linker-Schalter | Projekt-Einstellungen, auf die OLE-Importbibliotheken kann man getrost
verzichten, einige andere Importbibliotheken werden des öfteren benötigt:
olepro32.lib
(typischerweise für
OleLoadPicture()
ist richtig für Windows 95, aber falsch für Win7/64.
Für 64-Bit-Plattform zu oleaut32.lib wechseln!
oleaut32.dll gibt's sicher ab Windows 98 SE.
|
#pragma -Anweisungen ist nicht portabel
hin zum neueren MSVC9! Deshalb besser im Projekt setzen.
|
Diese Tabelle spiegelt nur den Unterscheid bei Microsoft Visual C++ (ab Version 6 geprüft) wider. Bei Borland C++ gelten andere Regeln!
Statt mit msvcrt-light.lib herumzuhampeln kann man den Compiler des Windows-DDKs benutzen. Dieser linkt automatisch konservativ zur alten MSVCRT.DLL mit folgender Kommandozeile, aus einer Build-Umgebung heraus:
Man erspart sich Probleme mit stdout
, __try
, __chkstk
und sicherlich auch der STL.
Ohne /entry= wird der Code größer, aber statische Konstruktoren
und Kommandozeilenargumente werden kompatibel behandelt.
Konstanten im Kodesegment (/GF) ersparen initialisierte statische Variablen.
(Alle nicht initialisierten statischen Variablen werden vom EXE-Loader
stets mit Null initialisiert, muss man wissen.
Es ist ohnehin unschöner Programmierstil, mit Nicht-Null initialisierte
statische Variablen nicht const
zu setzen.
Bei der Verwendung von
unicows.lib
bzw.
libunicows.lib
kommt man um solche Variablen nicht herum.)
Den Erfolg der Maßnahme kann man mittels dumpbin /headers
prüfen: das Datensegment (.data
) muss in der EXE-Datei leer sein.
SECTION HEADER #3
.data name
6F4 virtual size
8000 virtual address (00408000 to 004086F3)
0 size of raw data
0 file pointer to raw data
0 file pointer to relocation table
0 file pointer to line numbers
0 number of relocations
0 number of line numbers
C0000040 flags
Initialized Data
Read Write
Ohne Standardbibliothek kann man nicht „portabel“ im Sinne des ANSI-Standards
programmieren. Aber kein Windows-Programm ist jemals ANSI-konform.
Es gibt kein main
, kein printf
. (Wohin denn?)
Muss ich weiter argumentieren? (Ich rede hier nicht von Konsolen-Programmen.)
Also, Portabibiltät ist etwas für den Elfenbeinturm (Informatiker, Lehre)
oder für Unix (Linux - da kommt man nicht herum!).
Für Windows: da sage ich einfach Tschüss und winke-winke!
Jetzt muss man nur noch wissen, was man statt der gewohnten Funktionen
verwenden muss. Ganz nebenbei wird das Programm unicode-fähig, ohne jedesmal
den TCHAR-Präfix (bspw. _tprintf
statt printf
)
verwenden zu müssen (was ja auch nicht ganz portabel ist).
Hier eine kurze Übersicht
mit Laufzeitbibliothek | ohne Laufzeitbibliothek | |||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Kommandozeilen-Argumente | via strchr zerstückelt | PathGetArgs einsetzen!
| ||||||||||||||||||||||||||||||||||
String-Manipulation | Vorwort: Es gibt einen kleinen aber feinen Unterschied
bei der Behandlung von Puffergrößen bei String-Funktionen der Laufzeitbibliothek
und von Windows!
elemof(array) (siehe unten) leistet hier gute Dienste.
| |||||||||||||||||||||||||||||||||||
strlen | lstrlen (ist Unicode-fähig auch unter Win9x)
| |||||||||||||||||||||||||||||||||||
strcpy , strncpy | lstrcpy , besser lstrcpyn ; für Win9x-Unicode StrCpy , besser StrCpyN
Beachte: strncpy und StrCpyN arbeiten unterschiedlich bei Pufferknappheit!
StrCpyN garantiert eine terminierende Null, kopiert also maximal N-1 Zeichen; strncpy füllt den Puffer bis zum Rand.
| |||||||||||||||||||||||||||||||||||
strcat | lstrcat, StrCat , besser StrCatBuff
| |||||||||||||||||||||||||||||||||||
strchr bzw. strrchr | StrChr bzw. StrRChr
| |||||||||||||||||||||||||||||||||||
strstr bzw. strrstr | StrStr bzw. StrRStr
| |||||||||||||||||||||||||||||||||||
strpbrk | StrPBrk
| |||||||||||||||||||||||||||||||||||
strspn | StrSpn
| |||||||||||||||||||||||||||||||||||
strdup | StrDup
|
Hier könnte man noch beliebig fortsetzen.
Bei Einsatz von Gleitkomma bei Ein- und Ausgabe ist es besser,
ein funktionierendes sprintf()
und sscanf()
zu haben, diese beschafft man sich wie folgt:
EXTERN_C int (_cdecl*_stprintf)(PTSTR,int,PTSTR,...);
EXTERN_C int (_cdecl*_stscanf)(PTSTR,PTSTR,...);
#ifdef UNICODE
# define __IMP__STPRINTF "_snwprintf"
# define __IMP__STSCANF "swscanf"
#else
# define __IMP__STPRINTF "_snprintf"
# define __IMP__STSCANF "sscanf"
#endif
// irgendwo in der Programminitialisierung:
HINSTANCE hLibMSVCRT=LoadLibraryA("MSVCRT.DLL");
(FARPROC)_stprintf=GetProcAddress(hLibMSVCRT,__IMP__STPRINTF);
(FARPROC)_stscanf=GetProcAddress(hLibMSVCRT,__IMP__STSCANF);
oder, noch einfacher, durch eine spezielle Importbibliothek
.
Denn die MSVCRT.DLL ist seit Windows 95 standardmäßig installiert,
die braucht man weder mitzuliefern noch deren Funktionen statisch einzubinden.
Oftmals will der Linker eine Funktion __ftol oder __ftol2_sse. Siehe hier:
Bei Linker-Fehlern mit sse ist die Architektur beim Compilieren „falsch“ eingestellt;
das Programm würde auf älteren Prozessoren nicht laufen.
Schalter: /arch:IA32 (Microsoft C++).
Die Funktion _ftol ließe sich mit der Kommandozeilenoption
/QIfist inlinen.
Das generiert Kode wie diese Ersatzfunktion:
long _declspec(naked) _cdecl _ftol(void){ _asm{
push eax
fistp dword ptr [esp]
pop eax
ret
}}
Das Problem: Diese Funktion rundet standardmäßig.
(Die echte __ftol
-Funktion rundet zur Null.)
Meistens will man sowieso runden, und deshalb ist es besser, sämtliche
automatischen Typecasts zu entfernen und durch eine eigene Funktion
zu ersetzen, etwa:
__forceinline __int64 llrint(double f) {
__int64 i;
_asm fld f
_asm fistp i
return i;
}
statt dem langweiligen, aber üblichen i=(int)floor(x+0.5)
.
Das Rundungsergebnis ist dabei nicht ganz gleich,
bei x=0.5
rundet diese Zeile zu 1
(Kaufmännisches Runden),
während llrint(0.5)
zur geraden Zahl 0
rundet (Banker's Rounding).
X86-Assembler | AMD64-Assembler | Visual C (#include <intrin.h> ) | Gnu C | Funktion
| ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Bytetausch (Endian-Konvertierung) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
xchg ah,al_byteswap_ushort | __builtin_bswap16 | htons() auf Little-Endian
| bswap eax | bswap rax | _byteswap_ulong | _byteswap_uint64 | __builtin_bswap32 | __builtin_bswap64 | htonl() auf Little-Endian
| Bitsuche, Bitzählen, Bitrotieren
| bsf eax,ecx | bsf rax,rcx | _BitScanForward | _BitScanForward64 | __builtin_ctz | __builtin_ctzll | nicht identisch!
| bsr eax,ecx | bsr rax,rcx | _BitScanReverse | _BitScanReverse64 | __builtin_clz | __builtin_clzll | =(int)ld(x) logarithmus dualis
| bt [ecx],edx | setnz al bt [rcx],rdx | setnz al _bittest | _bittest64 | - | (a&1<<b) | Überspannt Speicherbereiche
| btr [ecx],edx | setnz al btr [rcx],rdx | setnz al _bittestandreset | _bittestandreset64 | __sync_fetch_and_nand | (m=1<<b,r=a&m,a&=~m,r)
| bts [ecx],edx | setnz al bts [rcx],rdx | setnz al _bittestandset | _bittestandset64 | __sync_fetch_and_or | (m=1<<b,r=a&m,a|=m,r)
| btc [ecx],edx | setnz al btc [rcx],rdx | setnz al _bittestandcomplement | _bittestandcomplement64 | __sync_fetch_and_xor | (m=1<<b,r=a&m,a^=m,r)
| popcnt eax,ecx | popcnt rax,rcx | __popcnt16, __popcnt, __popcnt64 | __builtin_popcount | __builtin_popcountll | Zählt 1-Bits
| or ecx,ecx | setp al or rcx,rcx | setp al - | __builtin_parity | __builtin_parityll | Parität, Anzahl 1-Bits modulo 2
| rol eax,cl | rol rax,cl | _rotl | _rotl64 | (a<<b|a>>-b) | Linksrotieren mit umlaufenden Bits
| ror eax,cl | ror rax,cl | _rotr | _rotr64 | (a<<-b|a>>b) | Rechtsrotieren mit umlaufenden Bits
| (popcnt: Neuer Befehl ab 2008) | Interlocked
| cmpxchg [ecx],edx | cmpxchg [rcx],rdx | _InterlockedCompareExchange | __sync_val_compare_and_swap | SMP-sichere Austauschoperation
| lock inc dword[ecx] | lock inc qword[rcx] | _InterlockedIncrement | ? | SMP-sicheres Inkrementieren einer Speicherstelle
| lock dec dword[ecx] | lock dec qword[rcx] | _InterlockedIncrement | ? | SMP-sicheres Dekrementieren einer Speicherstelle
| Sonstiges
| int 3 | __debugbreak | ? | Debugger-Aufruf
| nop | |