VS-Tipps & Tricks: Springe zur nächsten Klammer funktioniert auch für #if, #elif, #else und #endif

Wer sich schon durch die Windows Header gekämpft hat um herauszufinden warum welche Definition einer Struktur oder Funktion in irgend einer Windows Version so oder gar nicht vorhanden ist, der weiß auch wie einem #if, #elif, #else und #endif das Leben schwer machen können, was die Orientierung betrifft.

Netterweise hilft einem eine Funktion, die man nur von Blöcken und verschachtelten Funktionen her kennt Strg+´ (Edit.GotoBrace). Wichtig! Man darf nicht auf der Variable oder Bedingung stehen, sondern muss auf dem Schlüsselwort stehen.

Wenn man auf einer Präprozessor Direktive kann man mit den Tasten die einem zur passenden Klammer bringt zur nachfolgenden Direktive. Und mit dem Festhalten der Umschalttaste kann man den entsprechenden Block auch markieren.

AfxMessageBox versus CWnd::MessageBox

Jeder MFC Entwickler kennt AfxMessageBox. In der Klasse CWnd finden wir auch die Memberfunktion CWnd::MessageBox.
Mancher Entwickler wird sich nun fragen: Wann nehme ich denn nun was?

Kurze Antwort: Meide CWnd::MessageBox und nutze immer AfxMessageBox

Lange Antwort:
CWnd::MessageBox ist einfach nur ein direkter Wrapper für Windows API Funktion MessageBox.
Der Grund warum man AfxMessageBox verwenden sollte liegt darin was AfxMessageBox einfach noch mehr leistet:

  1. AfxMessageBox benutzt die virtuelle Funktion CWinApp::DoMessageBox. Damit kann man zentral eine andere Behandlung für Fehlermeldungen einbauen.
  2. AfxMessageBox sorgt dafür, dass OLE Controls benachrichtigt werden, dass Ihre nicht modalen Dialoge nun deaktiviert werden müssen. Es wäre ja ziemlich heftig, wenn man solche Dialoge trotz aktiver MessageBox noch benutzen könnte. (siehe CWinApp::DoEnableModeless und Implementierung von CWnd::GetSafeOwner)
  3. CWnd::MessageBox benutzt das aktuelle Fensterhandle aus dem es aufgerufen wird als Parent-Handle für die API Funktion MessageBox. Die wenigsten Entwickler kennen, lesen oder beachten den netten Zusatz in der MessageBox Doku:
    If you create a message box while a dialog box is present, use the handle of the dialog box as the hWnd parameter. The hWnd parameter should not identify a child window, such as a control in a dialog box.
    Es ist also gerade zulässig CWnd::MessageBox aufzurufen, denn oft genug haben wir es mit Child-Windows zu tun.
    Netterweise beachtet AfxMessageBox genau das. Es ermittelt das aktuelle aktive Top-Level Fenster und setzt es intern für den Aufruf von MessageBox.
    Das wird auch ganz besonders wichtig, wenn man evtl. mehrere UI Threads hat. AfxMessageBox verwendet automatisch den richtigen. CWnd::MessageBox verwendet den aktuellen Thread aber evtl. als Fenster das Fenster-Handle aus einem anderen Threads.
  4. CWnd:MessageBox hat keine Implementierung für die F1-Taste und die Hilfeimplementierung der MFC.
  5. Ich muss mich nicht um den Titel der MessageBox kümmern, denn AfxMessageBox benutzt den hinterlegten Applikationsnamen (CWinApp::m_pszAppName) der in der MFC hinterlegt und definiert wurde.
  6. Netterweise setzt AfxMessageBox passende Icons wenn nur die Reaktionsform definiert wird, also kein Icon. (MB_OK und MB_OKCANCEL benutzt MB_ICONEXCLAMATION, MB_YESNO und MB_YESNOCANCEL benutzt MB_ICONQUESTION).

HTH

Probleme bei der CTime Serialisierung mit MFC 7.1 und MFC 8.0

Der operator<< und operator>> in MFC 7.1 serialisiert grundsätzlich nur 32bit Werte des CTime. Das wurde offensichtlich gemacht um rückwärtskompatibel zu sein zu time_t obwohl mittlerweile ja intern __time64_t verwendet wird.
Gleichzeitig wurde Serialize64 eingeführt.
Man muss also in VS2003 mit MFC 7.1 Serialize64 verwenden um alle 64bit zu speichern, also den komplettem Wertebereich von CTime.

In VS2005 mit der MFC 8.0 ging man jetzt einen Schritt weiter. Die Operatoren operator<< und operator>> sollten auch 64bit verarbeiten können. Man wollte 64bit speichern und 32bit Daten beim Einlesen erkennen wenn es eben aus alten Dateien stammt und nicht mit der neuen MFC geschrieben wurde.

Dazu wird bei Speicherung von 64bit CTime über den operator<< zuerst ein DWORD gespeichert mit dem Wert cTime64Mark (der Wert ist INT_MIN+10). Danach der 64bit Wert.
Beim Lesen wird zuerst ein DWORD gelesen. Ist dieses nicht cTime64Mark (also INT_MIN+10), dann wird dieser Wert als 32bit CTime Wert gelesen.
Ansonsten werden die 64bit in den CTime Wert gelesen.

operator>> bläht also auch die Ausgabe um weitere 32bit auf. Ein 64bit CTime Wert wird in der MFC 8.0 als 32+64bit Wert gespeichert, also 4Bytes mehr.

Speicherst man mit MFC 7.1 und dem operator<<, dann kannst man die Daten in MFC 7.1 und MFC 8.0 mit dem operator>> lesen, denn der Wert wird wegen des fehlenden Leadin-Bytes als 32bit Wert in der MFC 8.0 erkannt.
Speicherst man aber mit MFC 8.0 und dem operator<< dann ist das für MFC 8.0 kompatibel. Und kannst es in MFC 8.0 mit dem operator>> lesen. Allerdings speichert – wie schon gesagt – MFC 8.0 zuerst ein DWORD (das Leadin für 64bit) und dann den 64bit Wert.
Das versteht nun MFC 7.1 gar nicht, denn das liest immer nur ein DWORD mit dem operator>>.

❗ Ein Problem liegt also im Schreiben unter MFC 8.0 mit dem operator<< und im Lesen der Daten mit einem Programm das die MFC 7.1 verwendet und den operator>>!

❓ Was kann man machen um komplett kompatibel zu sein?

  • Würde in beiden Programmen Serialize64 verwendet gibt es kein Problem!
    Wichtig Serialize64 verwendet kein Leadin weder unter MFC 7.1 noch unter MFC 8.0!
  • Kompatibel zu beiden Versionen von MFC 7.1 und MFC 8.0 ist man auch wenn in der MFC 7.1 und in der MFC 8.0 immer nur ein DWORD gespeichert wird (das kann ich aber wirklich nicht mehr empfehelen). Unter MFC 8.0 muss das manuell gemacht werden.
  • Wenn man also Code-kompatibel (nicht binärkomaptibel) für alle MFC Versionen ab 7.0 bleiben will sollte man immer Serialize64 verwenden was einem auch ein kompakteres Format beim Speichern der Daten garantiert.

Eigentümlich ist, dass dies nicht als Breaking Change dokumentiert ist.
Oder ich habe es (wie so manches) einfach überlesen, aber hier steht es nicht:
ATL and MFC Changes: ATL 7.1 and MFC 7.1
ATL and MFC Changes: ATL 7.0 and MFC 7.0

PS: Ich habe CTime Aufgrund der vielen Unzulänglichkeiten nie verwendet. Mein Favorit ist seit eh und je COleDateTime bzw. DATE und die dazugehörige COleDateTimeSpan Klasse. Diese ist wirklich einfach in der Kalkulation und war durch das Design nie von einem begrenzten Wertebereich betroffen.

Bug in der MFC71.DLL bzgl. CPropertySheet/CPropertyPage

Ich habe ein Projekt, dass einen eigenen MFC Build verwendet. Dieser basiert zu 99,999% auf dem MFC71 SP1 Build aus VC-2003.NET. Das dies ein eigener Build ist spielt für den Bug allerdings keine Rolle, allerdings evtl. für die Lösung.

Zu den Rahmenbedingungen. Man baut eine EXE/DLL mit folgenden Eigenschaften:

  • _WIN32_IE< =0x0500.
  • Verwendet die DLL-Version der MFC (Use MFC in a Shared DLL)
  • Man verwendet CPropertyPage und CProprtySheet als Wizard.
  • Man verwendet Header-Titel und Header-Subtitel und damit einen der beiden nachfolgenden Konstruktoren:
    CPropertyPage(UINT nIDTemplate, UINT nIDCaption, UINT nIDHeaderTitle, UINT nIDHeaderSubTitle = 0, DWORD dwSize = sizeof(PROPSHEETPAGE));
    CPropertyPage(LPCTSTR lpszTemplateName, UINT nIDCaption, UINT nIDHeaderTitle, UINT nIDHeaderSubTitle = 0, DWORD dwSize = sizeof(PROPSHEETPAGE));

Die MFC71.DLL ist so gebaut, dass sie mit allen 3 Versionen der PROPSHEETPAGE Struktur klar kommen soll. Es gibt drei Versionen, die mit folgenden _WIN32_IE Definitionen unterschieden werden:

  • für WIN32_IE<0x0400, die aller erste Version die mit Windows 95 kam. Größe 40 Bytes.
  • für WIN32_IE>=0x0400 && _WIN32_WINNT<=0x0500, hier wurden Titel und Subtitel hinzugefügt (pszHeaderTitle, pszHeaderSubTitle). Größe 48 Bytes.
  • und zuletzt _WIN32_WINNT>=0x0501, hier kam noch ein Activation-Context dazu (hActCtx). Größe 52 Bytes.

Was passiert nun:

  • Einer der genannten Konstruktoren für die CPropertyPage wird aufgerufen.
  • Der letzte Parameter sizeof(PROPERTYPAGE) zeigt nun der DLL an welche Version das aufrufende Programm verwendet, damit die MFC71.DLL die entsprechenden Größen bereitstellt.
  • AllocPSP wird entsprechend aufgerufen und dynamisch diese PROPERTYPAGE allokiert.

Bis hierher ist alles prima, aber leider passiert es un, dass diese vordefinierte Größe in der Funktion CommonConstruct(LPCTSTR lpszTemplateName, UINT nIDCaption, UINT nIDHeaderTitle, UINT nIDHeaderSubTitle) überschrieben wird (Zeile 184+189 in dlgprop.cpp). Das passiert mit dem Statement:
m_psp.dwSize = sizeof(m_psp);
D.h. die PROPERTYPAGE Struktur ist mit weniger als 40 oder 48 Bytes allokiert. Im dwSize Member steht aber nun 52.
In der Funktion BuildPropPageArray() wird nun aus den einzelnen PROPERTYPAGE’s ein Array gebildet. Dabei werden jeweils dwSize-Bytes (also 52) aus den entsprechenden allokierten Bereichen kopiert. Aber Achtung, diese sind kleiner allokiert (40 oder 48 Bytes)
In der Debug Version ist das oft nicht tragisch, denn da kommen hinten dran noch einige Füllbytes. Auch in der Release Version kracht es nur manchmal. Genau nur dann, wenn einer der allokierten PROPERTYPAGE Strukturen genau auf mit einer allokierten Speicherseite endet und das 49ste Byte der Struktur eben nicht allokiert ist und die Speicherseite exakt hier endet.

Diese sehr üble Konstellation machte es mir extrem schwer diesen Fehler zu finden. Ich hasse es einfach optimierten Code zu debuggen 😉

Fix:

  • Keinen der obigen Konstruktoren verwenden und die Member m_strHeaderTitle und m_strHeaderSubTitle selbst füllen.
  • oder wie ich einen eigenen MFC Build erzeugen und verwenden. Die entsprechenden Code Zeilen 184+189 aus der dlgprop.cpp entfernen.

Hinweis:
Crashdumps zeigen meistens im Stacktrace die Funktionen BuildPropPageArray und memcpy!

BTW:
Dieser Bug betrifft sowohl die MFC71.DLL im RTM und im SP1.
In der MFC 8.0 des VS-2005 ist dieser Bug behoben.

Voraussetzungen für Remote debugging mit MSVCMON (unmanaged)

Ich bin ein Fan von Remote Debugging!
Aber was benötigt man minimal für Remote Debugging auf dem Target auf dem man debuggen möchte (unmanaged Code)?
Das gesamte Remote Debugging Paket zu installieren ist aufwendig und verändert das Zielsystem. Wenn man auch noch bei einem Kunden vor Ort ist auch natürlich ein Eingriff, den man einem Admin erst mal erklären muss.
Geht es also auch mit weniger? JA…

Für VS.NET 2003 braucht man nicht viel, genau genommen 4 Dateien mit einem Datenvolumen von nicht mal 600kb (passt auf jede Diskette, sofern die noch einer benutzt ;-)) :

  • msvcmon.exe
  • msvcr71.dll
  • NatDbgDM.dll
  • NatDbgTLNet.dll

Wenn man nun auf Firewall Probleme verzichtet und Named Pipes als Transportschicht verwendet dann war es das schon. Dann muss man nur noch MSVCMON auf dem Target starten und es kann losgehen. Seit VS.NET 2003 startet MSVCMON ohne Angabe von Parametern mit named Pipes als Transportschicht. Unter Vista muss man nach meinen Erfahrungen immer den -u Parameter mit angeben wenn man Named Pipes verwendet (siehe hier).

Das einfachste ist es nun sich mit VS.NET 2003 remote auf einen laufenden Prozess zu attachen, den man debuggen möchte. Man öffnet dazu aus dem Menü Debug den Punkt Processes, wählt als Transportschicht Named Pipes und den Target PC. Die Liste der Prozesse füllt sich automatisch.
Nun nur noch den Prozess auswählen und auf Attach klicken. Sofern die PDB-Dateien übereinstimmen kann man sofort seine Breakpoints setzen und loslegen.
Das geht teilweise sogar noch, wenn eine UAE Meldung auf dem Monitor sichtbar ist.

Wer unbedingt TCP/IP als Transportschicht wählen will, der muss seit XP-SP2 einiges an der Firewall einstellen. Die zwei nachfolgenden Links geben entsprechende Hinweise:
http://support.microsoft.com/kb/833977/de
http://support.microsoft.com/kb/841177/en-us