Effiziente Win32-Programmierung

Für interessierte Quelltext-Leser. Und Programmierer, die ihre EXE-Größe klein halten wollen. Ganz nebenbei geht es auch darum, schnell zum Ziel zu kommen, da man sich mit derartigem Programmierstil viele Probleme mit Kompatibilität vom Hals halten kann.

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#. Oder, noch schlimmer, mit Python.

Einfache Win32-Programmbeispiele

Was die Fachliteratur als „Einfachstes Windows-Programm“ so anbietet ist schlichtweg abschreckend! Hier sind die wirklich einfachen Programme.

Nr. 1

#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.

Nr. 2

#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.

Nr. 3

#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.

Nr. 4

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.

Hinweis: Das automatische Skalieren der Kindfenster bei Größenveränderung ist mit Win32 lästig zu programmieren, mit Dividern ist es sogar richtig schwierig. Deshalb wird es gerne weggelassen, wie man an vielen Windows-Programmen sicherlich festgestellt hat. Statt sich jedesmal die Mühe zu machen, habe ich einen „allgemeinen Dialog-Resizer“ programmiert, der alle Dialoge größenveränderlich macht. Das klappte damals aber nur mit 16-Bit-Programmen.
Hierfür wäre wahrscheinlich ein Exkurs nach Qt interessant, wo so gut wie gar keine Positionsangaben vonnöten sind. Leider ist Qt richtig fett.

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.)

MSVC6 installieren

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.

Win32-Programme ohne Laufzeitbibliothek

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 LaufzeitbibliothekVerwendung der msvcrt.dll
Vorteile:
  • Keine Abhängigkeit zu undokumentierten Schnittstellen
    (Die msvcrt.dll ist ja nicht regulär dokumentiert.)
  • Quelltext kann für Windows 3.1 (16 bit oder Win32s) verwendet werden
  • Machbarkeit gemeinsamer Quelltexte für Eigenschaftsseiten-Lieferant
    des Geräte-Managers für Win9x/Me (16 bit) und Windows 2000 und höher (32 bit)
  • Man kann einfach float- und Mathematikfunktionen benutzen
  • Man kann Standard-Ein/Ausgabe (etwa printf) benutzen
  • Man kann new in C++-Programmen benutzen
  • Man kann ohne Umwege mit Gdiplus programmieren
  • Einige Laufzeitfunktionen (etwa memcpy) sind verfügbar
  • C-Exceptions (__try + __except) funktionieren, C++-Exceptions habe ich hingegen nicht hinbekommen
Nachteile:
  • Starke Einschränkungen bei der Programmierung
  • C++-Programme möglich aber kaum sinnvoll
  • Stack-Variablen < 4 KByte (pro Funktion)
  • Keine ANSI-Kompatibilität für Konsolenprogramme
  • Abhängigkeit von (sich leicht wandelnder) msvcrt.dll
    (Diese DLL ist ab Windos 95 standardmäßig verfügbar.)
  • Einige Workarounds (etwa für __fltused) trotzdem erforderlich
  • Statische Konstruktoren stehen nicht zur Verfügung (nur C++) — doch!
  • Keine ANSI-Kompatibilität für Konsolenprogramme,
    Wrapper-Funktion erforderlich, die bspw. argc und argv bereitstellt
Vorgehen:
  • Compiler-Schalter:
    • /GZ (Stackprüfung AUS)
    • /GS- (Pufferüberlaufprüfung AUS)
  • Eintrittspunkt WinMainCrtStartup() oder _DllMainCrtStartup() oder
  • Eintrittspunkt für Linker auf WinMain bzw. DllMain setzen
  • Compiler-Schalter:
    • /GZ (Stackprüfung AUS)
    • /GS- (Pufferüberlaufprüfung AUS)
    • /GR- (Laufzeit-Typinformation AUS, nur C++)
  • #define _DLL generiert Einsprünge ohne Trampolin, oder
    in Projekteinstellungen „DLL-Laufzeitbibliothek“ auswählen (bevorzugt; generiert das _DLL-Symbol).
  • Eintrittspunkt WinMainCrtStartup() oder _DllMainCrtStartup() oder
  • Eintrittspunkt für Linker auf WinMain bzw. DllMain setzen
  • Zusätzliche Importbibliothek msvcrt-light.lib
    (diese stellt die Eintrittspunkte für die DLL zur Verfügung, produziert keinen Kode)

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 Laufzeitbibliothekohne 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
  • /GZ (Stack-Prüfung) entfernen
  • /GF (Konstanten und konstante Strings ins Kodesegment) setzen
  • Release: Optimierung: Größe minimieren
  • Code-Generierung: _stdcall oder _fastcall, bloß nicht _cdecl
  • C++: keine Ausnahmebehandlung
  • neueres C++: Bereichsüberprüfungen ausschalten
  • Wenn dennoch eine Meldung mit „__chkstk“ kommt: /Gs65536 (Stack-Prüfung ab bspw. 64 Kilobyte) setzen (Zahl je nach Bedarf)
Linker-Schalter Projekt-Einstellungen, auf die OLE-Importbibliotheken kann man getrost verzichten, einige andere Importbibliotheken werden des öfteren benötigt:
  • winmm.lib (Multimedia, bspw. sndPlaySound)
  • comctl32.lib (ListView, TreeView, Buttonleiste, Statuszeile...)
  • shlwapi.lib (diverse Mini-Funktionen)
    • Unbedingt vor shell32.lib einfügen! Sonst wird StrChrA / StrChrW mit shell32.dll verknüpft, und die 🦎 Echse läuft nicht unter Windows 9x
  • hhctrl.lib (Zugriff auf HtmlHelp())
  • gdiplus.lib (GDI+ = Zeichenfunktionen mit Antialiasing),
    die zugehörige gdiplus.dll gibt's erst ab Windows XP dazu; man kann diese DLL ab Windows 98 einsetzen
Achtung, Falle bei Win64: 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.
  • Bibliotheken wie links angegeben
  • Alle Standardbibliotheken igonorieren (/nodefaultlib)
  • Bei Notwendigkeit von Win32s (Lauffähigkeit unter Windows 3.1x) Relokationstabelle erzeugen (macht die .EXE größer)
  • Den Schalter /opt:nowin98 angeben: die EXE wird kleiner und läuft dennoch unter Win98/Me
  • Den möglichen 3GB Adressraum sollte man immer aktivieren: Schalter /largeaddressaware angeben
  • Release: Warum nicht die Header-Prüfsumme generieren lassen? /release angeben
  • /merge:.rdata=.text, 2 Sections kombinieren, Linker-Warnung ignorieren, oder
    /section:.text,e, Kodesegment auf Execute-Only setzen (mehr Sicherheit vor Bugs, ändert die Programmgröße nicht)
  • Stack-Allokation gleich /Gs-Compilerschalter oder größer setzen
Die Verwendung von #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!

Hätte ich's bloß eher gewusst!

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:

cl /MD /O1 /GF %1 -I %Include%\..\crt /link /libpath:%Lib%\w2k\i386 /opt:nowin98 /merge:.rdata=.text /entry:main /align:16 /release
Ein DDK oder WDK braucht man sowieso irgendwann einmal, etwa zum Zugriff auf HID-Geräte.

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 Laufzeitbibliothekohne Laufzeitbibliothek
Kommandozeilen-​Argumente via strchr zerstückeltPathGetArgs 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!
  • C-Laufzeitbibliothek: Der Puffer kann restlos aufgefüllt werden, der resultierende String ist in diesem Fall nicht nullterminiert. Daher sieht man viel Linux-Kode, der das letzte Zeichen des Puffers mit '\0' besetzt, nur so ist er sicher. Die maximale Anzahl geschriebener Zeichen (Returnwert etlicher Funktionen) ist gleich Puffergröße.
  • Windows-Funktionen: Der Puffer wird mit maximal Länge-1 Zeichen aufgefüllt und der Inhalt stets nullterminiert. Fummeleien mit nicht-nullterminierten Strings entfallen, aber 1 Zeichen des Puffers geht flöten, wenn man ohnehin die Zeichenzahl weiter benutzt, etwa für TextOut(). Die maximale Anzahl geschriebener Zeichen (Returnwert etlicher Funktionen) ist gleich Puffergröße-1.
Beide Varianten zählen die Puffergröße in Zeichen, nicht in Bytes. Das Makro elemof(array) (siehe unten) leistet hier gute Dienste.
strlenlstrlen (ist Unicode-fähig auch unter Win9x)
strcpy, strncpylstrcpy, 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.
strcatlstrcat, StrCat, besser StrCatBuff
strchr bzw. strrchrStrChr bzw. StrRChr
strstr bzw. strrstrStrStr bzw. StrRStr
strpbrkStrPBrk
strspnStrSpn
strdupStrDup
s{n}printf, itoawsprintf, besser wnsprintf (siehe Anmerkungen nächste Zeile)
vsprintf, _vsnprintfwvsprintf, besser wvnsprintf (kümmerlich (keine variablen Feldbreiten mit *), kein Gleitkomma;
  letztere Funktion ist Win9x-Unicode-fähig, aber erst mit installiertem Internet Explorer 3.0 oder höher verfügbar — Windows 95 wurde mit IE 2.0 ausgeliefert)
Beachte: {v}snprintf und w{v}nsprintf arbeiten unterschiedlich bei Pufferknappheit!
w{v}nsprintf garantiert eine terminierende Null, schreibt also maximal N-1 Zeichen; {v}snprintf füllt den Puffer bis zum Rand.
sscanfkein Ersatz! Leider. Mit StrToInt ersetzen
atoiStrToInt, StrToIntEx (für Hexadezimalzahlen ein "0x" davorsetzen!)
shlwapi.h bietet noch eine Reihe weiterer nützlicher String-Funktionen, insbesondere zur Behandlung von Dateinamen.
Deren Unicode-Funktionen (Suffix …W) sind auch unter Windows 9x funktionsfähig, im Gegensatz zu fast allen Funktionen der USER32.DLL.
(Leider arbeitet PathFindExtension bei Leerzeichen sowie bei Unix-Dateinamen wie .htaccess fehlerhaft: selber schreiben!)
Speicher malloc, calloc, newLocalAlloc (C++-Operator new kann überladen werden)
free, deleteLocalFree (C++-Operator new kann überladen werden)
memset, ZeroMemory, FillMemorySelber schreiben:
void _declspec(naked) _fastcall FillMemB(size_t l,void*d,BYTE fill){ _asm{
	mov	eax,[esp+4]
	xchg	edi,edx		// Zeiger in EDX
	rep	stosb		// Länge in ECX, keine Aktion bei ECX=0
	mov	edi,edx		// EDI restaurieren
	ret	4
}}
#define FillMemory(d,l,f) FillMemB(l,d,f)
#define ZeroMemory(d,l) FillMemB(l,d,0)
oder Windows-Funktionen verwenden (lassen):
#undef RtlZeroMemory
EXTERN_C void _declspec(dllimport) WINAPI RtlZeroMemory(PVOID,SIZE_T);
#undef RtlFillMemory
EXTERN_C void _declspec(dllimport) WINAPI RtlFillMemory(PVOID,SIZE_T,BYTE);
oder durch Intrinsics ersetzen: __stosb(), __stosw() oder __stosd() (bei AMD64 auch _stosq()) je nach Datengröße
memcpy, CopyMemory,
memmove, MoveMemory
Selber schreiben:
void _declspec(naked) _fastcall MoveMemB(size_t l, void*d,const void*s){ _asm{
	mov	eax,[esp+4]	// Quelle auf Stack
	xchg	esi,eax
	xchg	edi,edx		// Ziel in EDX
	cmp	esi,edi
	jnc	l1
	lea	esi,[esi+ecx-1]
	lea	edi,[edi+ecx-1]
	std			// abwärts
l1:	rep	movsb		// Länge in ECX, keine Aktion bei ECX=0
	cld
	mov	edi,edx
	mov	esi,eax		// Register restaurieren
	ret	4
}}
#define CopyMemory(d,s,l) MoveMemB(l,d,s)
#define MoveMemory(d,s,l) MoveMemB(l,d,s)
oder Windows-Funktionen verwenden (lassen):
#undef RtlMoveMemory
EXTERN_C void _declspec(dllimport) WINAPI RtlMoveMemory(PVOID,const VOID*,SIZE_T);
#undef CopyMemory
#define CopyMemory MoveMemory
Hinweis: Einige Funktionen mehr sind in der kernl32p.lib verfügbar, statt der standardmäßigen kernel32.lib.
oder „inlinen“ lassen:
#pragma inline(memcpy,memset)
memchr, wmemchrSelber schreiben:
BYTE* _declspec(naked) _fastcall ScanMemB(size_t l,const BYTE*s,BYTE c){ _asm{
	mov	eax,[esp+4]
	xchg	edi,edx
	repne	scasb
	lea	eax,[edi-1]
	mov	edi,edx		// EDI restaurieren
	jz	l1
	xor	eax,eax		// NULL liefern wenn nicht gefunden
l1:	ret	4
}}
#define memchr(s,c,l) ScanMemB(l,s,c)
memcmp, wmemcmpSelber schreiben:
int _declspec(naked) _fastcall CmpMemB(size_t l,const BYTE*d,const BYTE*s){ _asm{
	jecxz	l1		// undefiniertes Ergebnis bei Länge 0
	mov	eax,[esp+4]
	xchg	esi,eax
	xchg	edi,edx
	repe	cmpsb
	movsx	edi,byte ptr [edi-1]
	movsx	esi,byte ptr [esi-1]
	sub	esi,edi
	mov	edi,edx
	xchg	esi,eax
l1:	ret	4
}}
#define memcmp(d,s,l) CmpMemB(l,d,s)

Die ähnliche Kernel-Funktion RtlCompareMemory ist bei Win32 nicht in kernel32.lib enthalten.
Formatierte Datei-Ein/Ausgabe fprintfzusammensetzen aus wvnsprintf, CharToOemBuff und WriteFile (oder WriteConsole)
Niemals printf mit Umlauten oder sonstigen lokalisierbaren Strings verwenden!
fscanfReadFile benutzen
Gleitkomma sin, cosInlinen lassen: #pragma intrinsic(sin,cos,atan,sqrt,fabs)
oder selber schreiben: Koprozessor nutzen
Irgendwo braucht es im Programm ein __fltused (EXTERN_C void _declspec(naked) _cdecl _fltused(void) {}), um den Linker ruhig zu stellen. Die genannte Quelltext-Zeile erzeugt nur das notwendige Symbol, keinen Kode.

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).

Intrinsische Funktionen

Übersicht
X86-AssemblerAMD64-AssemblerVisual C (#include <intrin.h>)Gnu CFunktion
Bytetausch (Endian-Konvertierung)
xchg ah,al_byteswap_ushort__builtin_bswap16htons() auf Little-Endian
bswap eaxbswap rax_byteswap_ulong_byteswap_uint64__builtin_bswap32__builtin_bswap64htonl() auf Little-Endian
Bitsuche, Bitzählen, Bitrotieren
bsf eax,ecxbsf rax,rcx_BitScanForward_BitScanForward64__builtin_ctz__builtin_ctzllnicht identisch!
bsr eax,ecxbsr 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,ecxpopcnt rax,rcx__popcnt16, __popcnt, __popcnt64__builtin_popcount__builtin_popcountllZählt 1-Bits
or ecx,ecx
setp al
or rcx,rcx
setp al
-__builtin_parity__builtin_parityllParität, Anzahl 1-Bits modulo 2
rol eax,clrol rax,cl_rotl_rotl64(a<<b|a>>-b)Linksrotieren mit umlaufenden Bits
ror eax,clror rax,cl_rotr_rotr64(a<<-b|a>>b)Rechtsrotieren mit umlaufenden Bits
(popcnt: Neuer Befehl ab 2008)Interlocked
cmpxchg [ecx],edxcmpxchg [rcx],rdx_InterlockedCompareExchange__sync_val_compare_and_swapSMP-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__noop?Leerbefehl

Neuere Visual Studios

Mit jeder neuen Version von Visual Studio lauern neue Überraschungen. Zum einen wurden Stringfunktionen als potenziell unsicher eingestuft:

Zum anderen werden die printf-Funktionen zusammengefasst zu __stdio_common_*printf. Ein Schritt, der die Verwendung der msvcrt.dll in Verbindung mit msvcrt-light.lib bzw. msvcrt-light-x64.lib unmöglich macht.

Zum Glück gibt es dafür Rückfall-Makros, die am besten für das gesamte Projekt festgelegt werden:

#define _NO_CRT_STDIO_INLINE	// Visual Studio 2017+: Verhindert __stdio_common_vswprintf
#define _CRT_SECURE_NO_WARNINGS	// Visual Studio 2008+: verhindert sinnlose (ja, falsche) Warnungen wegen drohender Pufferüberläufe

Statische Konstruktoren und Destruktoren (C++)

Für diese gibt es die MSVCRT.DLL-Funktion _initterm. Diese bekommt den Zeiger auf eine Liste von Funktionszeigern zum Fraß sowie deren Endadresse. Von den einzelnen Modulen werden diese Adressen in das Datensegment ".CRT$XCC" geworfen. Der Linker pappt diese Datensegmente in eher zufälliger Reihenfolge hintereinander und baut so eine Tabelle auf, die (wegen der nicht ganz sicheren Segmentausrichtung des Linkers) Nullen enthalten kann; _initterm() übergeht Nullen automatisch. Siehe auch: Wie ein Linker arbeitet.

Und so kommt man an die Anfangs- und Endadresse dieser Tabelle, siehe auch Microsoft-CRT-Quelle:

#include <windows.h>

#pragma bss_seg(".CRT$XCA")
static void* Anfang;	// Keine Initialisierung
#pragma bss_seg(".CRT$XCZ")
static void* Ende;
#pragma bss_seg()	// zurückschalten zu ".bss"

Es wird schlichtweg darauf vertraut, dass der Linker die Segmente in alphabetischer Reihenfolge zusammensetzt! Damit stehen diese beiden Tabellenelemente am Anfang und am Ende der Tabelle, und man kann _initterm() aufrufen:

extern "C" _CRTIMP void _cdecl _initterm(void*&,void*&);
 _initterm(Anfang,Ende);	// ruft alle statischen Konstruktoren

Die Konstruktoren enthalten auch Kode, um die statischen Destruktoren in die Liste für _onexit einzutragen. Das gesonderte Aufrufen der Destruktoren ist daher nicht notwendig.

Meine immer wiederkehrenden Unterprogramme

Manchmal habe ich den Eindruck, dass Microsoft einige immer wieder benötigte Funktionen und Makros verschläft zu implementieren. Die 1997 aufkommende shlwapi.dll war da schon ein kleiner Durchbruch, lieferte diese doch endlich die fehlenden String-Funktionen nach.

Immer noch fehlen Funktionen für Gleitkomma-Ein/Ausgabe, obwohl dieses Zahlenformat ins Win32-GDI eingezogen ist (siehe SetWorldTransform) und bei GDI+ für exakte Positionierung kantengeglätteter Figuren äußerst nützlich ist.

Kopfdateien und defines
WIN32_LEAN_AND_MEANlässt einigen unnötigen Windows-3.0-Ramsch weg. Nicht verwenden bei Verwendung von GDIplus!
STRICTSchaltet die strenge Typprüfung auch bei Win16 ein.
Wichtig für den klassischen Bug bei ReleaseDC: Man konnte HWND und HDC vertauschen, und irgendwann (viel später) krachte es im GDI, weil die Handles alle waren.
<windowsx.h>Nützliche Makros, ersparen eine Unmenge SendMessage, und man kann kombinierte Win16/Win32-Quelltexte schreiben
Makros
T("string")wie TEXT("string"), erzeugt je nach UNICODE "string" oder L"string"
#define T(x) TEXT(x)
elemof(array)liefert Anzahl der Elemente eines Arrays (Feldes) bekannter Länge, bspw. die Anzahl der Zeichen eines TCHAR-Feldes
#define elemof(x) (sizeof(x)/sizeof(*(x)))
nobreakexpandiert zu nichts und dient zur Kennzeichnung absichtlich „vergessener“ break bei switch + case
#define nobreak
Funktionen
InitStructFüllt eine Struktur mit Nullen, aber das erste UINT mit der Längenangabe. Zweckmäßig für eine große Zahl von Windows-Funktionen.
void _fastcall InitStruct(LPVOID p, UINT len) {
 __stosd(p,0,len>>2);
 *(UINT*)p=len;
}
EnableDlgItemVermählung von EnableWindow und GetDlgItem
void EnableDlgItem(HWND Wnd, UINT id, BOOL state) {
 Wnd=GetDlgItem(Wnd,id);
 if (Wnd) EnableWindow(Wnd,state);
}
ShowDlgItemVermählung von ShowWindow und GetDlgItem
void ShowDlgItem(HWND Wnd, UINT id, int state) {
 Wnd=GetDlgItem(Wnd,id);
 if (Wnd) ShowWindow(Wnd,state);
}
GetRadioCheckDas Gegenteil von CheckDlgButton
SetCheckboxGroupBitfeld in eine Reihe von Auswahlfeldern setzen
void SetCheckboxGroup(HWND Wnd, UINT u, UINT o, UINT v) {
 for (; u<=o; u++,v>>=1) CheckDlgButton(Wnd,u,v&1);
}
GetCheckboxGroupBitfeld aus einer Reihe von Auswahlfeldern lesen
UINT GetCheckboxGroup(HWND Wnd, UINT u, UINT o) {
 UINT v,m;
 for (v=0,m=1; u<=o; u++,m+=m) if (IsDlgButtonChecked(Wnd,u)==1) v|=m;
 return v;
}
SetEditFocussollte aufgerufen werden, wenn bei WM_COMMAND - IDOK ein fehlerhaftes Eingabefeld vorgefunden wird: der Text wird markiert
MoveRectIntoRect
MoveRectIntoFullScreen
(Fenster-)Rechteck in ein anderes (Bildschirm-)Rechteck schieben, zur Verhinderung deplatzierter Fenster.
(Wer kennt nicht die Programme mit klassischen Bugs? Win3.1 CLOCK.EXE, um mal anzufangen, WinAmp…)
LimitInteger-Zahl zwischen zwei Grenzen eingrenzen
iitrafoInteger-Zahl „transformieren“, bspw. Messwert nach Zweipunktabgleich
bsf,bsr„bit scan forward“, „bit scan backward“, so heißen die Assemblerbefehle, die hier nachgebildet werden: nieder- bzw. höchstwertigstes Bit suchen
#if _MSC_VER < 1400	// Visual C++ 6, nur X86
__forceinline char _BitScanForward(DWORD*Index, DWORD mask) {
 _asm	bsf	eax,mask
 _asm	mov	ecx,Index	// ohne ECX einzusauen geht es hier nicht
 _asm	mov	[ecx],eax
 _asm	setnz	al
}
__forceinline char _BitScanReverse(DWORD*Index, DWORD mask) {
 _asm	bsr	eax,mask
 _asm	mov	ecx,Index
 _asm	mov	[ecx],eax
 _asm	setnz	al
}
#endif
bt,btc,btr,bts„bit test“, „bit test and complement“, „bit test and reset“, „bit test and set“, so heißen die Assemblerbefehle, die nachgebildet werden.
  • _bittest(), _bittest64()
  • _bittestandcomplement(), _bittestandcomplement64()
  • _bittestandreset(), _bittestandreset64()
  • _bittestandset(), _bittestandset()

Nützlich zu wissen: Der Bittest funktioniert in Speicherbereichen bis 512 MByte Größe!
vMBox, MBoxMessageBox mit String aus Resource und statischem Titel
HINSTANCE ghInstance;	// kann je nach Ressourcen-DLL gesetzt werden, sonst 0 = aus EXE-Datei
TCHAR MBoxTitle[64];	// typischerweise mit LoadString() am Programmstart vorbelegen

int vMBox(HWND Wnd, LONG_PTR id, UINT style, va_list arglist) {
 TCHAR buf2[1024];
 if (IS_INTRESOURCE(id)) {	// String aus Ressource (kein Zeiger)
  TCHAR buf1[256];
  LoadString(ghInstance,(UINT)id,buf1,elemof(buf1));
  wvnsprintf(buf2,elemof(buf2),buf1,arglist);
 }else wvnsprintf(buf2,elemof(buf2),(PCTSTR)id,arglist);	// id ist Zeiger
 return MessageBox(Wnd,buf2,MBoxTitle,style);
}

int _cdecl MBox(HWND Wnd, UINT id, UINT style,...) {
 return vMBox(Wnd,id,style,(va_list)(&style+1));
}
GetFileNameExtZeiger auf Datei-Erweiterung liefern, funktioniert auch bei Unix-Dateien wie .login (keine Erweiterung!)
PathFindExtension() ist leider unbrauchbar.

Das fehlende typeof-Makro kann ich leider nicht nachliefern…

Realisierung von Plug-and-Play

Für die Programmierung einigermaßen gesitteter, hardwarenaher Programme ist eine PnP-Unterstützung erforderlich. Hierbei gibt es immer wieder Reinfälle bezüglich der Kompatibilität der Windows-Versionen untereinander.

Hier hat Microsoft eine Menge unterdokumentierter Fallstricke in die Kopfdateien eingebaut. Dies beginnt schon damit, dass man für manche noch so kleine Funktion oder GUID das DDK benötigt. Auch wenn's nur um das Auflisten verfügbarer serieller Schnittstellen geht.

SetupDi- und CfgMgr-Funktionen

Das moderne Arbeitspferd bei PnP sind SetupDi-Funktionen. Sie fangen alle mit SetupDi an und sind in der setupapi.lib implementiert. Diese gibt es in der „alten“ Ausführung mit Klassenbezug und in der „neuen“ mit zusätzlichem Interface-Bezug.

Hier zähle ich nur die wichtigsten Funktionen auf.

SetupDi mit Klassenbezug

Unter Klasse ist die Geräteklasse wie im Geräte-Manager zu verstehen. Also bspw. „Anschlüsse“ (englisch „Ports“), „Netzwerkadapter“ usw.

Diese Funktionen sind ab Windows 95 und Windows NT 4 verfügbar.

SetupDi mit Interface-Bezug

Unter Interface ist offenbar eine detailliertere Schnittstelle als bei den Geräteklassen gemeint. Klar, Geräteklassen verbindet eine gemeinsame Schnittstelle. Auf Ports (COM oder LPT) kann man Daten ausgeben, Daten lesen (vielleicht) und irgendetwas einstellen.

Im Laufe der Entwicklung hat Microsoft aber entschieden, dass bspw. parallele und serielle Schnittstellen doch etwas unterschiedlich sind (wer wäre nicht vorher drauf gekommen?), und hat neben den Geräteklassen (nicht unterhalb der Geräteklassen!) die Interfaces eingeführt. Diese haben andere GUIDs und benötigen z.T. auch andere Zugriffsfunktionen.

Die Unterstützung des Interface-Bezugs beginnt mit Windows 98 (IMHO Zweite Ausgabe), ist dort noch etwas hakelig (funktioniert bspw. bei seriellen Schnittstellen nicht), und ist ab Windows 2000 einigermaßen vollständig implementiert.

Die hauptsächliche Anwendung ist das Finden und Ansprechen von USB-Geräten, für die es für jede USB-Geräteklasse (nach USB-IF-Spezifikation) ein eigenes Interface mit eigener GUID gibt.

Um mit Interfaces zu arbeiten, ist beim Aufruf von SetupDiGetClassDevs() das Flag DIGCF_DEVICEINTERFACE anzugeben, und es sind statt SetupDiEnumDeviceInfo() folgende Funktionen zu verwenden:

Unter Windows 95 und Windows NT 4 sind diese Funktionen nicht in der setupapi.dll enthalten!

CfgMgr-Funktionen

Die Konfigurations-Manager-Funktionen sind eine recht alte Erfindung. Es gab sie bei den ersten VxDs im Kernel Mode sowie eine 16-Bit-Implementierung unter Windows 95. Vermutlich würde niemand die o.g. SetupDi-Funktionen benötigen, wäre hier alles sauber dokumentiert. Microsoft hat anscheinend entschieden, diese Unterdokumentierung beizubehalten. So als ob diese aussterben sollen. Dann ist es trotzdem verwunderlich, dass man sie in vielen Situationen zwingend benötigt.

Die Funktionsnamen beginnen mit CM_ und bestehen allesamt aus mit Unterstrichen zusammengesetzten Abkürzungen: Für Windows eher untypisch, war aber so ganz typisch für VxDs.

Es gibt sie ab Windows 95 sowie Windows NT 4 in einer 32-Bit-DLL namens cfgmgr32.dll.

Ab Windows 2000 sind die Funktionen in der setupapi.dll implementiert, und die noch vorhandene cfgmgr32.dll leitet die Aufrufe „älterer“ Programme einfach durch. Dies ist auch bei den 64-Bit-Windows-Versionen und -DLLs so, obwohl es da gar keine „alten“ Programme geben kann.

Einige häufiger benutzte Funktionen sind:

Rückrufe (Callbacks)

Um mitzubekommen, dass neue Geräte dazugekommen sind oder etwas entfernt wurde, genügt es, den Rundruf WM_DEVICECHANGE mitzubekommen und dann die Neuauflistung von Geräten zu bewerkstelligen. Da bisweilen mehrere dieser Rundrufe vorbeikommen, ist das Verwenden eines Windows-Timers (SetTimer()) sinnvoll, damit die Auflistung verzögert erfolgt, vor allem, wenn die Auflistung länger dauert. (SetTimer() setzt vorherige SetTimer()-Aufrufe mit gleicher ID zurück!) 2 Sekunden sind ein gutes Maß.

Beispiel

Das ist natürlich ein Programmschnipsel zur Auflistung aller COM-Schnittstellen. Im Internet findet man auch viel Unsinn. Auf bis zu 256 COM-Ports CreateFile() anwenden dauert zu lange. Dieses hier läuft ab Windows 98 (zumindest Zweite Ausgabe). Hier klicken ⇒

#include <setupapi.h>
  …

  case WM_DEVICECHANGE: {
   HWND hCombo;
   int i;
   hCombo=GetDlgItem(Wnd,ID_MEINER_COMBOBOX);
   ComboBox_ResetContent(hCombo);
   HDEVINFO devs;
   devs=SetupDiGetClassDevs(&GUID_DEVCLASS_PORTS,0,0,DIGCF_PRESENT);
// devs=SetupDiGetClassDevs(&GUID_DEVINTERFACE_COMPORT,0,0,DIGCF_PRESENT|DIGCF_DEVICEINTERFACE); funktioniert nicht unter Win98!
   if (devs!=INVALID_HANDLE_VALUE) {
    SP_DEVINFO_DATA devinfo;
    devinfo.cbSize=sizeof devinfo;
    for (i=0; SetupDiEnumDeviceInfo(devs,i,&devinfo); i++) {
     TCHAR s[16];
     DWORD size=sizeof s;
     int k;
     HKEY hKey;
     if ((hKey=SetupDiOpenDevRegKey(devs,&devInfo,DICS_FLAG_GLOBAL,0,DIREG_DEV,KEY_READ))
        ==INVALID_HANDLE_VALUE) continue;
     if (!RegQueryValueEx(hKey,TEXT("PortName"),NULL,NULL,(LPBYTE)s,&size)
     && _stscanf(s,TEXT("COM%d"),&k)==1) {	// LPTs herauswerfen
      int idx=ComboBox_AddString(hCombo,s);	// PortName-Wert, so wie er ist
      ComboBox_SetItemData(hCombo,idx,k);	// numerischen Wert extra setzen, erspart späteres sscanf()
      if (k==MEINE_GERADE_AKTIVE_SCHNITTSTELLE) ComboBox_SetCurSel(hCombo,idx);
     }
     RegCloseKey(hKey);
    }
    SetupDiDestroyDeviceInfoList(devs);
   }
  }break;

Der Kode kommt ohne Abhängigkeit zu cfgmgr32.dll aus. Das vermeidet etwaige Probleme unter Windows 98.

Es sollte sogar unter Windows NT 4 laufen. (Nur SetupDiOpenDevRegKey() habe ich nicht getestet.) Obwohl NT 4 gar kein richtiges Plug-and-Play-System hat. Die Eintrittspunkte sind auch unter Windows 95 verfügbar; allerdings liefert die Funktion SetupDiEnumDeviceInfo() FALSE, und GetLastError() liefert 0x103 == ERROR_NO_MORE_ITEMS.

Ressourcen - ein ewiges Drama mit MSVC

MSVC enthält eher einen RC-Zerstörer als einen -Editor.

Verglichen mit Borland Resource Workshop, den es aber nur bis BC5 gab. Den Quelltext kann man jedenfalls nach Angriff mit MSVC keinem Menschen anbieten.

Deshalb ein AWK-Skript, diese Dateien wieder menschenlesbar zu machen. Für meine Web-Seiten, und für kleinere .RC-Dateien: nice_rc.zip

Mehrsprachen-Ressourcen scheinen nie ordentlich zu funktionieren. So beschweren sich Amerikaner, dass meine zweisprachigen (englisch+deutsch) Programme selbst unter XP überraschend in Deutsch erscheinen. Ich habe hierzu keine gute Lösung. Vielleicht Windows Commander zum Vorbild nehmen und nur Texte übersetzen? Das Problem hierbei sind Dialoge im Rechts-nach-Links-Stil (arabisch).

Das Problem hat sich inzwischen gelegt, bei mir funktioniert die automatische Sprachselektion immer korrekt. Viele meiner neueren Programme sind nunmehr zweisprachig ohne viel Federlesen.

Manifest-Dateien und -Ressourcen

Marx hat das Manifest erfunden, dachte ich so(?) – und Microsoft erfindet es einfach noch einmal. In Form von verball­horntem XML.

Damit hat es Microsoft wieder mal geschafft, gestandene Programmierer so richtig zu ärgern. Und alte Programme alt aussehen zu lassen. Wie damals schon beim Übergang von 16-bit-​Programmen nach Windows95. Als Heer­scharen von Programmieren damit beschäftigt waren, OFN_​LONGNAMES – äh, 0x200000L – in die Flags bei GetOpenFileName nachzureichen. Und DS_​3DLOOK – äh, 0x0004 – in jede Dialog­resource. Und so weiter.

Für alle, die nicht wissen, worum es geht: Gewöhn­liche, nicht speziell für Windows XP geschrie­bene Pro­gramme bekommen bei akti­vierter „Tele­tubbie-​Optik“ zwar einen schicken Fenster­rahmen (über Geschmack lässt sich streiten), aber das Dialog-​Innere sieht altbacken aus. Es sei denn, man legt neben die Echse eine „dateiname.exe.​manifest“-​Datei mit ma­gi­schem Inhalt. Oder steckt diese in die Ressource: Typ: RT_​MANIFEST = 24 (nicht als Text stehen lassen!), ID: CREATEPROCESS_​MANIFEST_​RESOURCE_​ID = 1. Womit die Echse zwangs­läufig größer wird. Eigent­lich nur für ein ein­ziges Bit: „Mach mich schick.“

Zeile in .RC-Datei

1 24 .manifest

Der Inhalt der Manifest-Datei ist immer der gleiche:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<dependency>
 <dependentAssembly>
  <assemblyIdentity
   type="win32"
   name="Microsoft.Windows.Common-Controls"
   version="6.0.0.0"
   processorArchitecture="*"
   publicKeyToken="6595b64144ccf1df"
   language="*"
  />
 </dependentAssembly>
</dependency>
</assembly>
Den sonst üblichen Programmnamen und die Programmbeschreibung kann man weglassen. Mit processorArchitecture="*" hält man sich 64-bit-Probleme vom Hals.

Was auch immer Microsoft mit Manifest-Dateien eigentlich wollte (irgend­was mit C# und Microsoft Forms, ich habe es nicht verstanden), ich stecke jetzt immer eine Manifest-Datei ins Projekt und stecke sie in die Res­sour­ce. Damit sind die Echsen etwas größer, sehen aber modern aus. Für je­man­den, der Tele­tubbie-​Optik nie eingeschaltet hat (oder wie ich Win­dows 2000 oder noch äl­ter be­nutzt), nur sinn­lose Luft. Be­son­ders un­ver­ständ­lich für mich ist die Aus­wir­kung der Name­space-​Ele­mente (xmlns). Kann man den Quatsch viel­leicht weg­las­sen, oder spinnt dann ir­gend­eins der Mi­cro­soft-​Be­triebs­sys­te­me?

Besonderheiten ab Vista

Ab Windows Vista und 7 sind noch weitere must-have-Einträge hinzu gekommen. Macht zusammen 1 Kilo Manifest trotz dieser dichten Packung. Für gesittete Programme (die keine Admin-Rechte benötigen) ist am besten folgendes (habe ich teilweise vom Total Commander abgeguckt):
<?xml version="1.0" encoding="UTF-8" standalone="yes"?><assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v2"><security><requestedPrivileges><requestedExecutionLevel level="asInvoker" uiAccess="false"/></requestedPrivileges></security></trustInfo>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1"><application><supportedOS Id="{e2011457-1546-43c5-a5fe-008deee3d3f0}"/><supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}"/></application></compatibility>
<asmv3:application xmlns:asmv3="urn:schemas-microsoft-com:asm.v3"><asmv3:windowsSettings xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings"><dpiAware>true</dpiAware></asmv3:windowsSettings></asmv3:application>
<dependency><dependentAssembly><assemblyIdentity type="win32" name="Microsoft.Windows.Common-Controls" version="6.0.0.0" processorArchitecture="*" publicKeyToken="6595b64144ccf1df" language="*" /></dependentAssembly></dependency></assembly>
Manifest-Ressource, auch zum nachträglichen Einbau in Programme, die unscharf aussehen; Anleitung für MSVC6: Diese Maßnahme funktioniert gegenüber dem Windows-Rezept (Rechte Maustaste auf EXE-Datei, Eigenschaften, Hohe DPI-Einstellungen usw.) auch dann, wenn die Echse über eine Verknüpfung (.lnk oder Dateiverknüpfung) geöffnet wird. Etwa bei der Keil-µVision-IDE uv2.exe und der verknüpften Projektdatei *.uv2.

Der beliebte Registry-​Schlüssel HKEY_​LOCAL_​MACHINE ist praktisch tabu. Dieser Schlüssel wird für Programme mit normalen Benutzerrechten weg-​virtualisiert, und die Daten gelangen an eine unschöne, schwer auf­zu­findende Stelle. Für Ordnung in der Registrierung sollte man daher alle Ein­stellungen schön brav nach HKEY_​CURRENT_​USER speichern. Maschinen­spezifische Einstellungen wie etwa die Port­adresse einer Schnitt­stellen­karte lassen sich faktisch nur sehr umständ­lich (in einer INI-Datei; das Programme-Verzeichnis kann aber schreib­geschützt sein) speichern, für Ordnung und Einfachheit tue ich's ebenfalls zu HKEY_CURRENT_USER.
Diese Einstellungen sind „umziehend“, was bspw. für Portadressen Quatsch ist. Hier hilft eventuell das (missbräuchliche?) Speichern nach HKEY_CLASSES_ROOT als benutzer­spezifische, aber nicht „umziehende“ Daten.

Manifest-Ressourcen für DLLs, genauer: Eigenschaftsseiten-Lieferanten („Property Sheet Provider“)

Im konkreten Fall ging es darum, eine Erweiterung für MMC.EXE, genauer, dem Gerätemanager (Aufruf einfach mit devmgmt.msc) modern aussehen zu lassen. Laut Microsoft-Dokumentation muss man wie folgt vorgehen: Lässt man auch nur einen Schritt weg, bleibt alles altbacken. Warum eigentlich dieses Mehr an Aufwand? Ist mir total unverständlich.

[Screenshot] [Screenshot]
Alt und neu zusammen in einem Fenster sieht eben nicht toll aus! Man beachte vor allem die unterschiedlichen Knöpfe

Das ISOLATION_AWARE_ENABLED bewirkt ein Aufblähen des Kodes und den Zwang zur Laufzeitbibliothek! Dies wird seitens Microsoft über *.inl-Dateien erreicht, das sind Kopfdateien mit einer Menge Inline-Code. Dieser Inline-Code ist absichtlich kryptisch gehalten und damit kaum lesbar! Außerdem werden so unsinnige Sachen gemacht wie UNICOWS.DLL zu laden.
UNICOWS.DLL ist die Hauptdatei für Unicode-Unterstützung unter Windows 98/Me, aber diese Systeme haben mit Manifest-Ressourcen eh' nichts am Hut!

Der Schalter ISOLATION_AWARE_ENABLED ist also für mich völlig inakzeptabel, und so habe ich mich so lange durch die kryptischen *.inl-Dateien gewurstelt, bis nur noch der wirklich benötigte Kode übrig blieb. Das ist nun erfreulich wenig.

Einschalten der Teletubbie-Optik

Irgendwie braucht man dazu immer einen Aktivierungskontext hActCtx.
Beschaffen des Aktivierungskontextes
HANDLE hActCtx;		// „Aktivierungskontext“, das Ding mit der Teletubbie-Optik

BOOL APIENTRY _DllMainCRTStartup(HANDLE hModule, DWORD reason, LPVOID lpReserved) {
 switch (reason) {
  case DLL_PROCESS_ATTACH: {
   ACTIVATION_CONTEXT_BASIC_INFORMATION actCtxBasicInfo;
   BOOL (WINAPI*pQueryActCtxW)(DWORD,HANDLE,PVOID,ULONG,PVOID,SIZE_T,SIZE_T*);
   HANDLE hKernel;
   hInst=(HINSTANCE)hModule;
   DisableThreadLibraryCalls(hModule);
   InitCommonControls();
   hKernel=GetModuleHandle(T("KERNEL32.dll"));
   (FARPROC)pQueryActCtxW=GetProcAddress(hKernel,"QueryActCtxW");
   if (pQueryActCtxW
   && pQueryActCtxW(	// ziemlich undokumentiert
     QUERY_ACTCTX_FLAG_ACTCTX_IS_ADDRESS|QUERY_ACTCTX_FLAG_NO_ADDREF,
     &hActCtx,		// irgendeine Adresse
     NULL,
     ActivationContextBasicInformation,
     &actCtxBasicInfo,
     sizeof(actCtxBasicInfo),
     NULL)
   && actCtxBasicInfo.hActCtx!=INVALID_HANDLE_VALUE)
     hActCtx=actCtxBasicInfo.hActCtx;	// <hActCtx> nie freigeben, da ohne AddRef!
  }break;
 }
 return TRUE;
}
Der dynamische Einsprungpunkt via GetProcAddress() wird benötigt, damit die DLL auch unter Windows 2000 ladbar ist.
Der Trick bei Win16, mit UndefDynLink zur Laufzeit zur vergleichen, klappt bei Win32 nicht.
Wo (und ob?) InitCommonControls() aufgerufen wird, ist irrelevant. Beim Debuggen sieht man, dass InitCommonControls() zu nichts weiter als einem ret-Befehl führt.

Bei einer 64-bit-DLL kann man sich das Beschaffen des Einsprungpunktes zur Laufzeit ersparen, da es 64-bit-Editionen von Windows erst ab Windows XP gibt.
(Ja, gibt es! Wenn auch nur in englisch. Die meisten kennen das erst ab Windows Vista.)

Aktivieren der Teletubbie-Optik
Normalerweise kann man die Aktivierung mittels ActivateActCtx() während des Programmlaufs bewirken. Das klappt aber nicht mit Eigenschaftsseiten („Property Sheets“)! Da verreckt irgendwo MMC.exe, der Gerätemanager.

Eigenschaftsseiten verlangen ein besonderes Vorgehen:

 HPROPSHEETPAGE hpage;
 PROPSHEETPAGE page;
 …				// die normale Initialisierung
 page.dwFlags = … |PSP_USEFUSIONCONTEXT;
 page.hActCtx = hActCtx;	// Die erweiterte PROPSHEETPAGE-Struktur steckt in der Kopfdatei des XP-SDKs oder -DDKs
 hpage = CreatePropertySheetPage(&page);
Die übrigen Fenster, wie Unter-Dialoge oder Messageboxen bekommen automatisch Teletubbie-Optik, da braucht man sich nicht mehr zu kümmern. Keine Wrapper mit ActivateActCtx() und DeactivateActCtx(). Und, das ist sehr nett, keine weiteren dynamischen Einsprungpunkte.

Zu bedenken ist, dass das Hervorheben bestimmter Dialogelemente durch Farbe, insbesondere bei Checkboxen, nicht mehr mittels SetTextColor() innerhalb von WM_CTLCOLORSTATIC möglich ist. Die einfachste Lösung ist es, derartige Dialogelemente mittels SetWindowTheme() in ihr altes Aussehen zurückzuverwandeln.

Inzwischen ist mir der Sinn von Manifests (zumindest der <dependency>-Abschnitt) etwas klar geworden:

Gleitkomma-Operationen auf AMD64-Architektur

(Betrifft nur Programme, bei denen nicht einmal der Bezug zur msvcrt.dll erwünscht ist.)

Bevor das Drama richtig losgeht, erst mal eine Zusammenfassung der Möglichkeiten eines 32-Bit-Compilers.

Die Situation beim 32-Bit-Compiler

MSVC ist in der Lage, bestimmte Gleitkommafunktionen direkt („intrinsisch“) als x87-Opcode einzukompilieren.

Beim 64-Bit-Compiler

Hier ist die Situation derart, dass Microsoft sich entschieden hat, komplett auf MMX und x87 zu verzichten! So als ob künftige x86-64-Prozessoren nur noch SSE2 und aufwärts haben. (Womöglich haben sich Intel und Microsoft darauf geeinigt.) Das führt soweit, dass sich der Assembler ml64.exe weigert, irgendeinen MMX- oder x87-Befehl zu übersetzen. Und der 64-Bit-C-Compiler hat sowieso keinen Inline-Assembler mehr.

Da SSE2 keine transzendenten Funktionen anbietet, führen alle diese Funktionen zur Laufzeitbibliothek, die das Ergebnis trickreich ausschließlich per SSE2-Befehlssatz berechnet.

Trotzdem ist es möglich, derartige Funktionen mit der x87-Einheit ausrechnen zu lassen. Um den betreffenden Kode zu generieren, benötigt man einen alternativen Assembler. Ich habe kurzerhand YASM, ein Ableger von NASM ausprobiert.

Und so sieht eine selbstgeschriebene Sinusfunktion aus, die von C aus aufrufbar ist:

sin:	movq	rax,xmm0	;Der double-Parameter kommt in xmm0; dieser muss erst mal in den x87-Stapel
	push	rax		;Dazu muss der Operand speicheradressierbar sein, da bietet sich der Stack an
	fld	qword[rsp]	;Nun kann st(0) mit dem Winkelargument geladen werden
	fsin			;Die eigentliche Berechnung (fwait braucht man nicht mehr)
	fstp	qword[rsp]	;Jetzt der ganze Spaß in der anderen Richtung: Auf den Stack
	pop	rax		;runter vom Stack
	movq	xmm0,rax	;wieder ins SSE2-Register
	ret			;fertig

All die War­nun­gen, dass der x87-​Re­gis­ter­satz Kon­text­wech­sel nicht über­lebt, kann man vor­erst ge­trost ig­no­rie­ren. Denn das stimmt schlicht­weg nicht. Zu­min­dest im User Mode. Denn da­mit 32-​Bit-​Pro­gram­me un­ter 64-​Bit-​Win­dows lau­fen, muss Win­dows den x87-​Zu­stand beim Kon­text­wech­sel si­chern. Da wür­de es kaum nut­zen, wenn man das nur für 64-​Bit-​Pro­zes­se nicht tä­te. Erst wenn 32-​Bit-​Pro­gram­me aus­ge­stor­ben sind (schät­zungs­wei­se im Jahr 2030), könn­te sich das än­dern, aber wo­mög­lich tau­chen vor­her schon x64-​Pro­zes­so­ren oh­ne x87 und oh­ne 32-​Bit-​Kom­pa­ti­bi­li­täts­mo­dus auf.

Die in jeder Hilfe fehlende Liste von Ersetzungsroutinen

Für die Konstanten _USE_MATH_DEFINES benutzen! (Nicht unter MSVC6 verfügbar.)

HäufigKonstantenwert
10xexp(x*M_LN10)
pow(10,x)
2.30258509299404568402
2xexp(x*M_LN2)
pow(2,x)
0.693147180559945309417
log2 xlog(x)/M_LN2
Seltener, etwa bei selbst geschriebenen Gleitkomma-Routinen via 80x87
log10 xlog(x)/M_LN102.30258509299404568402
arcsin x_copysign(atan(sqrt(x*x/(1-x*x))),x)
sinh x(exp(x)-exp(-x))*0.5
cosh x(exp(x)+exp(-x))*0.5
arsinh xln(x+sqrt(x*x-1))
arcosh xln(x+sqrt(x*x+1))

Weitere Operatoren für Gleitkommazahlen

Der C-Standard unterstützt die Operatoren <<, >> sowie % und ^ nicht. Warum eigentlich nicht? In der Standardbibliothek sind entsprechende Funktionen ja bereits vorhanden. Eine kleine Hilfsklasse schafft Abhilfe in C++:
struct Double{
  Double(double v) : val(v)		{}
  operator double()		  const	{return val;}
  Double& operator <<=	(int exp)	{val=ldexp(val,exp); return *this;}
  Double  operator <<	(int exp) const	{Double o(*this); return o<<=exp;}
  Double& operator >>=	(int exp)	{return *this<<=-exp;}
  Double  operator >>	(int exp) const	{return *this<<-exp;}
  Double& operator %=	(double n)	{val=fmod(val,n); return *this;}
  Double  operator %	(double n)const	{Double o(*this); return o%=n;}
  Double& operator ^=	(double n)	{val=pow(val,n); return *this;}
  double  operator ^	(double n)const	{return pow(val,n);}
  double  operator &	(double n)const	{return hypot(val,n);}		// Betrag des Vektors
  Double  operator |	(Double n)const	{return ~(~*this+~n);}			// Parallelschaltung (von Widerständen)
  Double  operator ~	()	  const	{return Double(1/val);}			// Inversion
private: double val;
};
Schade, dass C++ nicht das Überladen undefinierter Operatoren für double (klein!) unterstützt.

Unicode unter Windows 98

Standardmäßig unterstützt Windows 98 (gleiches gilt für Windows 95 und Windows Me) nur einige wenige Unicode-Funktionen: Es reicht immerhin, um Unicode-Sonderzeichen zu malen. Steuerelemente mit Unicode sind hingegen eher aufwändig, am ehesten geht es mittels CustomDraw- bzw. OwnerDraw-Malereien. Die Titelzeile eines Fensters sowie Tooltips sind für erweiterte Unicode-Zeichen jedoch eher taub; da müsste man alles selber malen. (Tooltips selber machen ist nicht allzu schwierig.) Inwiefern KernelEx das Problem löst habe ich nicht mehr nachvollzogen.

Das Microsoft-Paket unicows hilft da nicht wirklich weiter, denn:

Fehlt jetzt eigentlich nur noch die Funktion LoadStringW, um eine Zeichenkette aus der ohnehin in Unicode (genauer: UTF-16) vorliegenden String-Ressource zu laden:
int MyLoadStringW(HINSTANCE hInst, UINT id, PWSTR buf, int len) {
 HRSRC r=FindResource(hInst,MAKEINTRESOURCE((id>>4)+1),RT_STRING);
 PCWSTR p=(PCWSTR)LoadResource(hInst,r);
 if (!p) return 0;
 for (id&=15; id; id--) {
  p+=*p+1;
 }
 len--;
 if (len<0) return *p+1;	// Allokationsgröße in Zeichen
 if (len>*p) len=*p;
 p++;
 __movsw(buf,p,len);	// inklusive etwaiger Nullen!
 buf[len]=0;
 return len;
}
Die eigene Funktion kann man nicht LoadStringW nennen, deshalb der Präfix My. __movsw ist ein intrinsischer Befehl ab Visual Studio 2003. Bei MSVC 6 geht da ersatzweise:
__forceinline void __movsw(void*d, const void*s, DWORD count) {
 _asm	mov	edi,d
 _asm	mov	esi,s
 _asm	mov	ecx,count
 _asm	rep movsw
}
Inwieweit man das Problem unter Vorhandensein von KernelEx überhaupt angehen muss, muss ich noch untersuchen. Womöglich erübrigt sich ja das Erstellen spezieller Win9x-Kompilate …

OEM-Zeichensatz

Bisweilen wünscht man sich das Ausgeben von IBM-Rahmensymbolen auf die Konsole. Aus der Ressource, wenn's geht. Dazu benutzt man niemals die Funktion AnsiToOem() (außer für Dateinamen von der Kommandozeile), sondern verwendet ausschließlich OEM-Strings in seinem ANSI-Programm. Nun muss man nur noch OEM-Strings aus der Ressource laden:
int LoadStringOem(HINSTANCE hInst, UINT id, PSTR buf, int len) {
 HRSRC r=FindResource(hInst,MAKEINTRESOURCE((id>>4)+1),RT_STRING);
 PCWSTR p=(PCWSTR)LoadResource(hInst,r);
 if (!p) return 0;
 for (id&=15; id; id--) {
  p+=*p+1;
 }
 len=WideCharToMultiByte(CP_OEMCP,0,p+1,*p,buf,len-1,NULL,NULL);
 buf[len]=0;
 return len;
}

Sieht logischerweise so ähnlich aus wie die MyLoadStringW()-Routine oben.

Die Ressourcendatei selbst muss in UTF-16 abgespeichert werden und kann nur mit RC.EXE ab Visual Studio 2005 compiliert werden; MSVC6 geht hierfür nicht.

Sollten auf dem Zielsystem keine Rahmensymbole zur Verfügung stehen wandelt WideCharToMultiByte() diese in optisch entsprechende ASCII-Zeichen um. Daher braucht man beim Export eines solchen Programms in exotische Länder keine Angst zu haben.

CHM-Hilfe

Verschoben nach mein_chm.htm.