...someplace, where there isn't any trouble? Do you suppose there is such a place, Toto?

64 bit Windows - Teil 9

Heute geht es mal um was ganz anderes, nämlich um Managed Code unter x64. Jawoll, ich habe die Stirn, was über Managed Code zu schreiben, obwohl ich bis vor zwei Wochen noch einen weeeiiiten Bogen drumrum gemacht habe. Genauer gesagt will ich was über Managed Code auf x64 schreiben, der P/Invoke macht aus C#-Code, denn dabei kann so einiges schiefgehen. Mit P/Invoke kann man aus Managed Code traditionellen unmanaged Code aufrufen, indem man bei der Deklaration einer Methode angibt, wie die DLL heißt, aus der der unmanaged Code aufgerufen wird, wie die Funktion heißt, die man daraus aufrufen will und wie die Funktionsparameter gemarshalt (gemarshaled?) werden sollen.

Beispielsweise könnte ich schon seit Jahren eine 32-bittige unmanaged DLL namens foo.dll haben, die eine Funktion Bar mit folgendem Prototyp exportiert:

  BOOL __stdcall Bar(DWORD dwLevel, LPVOID pSrc, DWORD dwFlags, 
      LPCWSTR szString, LPCWSTR szMessage);

Ich komme nun auf die Idee, in einem funkelnagelneuen .NET-Assembly diese DLL verwenden zu wollen weil in dieser uralten DLL extrem viel Gehirnschmalz drinsteckt, oder weil das Biest extrem performant und deswegen unmanaged sein muß oder was auch immer.

Dann könnte eine passende P/Invoke-Implementation in C# beispielsweise so aussehen:

namespace MyCompany.Whatever.DoIlooklikeIgivaShit
{
    public class FooDll
    {

       [DllImport("foo.dll", CharSet = CharSet.Unicode, 
                             CallingConvention = 
                             CallingConvention.Winapi, 
                             SetLastError=true,
                             EntryPoint="Bar")]
       public static extern bool FooBar(uint dwLevel, IntPtr pSrc, 
             uint dwFlags, 
             [MarshalAs(UnmanagedType.LPWStr)] string strString,            
             [MarshalAs(UnmanagedType.LPWStr)] string strMsg);
    }
}

Wenn man das nun so macht und mit der Einstellung "Any CPU" für das .NET-Assembly buildet, dann funktioniert das wunderbar auf einer herkömmlichen x86-Maschine. Die Welt ist schön. Wenn man aber nun dieselben Files auf eine x64-Maschine mit dem .NET-Framework 2.0 kopiert und denkt, das funktioniere da genauso, stellt man plötzlich fest, daß der Prozeß nach kürzester Zeit mit folgender Fehlermeldung kollabiert:

Unhandled Exception: System.BadImageFormatException: 
An attempt was made to load a program with an incorrect format. 
(Exception from HRESULT: ....

Das Problem hierbei ist, daß das .NET-Assembly mit "Any CPU" übersetzt worden ist. Daher wird versucht, den Prozeß als nativen Prozeß (für die CPU) auszuführen, also als x64-Prozeß. Wenn aber leider die DLL, die aus diesem 64-bittigen Prozeß aufgerufen werden soll, eine 32-bittige ist, wie in unserem Fall die Datei foo.dll, dann geht das nicht. Tyischer Fall von "Isso".

Man hat nun zwei Optionen:

  • Das .NET-Assembly nur für x86 übersetzen, wobei die schöne Prozessorunabhängigkeit von .NET verloren geht (sounds like Java, eh?)
  • Die Datei foo.dll native auf x64 portieren

Wer mit Option 1 zufrieden ist, ist fein raus, wird aber niemals die Goodies von x64 in Anspruch nehmen können und kann jetzt mit weiterlesen aufhören.

Im folgenden nehme ich an, daß wir aber unser tolles .NET-Assembly unter x86 als native x86-Prozeß laufen lassen wollen und unter x64 als native x64-Prozeß. Wir lassen also die Einstellung für das Assembly auf "Any CPU" und fangen an, die Datei foo.dll nach x64 zu portieren, schließlich haben wir auch ein paar native x64 Binaries aus unmanaged Code, die einen x64-Port von foo.dll auch ganz gut benutzen könnten.

Haben wir jetzt ein reinrassiges x86-Softwareprodukt und ein reinrassiges x64-Softwareprodukt, wo es keine Übereinstimmungen im Auslieferungsumfang zwischen beiden Varianten geben soll, dann haben wir jetzt kein Problem mehr. Besteht unser Softwareprodukt jedoch aus gemischten Komponenten, also teils aus x86-Binaries und x64-Binaries und installieren wir alles in ein und dasselbe Installationsverzeichnis, dann haben wir jetzt ein Problem: Wie soll denn die native x64-Version von foo.dll heissen? foo.dll? Geht nicht, so heisst schon die x86-Version. foox64.dll? Geht nicht, so kann unser tolles .NET-Assembly sie nicht laden, weil es im DllImport-Attribut was von "foo.dll" stehen hat und nicht "foox64.dll". Sollen wir dann zwei Versionen von unserem Assembly machen, die sich jeweils nur im Dll-Namen im DllImport-Attribut unterscheiden? Can you say maintenance nightmare?

Was wir brauchen, ist eigentlich ein "Mediator", der automatisch die Aufrufe aus unserem tollen .NET-Assembly unter x86 in Aufrufe in die x86-Variante foo.dll übersetzt und unter x64 in die x64-Variante foox64.dll. Und an der Stelle kommt uns eine nette Eigenschaft von native DLLs entgegen, nämlich die Möglichkeit, Aufrufe in andere DLLs zu forwarden. Wir erzeugen also eine neue DLL namens foofwd.dll, die keinerlei Code enthält, sondern alle Exporte von foo per def-file identisch exportiert. Diese foofwd.dll builden wir einmal für x86 und einmal für x64 und nennen sie beidesmal gleich, nämlich foofwd.dll. Die x86-Version wird alle ihre Exporte nach foo.dll forwarden und die x64-Version alle ihre Exporte nach foox64.dll. Mit zwei getrennten def files, die man evtl auch noch als Teil des Builds skriptgesteuert erzeugen läßt, ist das absolut Kinderfasching.

Was ist aber nun mit unserem tollen .NET-Assembly? Das .NET-Assembly schreiben wir nun, da wir um den "Mediator" in Form von foofwd.dll wissen, in vorauseilendem Gehorsam so, daß er ausschließlich die Datei "foofwd.dll" in seinen DllImport-Attributen referenziert, also nicht foo.dll oder foox64.dll.

Und was liefern wir nun auf welcher Plattform wie aus?

Auf einem 32-bit Windows XP macht es gar keinen Sinn, die x64-Binaries mitauszuliefern. Unser Installationsprozeß wird also die x64-Binaries auf diesem Zielsystem komplett übergehen. Die Datei foofwd.dll landet hier zusammen mit foo.dll in unserem Installationsverzeichnis und alles ist gut, denn die .NET-basierten Prozesse können hier nur als 32-bit-Prozesse laufen, finden zur Laufzeit die 32-bittige foofwd.dll im Installationsverzeichnis, welche beim Laden automatisch die 32-bittige foo.dll nachzieht.

Auf einem 64-bittigen Windows XP machen wir erstmal die Installation exakt wie unter dem 32-bittigen XP, kopieren also die 32-bittige foofwd.dll und die 32-bittige foo.dll ins Installationsverzeichnis. Dann kopieren wir die 64-bittige foox64.dll ins Installationsverzichnis. Damit aber ein .NET-basierter Prozess nun seine 64-bittige forwarder DLL findet, müsssen wir diese an einen Ort kopieren, wo ein beliebiger 32-bittiger Prozeß (also beispielweise auch ein .NET-Assembly das nicht mit "Any CPU" übersetzt wurde sondern mit "x86") sie auf keinen Fall aus Versehen laden kann (was nämlich fehlschlagen würde) aber ein 64-bittiger Prozeß sie auf jeden Fall findet.

Genau dieses Problem hat MS ja auch, denn es gibt unter Windows XP x64 Edition beispielsweise einen 32-bittigen regedit.exe und einen 64-bittigen regedit.exe. Oder einen 32-bittigen cmd.exe und einen 64-bittigen cmd.exe. Beide linken dynamisch gegen kernel32.dll aber der 32-bittige regedit gegen den 32-bittigen kernel32.dll und der 64-bittige regedit gegen den 64-bittigen kernel32.dll, und beide Versionen von kernel32.dll befinden sich in verschiedenen Unterverzeichnissen, die durch file system redirection voneinander abgeschottet sind. So ist die 32-bittige Version von kernel32.dll unter %systemroot%\syswow64 zu finden und die 64-bittige Version von kernel32.dll unter %systemroot%\system32. Einem 32-bittigen Prozess wird, so er irgendetwas aus %systemroot%\system32 laden will, automatisch immer in Wahrheit %systemroot%\syswow64 durch diesen Mechanismus namens "file system redirection" untergeschoben.
Und diesen Mechanismus können wir uns bei unsrem Problem ganz einfach zunutze machen, und zwar indem wir die Datei foofwd.dll in ihrer 64-bittigen Variante bei der Installation auf einem x64-Betriebssystem schlicht und einfach nach %systemroot%\system32 kopieren. Dann finden alle "Any-CPU"-.NET-basierten Prozesse die foofwd.dll über %systemroot%\system32 (also über die Umgebungsvariable %PATH%) und alle "x86"-.NET-basierten Prozesse über das lokale Installationsverzeichnis.

Trackback address for this post

This is a captcha-picture. It is used to prevent mass-access by robots.
Please enter the characters from the image above. (case insensitive)

3 comments, 7 trackbacks

Trackback from: Dirk's Blog [Visitor]
Dirk's BlogDer managed P/Invoke Mediator
03/05/06 @ 22:23
Trackback from: Dirk's Blog [Visitor]
Dirk's BlogDer managed P/Invoke Mediator
03/05/06 @ 22:39
Trackback from: Dirk's Blog [Visitor]
Dirk's BlogDer managed P/Invoke Mediator
03/05/06 @ 22:48
Trackback from: Dirk's Blog [Visitor]
Dirk's BlogDer managed P/Invoke Mediator
03/05/06 @ 23:00
Trackback from: Dirk's Blog [Visitor]
Dirk's BlogDer managed P/Invoke Mediator
03/05/06 @ 23:06
Trackback from: Dirk's Blog [Visitor]
Dirk's BlogDer managed P/Invoke Mediator
03/05/06 @ 23:07
Comment from: dirk [Visitor]
dirkDa ich eine zusätzliche Forwarder-DLL vermeiden wollte, habe ich meinen Ansatz mal ausformuliert (Siehe Track Back Url). Ich denke das gibt genügende Diskussionsstoff für einen Tasse Kaffee.
03/05/06 @ 23:11
Comment from: Stefan [Visitor]
StefanHey Dirkie,

ein Trackback statt 6 alle 5 Minuten haette auch genuegt. Und das braucht nicht nur eine oder zwei Tassen Kaffee sondern evtl. ein reinrassiges Montagsbesäuf...

Ob wohl unsere Dotnetjunkies aus'm Servertrack sich auch schon solche Gedanken gemacht haben?

--
S
03/06/06 @ 22:02
Comment from: dirk [Visitor]
dirkZu meiner Verteidigung muss ich sagen, das die Trackbacks irgendwie automatisch von meiner Blog-Engine angelegt wurde. Es wurde jedesmal ein neuer angelegt, wenn ich einen Fehler im Text behoben habe ;-).
03/06/06 @ 22:44
Trackback from: Dirk's Blog [Visitor]
Dirk's BlogDie 64Bit Windows Artikel des eMCSCs
03/06/06 @ 23:07

Comments are closed for this post.