Warum hat CWnd keine virtual functions für Windows Messages?
Diese Frage mußte ich mir 1994 mal stellen lassen bei meinem allerersten Arbeitgeber als Entwickler, als es darum ging, verschiedene plattformunabhängige Windowing-Framework Class Libraries zu vergleichen und diese dann wiederum mit den MFC in punkto Leistungsfähigkeit zu vergleichen. Ich wußte darauf nur mit einem Achselzucken zu antworten und daß die MFC halt eben diese ominösen Message Maps verwenden um ein CWnd (genauer gesagt: ein CCmdTarget) auf eine Windows Message vom Typ WM_SCHLAGMICHTOT reagieren zu lassen. Aber warum denn nur? War es nicht ein Zeichen von Objektorientierung, daß die plattformunabhängigen Klassenbibliotheken ihrem CWnd-Pendant für diese Zwecke jeweils eine virtuelle Methode spendierten, die man in einer abgeleiteten Klasse nur redefinieren mußte um einen Message-Handler zu haben?
Und später, als in MFC die Property Sheets und Property Pages zu den MFC hinzukamen, warum haben die Leute im MFC-Team dann plötzlich die Kehrtwende eingeleitet und für jeden Pups, der über WM_NOTIFY an der Property Page ankam, eine virtuelle Methode eingeführt, statt die traditionellen Message Maps zu verwenden?
Die Antworten gebe ich jetzt erstmal nicht, aber wer eine Vermutung hat, wie man diese Fragen korrekt beantworten könnte, ist hiermit eingeladen, einen Kommentar abzugeben.
Projektabhängigkeiten in VS6 dsw-files darstellen
Projektabhängigkeiten in VC6 Workspace Files (dsw files) sind mitunter nicht so einfach zu erkennen. Wenn die Zahl der dsp files darin dann so in die Hunderte zu gehen scheint wie bei einem besonders prominenten dsw file bei επτ€σ, wird's Zeit, das Ganze aufzubrechen, so daß es einzelne Personen vielleicht wieder 'mal überblicken können. Drum war die Überlegung von SNB, Tommy de Markolf und mir, das Ganze erstmal graphisch überblicken zu wollen um dann den Split zu machen, wenn Muster von Abhängigkeiten zu erkennen sind und Redundanzen beseitigt sind. Ein erster Schritt dazu ist dieses Tool, mit dem man sich ein .dot file erzeugen kann wie folgt:
dswdep dswfile c:\temp\mydot.dot
Dieser Aufruf in einer Konsole erzeugt aus dem .dsw file "dswfile" ein dot-file c:\temp\mydot.dot.
Das File c:\temp\mydot.dot kann man dann dem tool dot.exe aus dem AT&T graphviz package wie folgt einfüttern:
dot -Tpng c:\temp\mydot.dot -o c:\temp\mydot.png
Damit wird nun ein png file unter c:\temp\mydot.png erzeugt, das die Abhängigkeiten der einzelnen dsp files im dsw file das dem dswdep tool als erstem Parameter übergeben wurde, sehr schön graphisch darstellt.
Für mein allseits immer wieder gern gesehenes SUperior SU wird damit beispielsweise folgende Graphik erzeugt (macht nur wirklich Sinn jetzt, auf das Bild zu klicken und in großer Auflösung zu betrachten, ehrlich!):
Da man ja bei so einem Projekt normalerweise immer ein "top-level" dsp hat, wie bei mir das "allbuild", macht es wahrscheinlich am meisten Sinn, die Grafik darzustellen ohne das top-level dsp file (denn das wird ja durch's Aufsplitten überflüssig), aber dazu bin ich jetzt zu müde, vielleicht liefere ich das hier noch nach.
Woher kommt eigentlich der Begriff "Booten"?
Wo ich schon beim schlaumeiern bin jetzt noch die Erklärung des Begriffs "Booten". Das kommt vom englischen "Bootstraps", also den "Stiefelriemen" oder "Stiefelschlaufen", nicht zu velwechsern mit den "Bootlaces", den Schnürsenkeln. Als ich noch ein junger (!) Mensch war und studierte, hatte ich den ehrenwerten Professor Baeger ohne ä und mit ae (den besten Prof, den ich überhaupt je hatte), der seine Studenten über die Etymologie (ετυμολογία für den Löselic) dieses Begriffs aufklärte, uns aber gleichzeitig anhielt, den deutschen Begriff "Urladeprogramm" statt "Bootstrapper" zu verwenden.
Was hat nun das "Urladeprogramm" oder das "Bootstrap Program" mit irgendwelchen Stiefelriemen zu tun? Ganz einfach: Im Deutschen gibt es ja die Sagen vom Münchhausen und eine dieser Sagen handelt davon, wie sich Münchhausen an den eigenen Haaren aus dem Sumpf zieht. Eine ähnliche Sage gibt es im Englischen Sprachraum (oder im Amerikanischen? SBryant to the rescue?), wo ein Männchen zum Mond will und deswegen immer wieder seine Stiefelriemen solange aneinander bindet, wieder aufbindet und wieder aneinander bindet und daran währenddessen hochklettert, bis es am Ende beim Mond angelangt ist. Die Wikipedia hingegen sagt schlicht dazu, daß in der amerikanischen Fassung des Münchhausen er sich an den Stiefelschlaufen aus dem Sumpf zieht. Was auch immer richtig ist, man bekommt jetzt vielleicht so eine Ahnung woher der Begriff kommt.
Grob vereinfacht ist also der Bootstrapper ein simples Programm, das sehr viel kompliziertere Dinge, wie etwa den Start eines so komplexen Programms wie eines Betriebssystems gestattet, indem es sich irgendwie magisch selbst hochzieht, oder jedenfalls so ähnlich. Mit der Zeit ist das Ganze dann vom "Bootstrapping Programm" zum "Bootstrapper" und schliesslich zum Verb "Booten" verballhornt worden und da ist es wo wir heute stehen.
Woher kommt eigentlich der Begriff "Logging"?
Heute war eine interessante Diskussion mit Kollegen in deren Verlauf wir auf's Thema Logging zu sprechen kamen. Meine Frage in die Runde, wer denn wisse, woher der Begriff "Logging" oder "Logfile" kommt, wurde mit einem Achselzucken und "Vielleicht vom Logbuch?" beantwortet. Das ist soweit ganz richtig, aber woher kommt denn nun das "Logbuch"? Ein "Log" ist im britischen Idiom ein "Holzscheit", ist also der Windows Eventlog Service etwa ein Fensterereignisholzscheitdienst?
Und weil ich ja bekanntermaßen nicht nur ein Zeitgenosse von überragender Allgemeinbildung bin, sondern auch noch beinahe F.D.P.-haft anmutende Profilierungssucht und messianisches Sendungsbewußtsein zu meinen Kernkompetenzen zähle, habe ich die Erklärung gleich nachgeliefert: In der christlichen Seefahrt gehörte neben der Positionsbestimmung die Messung der Geschwindigkeit eines Schiffes zu denjenigen Dingen, die ins Logbuch eingetragen wurden, so daß man in der Lage war, den eigenen Kurs zu halten. Nun, wie mißt ein Segelschiff im 18. Jahrhundert seine eigene Geschwindigkeit? Ganz einfach, man stellt sich an den Bug des Schiffes, wirft einen schwimmfähigen Gegenstand ins Wasser und stoppt die Zeit, die vergeht, bis dieser am Ende des Schiffes vorbeizieht. Aus der bekannten Länge des Schiffes kann man gemäß v = s/t die aktuelle Geschwindigkeit näherungsweise bestimmen. Dieser schwimmfähige Gegenstand war in aller Regel ein Holzscheit, von denen man zu diesem Zweck immer eine ausreichende Anzahl an Bord mitführte. Und das Zeitnormal konnte man schon sehr früh ausreichend genau bereitstellen, sodaß man mit diesem einfachen Verfahren sehr gut bestimmen konnte wieviel Fahrt das Schiff gerade macht.
Fun with Globals - Der Loader Lock
Wer mich kennt, weiß daß ich sehr schnell ballistisch werde, wenn ich irgendwo Code sehe, der nichtriviale globale Objekte enthält, oder Code einer DLL, deren DllMain ganz dumme Sachen macht. Anhand eines Beispiels möchte ich hier jetzt einmal ein Phänomen vorstellen, das als "Loader Lock" bekannt ist.
Das Beispiel besteht aus einer ganz trivialen Exe-Datei ("Loader") und einer DLL ("Locker"). Mit Hilfe von Aufrufen des APIs Sleep provoziere ich eine Race Condition, wie sie im wirklichen Leben auch auftreten kann. Und damit niemand glaubt, daß das ein konstruiertes Beispiel sei: Es ist mir im richtigen Leben erst kürzlich begegnet. Das komplette Projekt (VC6) kann man sich hier downloaden
Erstmal hier der Code in der DLL:
#include "stdafx.h" #include <tchar.h> #define USE_GLOBAL_OBJECT 1 static void CreateAndWaitEvent() { HANDLE hEvent = CreateEvent(NULL, FALSE, FALSE, _T("oohdelalllyeh!")); if(hEvent) { WaitForSingleObject(hEvent, INFINITE); CloseHandle(hEvent); } } #if USE_GLOBAL_OBJECT class CDoomed { public: CDoomed() { CreateAndWaitEvent(); } }; CDoomed g_Doomed; #endif BOOL APIENTRY DllMain( HANDLE /*hModule*/, DWORD ul_reason_for_call, LPVOID /*lpReserved*/ ) { #if !USE_GLOBAL_OBJECT if (DLL_PROCESS_ATTACH==ul_reason_for_call) CreateAndWaitEvent(); #endif ul_reason_for_call; return TRUE; }
Was passiert hier? Mit Hilfe des Makros USE_GLOBAL_OBJECT wird entschieden, ob entweder ein globales Objekt der Klasse CDoomed instanziiert wird (USE_GLOBAL_OBJECT=1), oder die Funktion CreateAndWaitEvent() (USE_GLOBAL_OBJECT=0) beim Process Attach der DLL aus DllMain heraus aufgerufen aufgerufen wird. Im ersteren Fall wird auch die Funktion CreateAndWaitEvent() aufgerufen, jedoch aus dem Konstruktor der Klasse CDoomed, von der die globale Instanz g_Doomed existiert.
Die Funktion CreateAndWaitEvent() erzeugt oder öffnet ein bereits existierendes Named Event Kernel Object mit dem Namen "oohdelalllyeh!". Auf dessen Signalisierung wartet diese Funktion und kehrt dann zum Aufrufer zurück. So weit so gut, ist ja schließlich keine wahrhafte Raketenwissenschaft.
Jetzt zu der Exe-Datei, ihr Code schaut folgendermaßen aus:
#include "stdafx.h" #include <process.h> #include <stdio.h> #include <tchar.h> #include <windows.h> #ifndef dimof #define dimof(a) (sizeof(a)/sizeof(a[0])) #endif // dimof unsigned __stdcall ThreadFunction(LPVOID) { Sleep(5000); HMODULE hModule = LoadLibrary(_T("locker")); if (hModule) FreeLibrary(hModule); return TRUE; } int main(int /*argc*/, char* /*argv*/[]) { unsigned tid; TCHAR szFileName[_MAX_PATH]; HANDLE hEvent = CreateEvent(NULL, FALSE, FALSE, _T("oohdelalllyeh!")); HANDLE hThread = (HANDLE)_beginthreadex(NULL, 0L, ThreadFunction, NULL, 0L, &tid); Sleep(2000); GetModuleFileName(NULL, szFileName, dimof(szFileName)); if (hEvent) SetEvent(hEvent); if (hThread) { _tprintf (_T("Now waiting for thread to terminate...\n")); WaitForSingleObject(hThread, INFINITE); CloseHandle(hThread); } if (hEvent) CloseHandle(hEvent); _tprintf (_T("Done!\n")); return 0; }
Der Code der Exe-Datei erzeugt denselben named Event wie die DLL und startet dann einen Thread mit der Threadfunktion ThreadFunction. Wenn man nun die Aufrufe von Sleep außer acht läßt, dann macht dieser Secondary Thread nichts anderes, als die DLL zu laden. Der Primary Thread unterdessen ermittelt kurz den Pfad zum eigenen Modul mit GetModuleFileName und signalisiert dann den zuvor erzeugten Named Event um schließlich auf die Beendigung des Threads zu warten (dieses Warten hat mit den Race conditions nicht zu tun, es entspringt nur meiner guten Kodierkinderschule).
Man sollte also annehmen, dass der Ablauf des Programms der Folgende ist, was durch die Wahl der Werte für Sleep dem Scheduler von NT quasi nahegelegt wird:
- Die Applikation erzeugt in der Exe-Datei den Named Event.
- Der Thread wird gestartet, aber er legt sich erstmal für 5s schlafen.
- Unterdessen besinnt sich der Code der Exe-Datei erstmal für 2s und schläft, dann ermittelt er seinen eigenen Pfad im Dateisystem (mit GetModuleFileName). Anschließend signalisiert er den Named Event und wartet auf das Terminieren des Threads.
- Wir warten ca. 3s (Gähn!).
- Der Thread lädt die DLL.
- Der C-Runtime Startupcode führt den Konstruktor des globalen Objekts aus. Dieser öffnet nur nochmal ein zweites Handle zu dem bereits vorher erzeugten Named Event. Der Code der Exe-Datei hatte ja schon vorher den Event signalisiert, sodaß in der Funktion CreateAndWaitEvent die Wait-Operation ungestreift durchrauscht.
- Die Dll wird wieder entladen und der Thread terminiert.
- Der Code aus der exe, der auf das Terminieren des Threads wartet, kehrt aus seiner Wait-Operation zurück und main kehrt zurück in die C-Runtime.
Insgesamt sieht also das Ganze auf der Konsole so aus:
So, und jetzt spielen wir mal mit den Sleeps und machen aus den 5000 in der Threadfunktion nur noch schlappe 500.
Wenn wir jetzt den Prozeß erneut starten, stellen wir nach kürzester Zeit fest, daß sich der Prozeß nur noch durch höhere Gewalt beenden läßt. Schnell einen Debugger attacht und die Callstacks der Threads angeschaut. Und da stellt man dann Erstaunliches fest:
- Der Secondary Thread steht in der Wait-Operation in der DLL und wartet darauf, daß der Named Event signalisiert wird.
- Der Primary Thread ist im API GetModuleFileName steckengeblieben.
Ja, aber was ist denn hier passiert? Der Primary Thread kommt nie dazu, den Named Event zu signalisieren, weil er im API GetModuleFileName stecken bleibt. Der komplette Prozeß ist damit hoffnungslos im Deadlock, aber warum denn nur?
Es liegt daran, daß der Secondary Thread, der die DLL lädt, den "Loader Lock", den NT intern zur Serialisierung der DLL-Ladevorgänge einsetzt, für die Zeitdauer der LoadLibrary-Operation hat, aber das GetModuleFileName API diesen Lock halt dummerweise auch braucht.
Wenn man nun das Makro USE_GLOBAL_OBJECT auf 0 stellt, ändert sich am ganzen Verhalten nichts, denn Code, der beim Process Attach ausgeführt wird, unterscheidet sich praktisch in nichts von Code, der im Konstruktor eines globalen Objekts ausgeführt wird.
Und jetzt wird vielleicht auch klar, warum zu DllMain in der MSDN-Dokumentation folgendes steht:
Das ist doch echt verständlich, oder? Und trotzdem sehe ich täglich nichttriviale globale Objekte, superkomplizierten DllMain-Code und andere Grausamkeiten und Fahrlässigkeiten, wie bespielsweise beliebig aufgerufene Funktionen mit Hilfe von Message-Passing-Patterns, wo weiß Gott was passieren kann (SCNR DEI!). Man mag das Ganze nun abtun als Spezialität von GetModuleFileName, aber es fallen natürlich statt GetModuleFileName noch viele weitere APIs darunter, darunter LoadLibrary selber und natürlich sämtliche erstmalige Aufrufe in eine gedelayloadete DLL. Allesamt Dinge, die ganz unschuldig mal so in den Code reinrutschen können, ohne daß jemand was Böses ahnt.
Die Moral von der Geschicht: Mach eine komplizierte DllMain nicht. Benutze ein Minimum an globalen Variablen und schon gar keine globalen Objekte. Wer's trotzdem tut sollte mit Codemaintenance nicht unter drei Jahren belohnt werden.
Über den Loader Lock hat übrigens Michael Grier hier so einiges geschrieben und gleich eine ganze Serie von Blogposts gemacht.
DISCLAIMER: Ein naiver Beobachter könnte nun meinen, ich sei ein Advokat des Einsatzes von Sleep zur Threadsynchronisation. Wer so denkt, sollte erwachsen werden und hat leider nichts verstanden und sollte mal dieses hier lesen und natürlich Fachliteratur wie den Tanenbaum und andere Classics.