Integer Swapping ohne temporäre Variable
Kürzlich bin ich beim kursorischen Durchkucken der Sourcen eines Kollegen auf ein Makro gestoßen, das die Werte zweier Integervariablen vertauschen soll. Die Implementation sah in etwa folgendermaßen aus:
#define SWAP(a,b,type){type iIdunno = a; a=b; b=iIdunno;}
und wurde beispielsweise so angewendet:
int i1 = rand(); int i2 = rand(); PrintValues(i1, i2); SWAP(i1, i2, int); PrintValues(i1, i2);
Diese naive Implementation muss nicht unbedingt die beste sein. Will man beispielsweise die temporäre Variable vermeiden und ein universelles Makro ohne Angabe des Typs haben, kann man auch drei aufeinanderfolgene XOR-Operation ausführen:
#define SWAP(a,b){a^=b;b^=a;a^=b;}
Je nachdem, wie schnell eine XOR-Operation im Vergleich zu einer MOV Operation auf dem gegebenen Prozessor ist (also wieviele Maschinenzyklen gebraucht werden) und wieviele Bytes die jeweiligen Instruktionen an Code brauchen, kann es sein, daß diese Implementation auch schneller ist oder kleineren Code erzeugt. Und man muß natürlich wirklich auch den jeweiligen Assemblercode anschauen, zu dem der Compiler den C-Code optimiert, um beurteilen zu können, was die schnellere Methode ist. Auf den alten 8088-Prozesooren war der XOR IIRC schneller als ein MOV, weshalb das durchaus von Vorteil war. Und natürlich ist sowas auch nur wirklich interessant in einer innermost Loop - und aus rein akademischen Gesichtspunkten :-).
64 bit Windows - Teil 8
Nach langer Pause jetzt mal wieder was über meine fortdauernde Beschäftigung mit 64-bit Windows. Diesmal will ich auf wiederkehrende Muster bei dem Problem, meinen Sourcecode 64-bit-tauglich zu machen, eingehen.
Die Sache mit dem size_t
An vielen Stellen ging mein Code in der Vergangenheit davon aus, dass die Größe eines size_t 32-bit ist, oder dasselbe ist wie die Größe eines DWORD oder long/LONG. Das stimmt unter 64-bit Windows nicht mehr, der size_t ist so groß wie ein Zeiger, also gilt folgendes:
sizeof(size_t)!=sizeof(DWORD)
Was hat das für praktische Konsequenzen? Also erstmal eine Menge casts, wo vorher keine nötig waren, wenn man warnungsfrei bleiben will.
Beispiele:
Aus
size_t stLen = _tcslen(szFoo); dwLen += stLen*sizeof(TCHAR);
wird jetzt
size_t stLen = _tcslen(szFoo); dwLen += (DWORD)stLen*sizeof(TCHAR);
oder aus
WriteFile(hFile, szPath, _tcslen(szPath), &dwWritten, NULL);
wird
WriteFile(hFile, szPath, (DWORD)_tcslen(szPath), &dwWritten, NULL);
und aus
RegSetValueExW( hKey, NULL, 0L, REG_SZ, (LPBYTE)szPath, wcslen(szPath)+1)*sizeof(wchar_t));
wird
RegSetValueExW( hKey, NULL, 0L, REG_SZ, (LPBYTE)szPath, (DWORD)(wcslen(szPath)+1)*sizeof(wchar_t));
oder aus
VERIFY(MultiByteToWideChar(CP_ACP, MB_PRECOMPOSED, (LPCSTR)name, -1, &szFullname[wcslen(szFullname)], dimof(szFullname)-wcslen(szFullname)));
wird
VERIFY(MultiByteToWideChar(CP_ACP, MB_PRECOMPOSED, (LPCSTR)name, -1, &szFullname[wcslen(szFullname)], (int)(dimof(szFullname)-wcslen(szFullname))));
Huh, das war aber einfach, oder? Hat man einen size_t aber als Teil einer Datenstruktur, die serialisiert wird, hat man jetzt ein größeres Problem.
Pointer und Handles sind jetzt 64-bittig - naja fast alle...
Das sollte man nun wirklich erwarten, dass Pointer und Handles 64-bit breit sind, schließlich wollten wir doch den großen Adreßraum, oder? Naja, aber was fangen nun 32-bittige GUI-Prozesse auf einem 64-bittigen Desktop mit 64-bit breiten Windows-Handles an? Was, wenn sie einen schlichten FindWindow machen wollen auf ein Fenster, das zu einem 64-bittigen Prozess gehört? Deswegen sind Window Handles für 64-bittige Prozesse zwar 64-bit breit, aber die oberen 32-bit davon bleiben ungenutzt (also Null). Daher funktioniert beispielsweise folgender Code (aus einer InstallShield Extension DLL) nach wie vor, egal, ob für 32-bit oder 64-bit uebersetzt:
__declspec(dllexport) LONG __cdecl FindWindowClass(HWND hwnd, LPLONG lpIValue, LPSTR lpszValue) { #pragma warning (disable: 4305 4311) Unreferenced(hwnd); Unreferenced(lpIValue); return (LONG)FindWindow(lpszValue, NULL); #pragma warning (default: 4305 4311) }
INVALID_HANDLE_VALUE und 0xFFFFFFFF
Aus irgendeinem dummen Grund habe ich an vielen Stellen in altem Code als Test fuer das Scheitern des CreateFile APIs auf die Rückgabe von 0xFFFFFFFF geprüft. Das ist auch soweit OK auf x86. Die eigentlich korrekterweise zu verwendende Konstante (der Wert INVALID_HANDLE_VALUE) ist auch als (HANDLE)-1 definiert. Dummerweise ist aber damit INVALID_HANDLE_VALUE auf x64-Systemen der numerische Wert 0xFFFFFFFFFFFFFFFF und nicht 0xFFFFFFFF. Also Moral von der Geschicht': Benutz´' die Zahlenwerte nicht, benutze die Makros aus den Headern, obwohl das erstmal gleich ausschaut.
Die Sache mit LONG_PTR und INT_PTR
Bei einer ganzen Reihe von Parametern zu APIs und MFC-Methoden wurden in den vergangenen Jahren still und heimlich die Prototypen leicht modifiziert. Das betrifft in erster Linie Parameter, über die traditionell ein Zeiger transportiert wurde, die aber als formalen Parameter einen LONG oder DWORD erwarten (die unter 64-bit Windows weiterhin 32-bit breit sind und deswegen keinen Pointer mehr transportieren können). Ein INT_PTR oder ein LONG_PTR etc.. sind also Integerwerte, die je nach Plattform so breit sind wie ein Zeiger auf der entsprechenden Plattform. Man sollte so einen Datentyp also in keinem Fall für persistentes und plattformübergreifendes Serialisieren von Daten verwenden.
Beispiele aus der Praxis:
Folgender Altcode wird nicht mehr sauber übersetzt mit einem x64-Compiler:
BOOL CCustomToolTip::AddAutoTool(CWnd *pWnd,LPCTSTR pszText) { TOOLINFO ti; ti.cbSize=sizeof(TOOLINFO); ti.uFlags=TTF_IDISHWND|TTF_SUBCLASS; ti.hwnd=pWnd->GetParent()->GetSafeHwnd(); ti.uId=(UINT)pWnd->GetSafeHwnd(); . .
Die letzte Zeile muß einfach mit einem korrekten cast versehen werden, weil der uId member der TOOLINFO Struktur jetzt ein UINT_PTR ist:
ti.uId=(UINT_PTR)pWnd->GetSafeHwnd();
Ähnliches bei der NEWCPLINFO Struktur für Control Panel Applets. Folgender Altcode wird nicht mehr sauber übersetzt mit einem x64-Compiler:
LRESULT CCPApplet::OnNewInquire(NEWCPLINFO& info) { info.dwSize = sizeof(NEWCPLINFO); info.dwFlags = 0; info.dwHelpContext = 0; info.lData = (LONG)this; . .
Die letzte Zeile muß auch hier mit einem korrekten cast versehen werden, weil der lData member der NEWCPLINFO Struktur jetzt ein LONG_PTR ist:
info.lData = (LONG_PTR)this;
Oder InsertMenu aus MFCs CMenu Klasse: Diese Methode erwartet als dritten Parameter entweder eine ID als UINT oder ein Menu-Handle wenn das Flag MF_POPUP im zweiten Parameter übergeben wird. Alter Code sieht daher so aus:
pSubMenu->InsertMenu(0,MF_BYPOSITION|MF_POPUP|MF_ENABLED, (UINT) hMenu, str);
was aber der x64-Compiler nicht akzeptiert. Es muß heißen:
pSubMenu->InsertMenu(0,MF_BYPOSITION|MF_POPUP|MF_ENABLED, (UINT_PTR)hMenu, str);
Weitere vergleichbare Probleme:
- SendMessageTimeout erwartet einen PDWORD_PTR statt eines PDWORD als letzten Parameter
- IContextMenu::GetCommandString erwartet einen UINT_PTR, keinen UINT
- SendMessage liefert einen LRESULT, keinen int. LRESULT ist jetzt 64-bittig
- Eine WNDPROC hat einen LRESULT, keinen BOOL oder int als Rückgabewert
- GetWindowLong/SetWindowLong sind obsolet um etwa eine WNDPROC zu bekommen für's subclassing. Stattdessen: GetWindowLongPtr/SetWindowLongPtr
- CPropertySheet :: DoModal() liefert einen INT_PTR zurueck, keinen int. Damit können erstmal komplete Ableitungsketten von Altcode zerstört werden oder können zumindest nicht auf Anhieb fehlerfrei übersetzt werden
- CList :: GetCount() oder CMap :: GetCount() liefern einen INT_PTR zurueck, keinen int.
Um alle diese Probleme erstmal zu sehen (denn nicht immer hilft ein cast - und unbesehen können hier Datenverlust oder Crashes auftreten - wenn etwa Pointer truncated werden) sollte man unbedingt den Compilerschalter /Wp64 verwenden, der solche Dinge zu erkennen hilft.
Ganz krass sind auch die Umstände zum Aufzählen von files in einem Directory, wenn das Ganze portabel sein soll. Aus folgendem Altcode:
struct _tfinddata_t data; _tcscat(szPath,"\\*cpl"); long handle = _tfindfirst(szPath, &data);
muß die letzte Zeile geändert werden zu:
intptr_t handle = _tfindfirst(szPath, &data);
Zusätzlich mußte noch folgende #ifdef-Orgie vorangestellt werden - damals war's noch ein Beta-SDK, ich hoffe, heute geht's auch ohne solchen Heckmeck:
#ifndef _INTPTR_T_DEFINED #ifdef _WIN64 typedef __int64 intptr_t; #else typedef _W64 int intptr_t; #endif #define _INTPTR_T_DEFINED #endif
Luna Manifeste
Die Manifeste für Luna waren für mich ein weiteres, größeres Hindernis. Hat man nämlich dann eine exe fuer x64 endlich erfolgreich portiert und übersetzt, können die Resultate selbst dann noch zu Problemen führen. Folgendes Manifest aus einem x86-Binary wird beispielweise problemlos in ein native x64 Binary eingebunden:
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0"> <assemblyIdentity version="1.0.0.0" processorArchitecture="x86" name=Nettosport.Cool.Product type="win32" /> <description> NettosportApplication </description> <dependency> <dependentAssembly> <assemblyIdentity type="win32" name="Microsoft.Windows.Common-Controls" version="6.0.0.0" processorArchitecture="x86" publicKeyToken="6595b64144ccf1df" language="*" . . .
Aber die native für x64 gebuildete exe startet leider um's Verrecken nicht auf einem x64-System. Es hat eine halbe Ewigkeit gedauert, bis ich rausgefunden hatte, daß ein Entfernen des Manifests einen problemlosen Start ermöglicht, aber mit genau diesem Manifest aus der x86-Version der Exe kein Start möglich war. Dann das Manifest genauer angeschaut und die Lösung ist simpel:
Überall da, wo in diesem Manifest die processorArchitecture mit "x86" festgelegt ist, ersetzt man einfach "x86" durch "*" und schon klappt's auch mit dem x64-Programmlader und ein- und dasselbe Manifest funktioniert für's Übersetzen desselben Sourcecodes unter x64 und x86. So einfach kann's sein.
"DESCRIPTION" in def files
Für den Linker ist die "DESCRIPTION" in def files für x64 obsolet. Drum: Einfach zukünftig weglassen, entfernen oder mit einer Linkerwarnung leben. Für kurze def-files in denen sich wenig ändert kann man sich auch eine zweite x64-Version machen, wo "DESCRIPTION" fehlt und die man statt über das dsp-file dann eben über die Linkerkommandozeile ändert. Damit "a jeda" weiß, wovon ich hier rede: in folgendem def file muß die Zeile mit "DESCRIPTION" einfach komplett raus:
LIBRARY "foo" DESCRIPTION 'Supercalifragilisticexpialidocius Nettosport DLL' EXPORTS ; Explicit exports can go here Bar PRIVATE Baz PRIVATE Oohdelallye PRIVATE
So, das wär's jetzt erstmal wieder. Hoffentlich vergeht nicht wieder ein Vierteljahr, bis ich wieder was über meine x64-Erfahrungen schreibe...
The CD subject story...
Also los, es gibt bei επτ€σ eine Menge neuer Kolleginnen und Kollegen und außerhalb der Entwicklung ist die Story (oder besser "die Spekulation") vom "CD Subject" eh' nicht bekannt, deswegen schreibe ich jetzt mal drüber:
Bis vor einem halben Jahr etwa war es üblich, daß sich die Mitarbeiter von επτ€σ freitags versammelt haben, um die latest news über die eigene Company zu erfahren. Irgendwann 'mal wurde beschlossen, daß die Protokolle dieser Versammlungen auch ins britische Idiom übersetzt werden müssen, damit die Kollegen in USA auch 'was davon haben. Und so kam es, daß eines schönen Freitags die brandneuen Trainingsunterlagen für die Kunden vorgestellt wurden. Diese bestanden aus einem DIN-A4-Ordner, der aber raffinierterweise in seinem Deckel ein transparentes Plastikdingsbums hatte, wo dann die Produkt-CD drinstak. Das war echt revolutionär und wurde als größte Erfindung seit Erdbeereis natürlich gebührend gefeiert. In der englischen Übersetzung dieser Vorstellung des neuen Trainingsordners war dann in etwa folgendes zu lesen:
"... the new binder for the customer training material has a transparent CD subject..."
Da mußte ich dann doch stutzen: "CD subject"?
Da das eine der wenigen Gelegenheiten war, wo ich bei dieser freitäglichen Versammlung anwesend war (mir war das sonst meistens zu früh), habe ich mich versucht zu entsinnen, wovon denn bei der Vorstellung die Rede war.... Richtig: Vom "CD-Leerfach" war da die Rede. Also hat der kleine Watson in mir messerscharf gefolgert, daß das deutsche "CD-Leerfach" irgendwie zum englischen "CD subject" transmogrifiziert wurde.
Also dict.leo.org im Webclient meiner Wahl angeworfen und "Leerfach" eingegeben. Aber was eine Enttäuschung: dict.leo.org sagt:
Die Suche nach Leerfach lieferte keine Treffer
Aber halt, fast hätten wir's übersehen: dict.leo.org zeigt darunter gleich einen Verbesserungsvorschlag an:
Orthographisch ähnliche Wörter ? Deutsch:
lehrfach
Also kurzerhand auf den Link mit "lehrfach" geklickt und was grinst mich an?
Unmittelbare Treffer
discipline das Lehrfach
subject das Lehrfach
"Uhhh, ahhh, "CD-discipline" - das klingt doch irgendwie - hhhm, ..... zu preußisch, das kann kein ordentliches Englisch sein. Aber hey, "CD subject", das klingt doch echt gut, oder?"
Und so hat vermutlich das "CD-Leerfach" über das "CD-Lehrfach" seinen Weg als "CD Subject" in dieses Protokoll gefunden. dict.leo.org sei's getrommelt und gepfiffen.
Wegweisende Technologien in einem Produkt vereinigt
Heute bei einem namhaften Lebensmitteldiscounter im Angebot: SFRs und meine Paradedisziplinen in einem Produkt:
Hey, das is'n Insiderjoke!
Regedit.exe mehrfach ausführen
Das Tool regedit.exe kann man normalerweise auf einem Desktop nur in einer einzigen Instanz starten. Genaugenommen beendet sich eine zweite Instanz sofort wieder, wenn sie bemerkt, daß eine Instanz bereits auf einem Desktop läuft. Eine wenig bekannte Kommandozeilenoption von regedit gestattet es allerdings, mehrere Instanzen gleichzeitig auf einem Desktop nebeneinanderher laufen zu lassen:
regedit -m
Wozu kann man das brauchen? Immer dann wenn man komfortabel die Änderungen in der Registry an zwei Stellen beobachten will, beispielweise während des Debuggens. Besonders geschickt ist das beispielsweise auch, wenn man auf einer x64-Maschine Werte im eigentlichen HKLM-Zweig mit denen im HKLM-Zweig für x86-Prozesse vergleichen will. Dann startet man
%SYSTEMROOT%\system32\regedit.exe -m
um die eigentliche Registry zu sehen und
%SYSTEMROOT%\SysWOW64\regedit.exe -m
um den Teil der Registry zu sehen, den ein x86-Binary sieht.
Leider gibt es diese Kommandozeilenoption aber erst seit Windows XP.