Eine COM-Bridge zum Aufruf einer 64-Bit-DLL aus einer 32-Bit-Anwendung – ein Praxisbeispiel

· · COM, Windows-Entwicklung, 32bit, 64bit

Die Anforderung, eine 64-Bit-DLL aus einer 32-Bit-Anwendung heraus aufzurufen, ist unter Windows recht typisch. Vor allem wenn vorhandene Bestände erhalten bleiben sollen und nur die Funktionalität der 64-Bit-Seite genutzt werden soll, erweist sich eine COM-Bridge-Architektur häufig als praktikable Lösung.

Zielgruppe: Wer eine bestehende 32-Bit-Windows-Anwendung pflegt und DLLs oder Bibliotheken auf der 64-Bit-Seite nutzen möchte. Der Text ist so geschrieben, dass er auch mit dem Vorwissen „von COM schon gehört, aber selbst noch nichts damit gebaut“ lesbar ist.

Vorausgesetzte Umgebung: Eine 64-Bit-Version von Windows (x64) sowie eine Entwicklungsumgebung, in der Sie C# schreiben können (etwa Visual Studio). Für die Registrierung eines COM-Servers systemweit (unter HKEY_LOCAL_MACHINE) sind Administratorrechte erforderlich. Die grundlegende Denkweise von COM ordnen wir in „Was ist COM? – Warum das Design von Windows COM auch heute noch schön ist“ ein.

Inhaltsverzeichnis

  1. Die Ausgangslage
  2. Die Lösung
  3. Der Ablauf (Sequenzdiagramm)
  4. Beispielcode (konzeptionell)
  5. Vollständiger Beispielcode
  6. Zusammenfassung
  7. Quellen

1. Die Ausgangslage

Das ist der Fall, in dem die bestehende 32-Bit-Anwendung unverändert bleiben soll, aber die Verarbeitung einer 64-Bit-DLL genutzt werden soll. Das Problem: Ein 32-Bit-Prozess kann keine 64-Bit-DLL laden. Das ist eine Einschränkung auf Betriebssystemebene – kein Problem, das sich mit einem Kunstgriff lösen ließe.

Häufig sieht die Situation so aus:

  • Die bestehende 32-Bit-Anwendung ist ein großer Bestand und lässt sich nicht kurzfristig migrieren
  • Die 64-Bit-DLL bringt neue Funktionen mit, oder ihre Abhängigkeiten liegen nur in 64-Bit vor
  • Der Aufruf von der 32-Bit-Seite soll „typsicher“ erfolgen

Bei dieser Kombination ist der Weg über denselben Prozess von vornherein versperrt.

2. Die Lösung

Da ab diesem Kapitel Fachbegriffe folgen, hier zunächst das nötige Minimum an Vokabular.

Begriff Bedeutung
In-Proc-COM (DLL-Server) Eine COM-Komponente wird in denselben Prozess wie der Aufrufer geladen und genutzt. Schnell, lässt sich aber bei unterschiedlicher Bitness nicht laden
Out-of-Proc-COM (EXE-Server) Eine COM-Komponente wird als separater Prozess gestartet und genutzt. Funktioniert auch bei unterschiedlicher Bitness
LocalServer Ein COM-Server, der als separater Prozess auf demselben Rechner läuft. Der Pfad zur EXE wird im Registrierungsschlüssel LocalServer32 hinterlegt
IDL / TypeLib Die Definition der Schnittstellenform (Methodennamen, Parametertypen) als IDL sowie deren binäre Form als TypeLib. Beide Seiten nutzen so denselben „Vertrag“
Marshalling Das Umpacken von Argumenten und Rückgabewerten in eine übertragbare Form, um die Prozessgrenze zu überschreiten. Das Gegenteil ist Unmarshalling
Proxy / Stub Der Stellvertretercode, der das Marshalling tatsächlich durchführt. Auf der Aufruferseite steht der Proxy, auf der Serverseite der Stub
WOW6432Node Der Ort, an dem auf einem 64-Bit-Windows die Registrierungsinhalte für 32-Bit-Anwendungen physisch abgelegt werden. Unter demselben Schlüsselnamen unterscheiden sich die Inhalte auf der 32-Bit- und der 64-Bit-Seite

Die grundlegende Lösung ist die Trennung über Out-of-Proc-COM (einen EXE-Server). Die 64-Bit-DLL wird von einem 64-Bit-COM-Server (EXE) aus aufgerufen, und die 32-Bit-Anwendung nutzt ihn über COM.

Der Ablauf sieht so aus:

  1. Einen 64-Bit-COM-LocalServer (EXE) bereitstellen, der intern die 64-Bit-DLL aufruft
  2. Die COM-Schnittstelle (IDL/TypeLib) teilen, um die Typen offenzulegen
  3. Die 32-Bit-Anwendung ruft COM „typsicher“ auf (Kommunikation über Proxy/Marshalling)

Es gibt jedoch auch Dinge zu beachten.

  • Die Registrierung für 32-Bit und 64-Bit ist getrennt (einschließlich WOW6432Node)
  • Eigene Strukturen erfordern ein Marshalling-Design
  • IPC-Overhead – bei häufigen Aufrufen ist Vorsicht geboten

Mit anderen Worten: Der Königsweg lautet, „die 64-Bit-Verarbeitung in einen separaten Prozess auszulagern und über COM zu überbrücken.“

3. Der Ablauf (Sequenzdiagramm)

Im Folgenden der Ablauf, wenn die 32-Bit-Anwendung die Verarbeitung der 64-Bit-DLL aufruft.

Wird von der registrierten COM-Marshalling-Infrastruktur verarbeitet64-Bit-DLL64-Bit-COM-Server(EXE)COM-Stub(64-Bit-Seite)RPC/IPC(Interprozesskommunikation)COM-Proxy(32-Bit-Seite)32-Bit-Client-App64-Bit-DLL64-Bit-COM-Server(EXE)COM-Stub(64-Bit-Seite)RPC/IPC(Interprozesskommunikation)COM-Proxy(32-Bit-Seite)32-Bit-Client-AppParameter marshallenParameter unmarshallenRückgabewert marshallenRückgabewert unmarshallenICalcService.Add(1, 2)Serialisierte DatenÜber die Prozessgrenze übertragenAdd(1, 2)Nativer FunktionsaufrufErgebnis: 3Ergebnis: 3Serialisiertes ErgebnisÜber die Prozessgrenze übertragenErgebnis: 3

Wichtig:

  • Die 32-Bit-Anwendung kann über die Schnittstelle ICalcService typsicher aufrufen
  • Die COM-Laufzeit überschreitet die Prozessgrenze mithilfe der registrierten Proxy/Stub-DLL, des TypeLib-Marshallers, des Standard-Marshallers und Ähnlichem
  • Wegen des Interprozesskommunikations-Overheads ist Sammelverarbeitung feingranularen Aufrufen vorzuziehen

4. Beispielcode (konzeptionell)

4.1. Gemeinsame Schnittstelle sowie Server und Client

Das Folgende ist eine konzeptionelle Skizze. Damit sie tatsächlich läuft, ist zusätzlich die Registrierung aus 4.2 nötig.

// Gemeinsame Schnittstelle (IDL-Äquivalent)
[ComVisible(true)]
[Guid("7A4B5B23-0A2F-4D2B-9D4D-8A2A92B8B001")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface ICalcService
{
    int Add(int a, int b);
}

// 64-Bit-COM-LocalServer (EXE-Seite)
[ComVisible(true)]
[Guid("1C9B6F4D-1E9A-4E61-9A4F-6A0F1D2D9A11")]
[ClassInterface(ClassInterfaceType.None)]
[ProgId("KomuraSoft.CalcService")]
public class CalcService : ICalcService
{
    public int Add(int a, int b)
    {
        // Hier wird die 64-Bit-DLL aufgerufen
        return a + b;
    }
}

// 32-Bit-App-Seite (Client)
Type t = Type.GetTypeFromProgID("KomuraSoft.CalcService");
var calc = (ICalcService)Activator.CreateInstance(t);
int result = calc.Add(1, 2);

In dieser Form lässt sich die 32-Bit-Seite „typsicher“ ansprechen. COM setzt intern Proxy/Stub ein und übernimmt den Aufruf über IPC.

[ProgId("KomuraSoft.CalcService")] ist gesetzt, damit die Client-Seite sie über Type.GetTypeFromProgID("KomuraSoft.CalcService") finden kann. Die ProgID ist nur ein „für Menschen lesbarer Alias“ – den Server tatsächlich findet die im Folgenden erläuterte Registrierung der CLSID.

4.2. Die minimal nötigen Registrierungsschritte

COM funktioniert nach dem Prinzip, dass die COM-Laufzeit eine in der Registrierung hinterlegte CLSID nachschlägt und darüber startet. Deshalb läuft nicht registrierter Code grundsätzlich nicht (Type.GetTypeFromProgID liefert null, oder CreateInstance liefert REGDB_E_CLASSNOTREG). Für einen EXE-Server (LocalServer) sind im Kern nur die folgenden drei Schlüssel nötig.

Was registriert wird Schlüssel Wert
Zuordnung ProgID → CLSID HKEY_CLASSES_ROOT\KomuraSoft.CalcService\CLSID {1C9B6F4D-1E9A-4E61-9A4F-6A0F1D2D9A11}
Zuordnung CLSID → EXE-Pfad HKEY_CLASSES_ROOT\CLSID\{1C9B6F4D-…}\LocalServer32 Vollständiger Pfad zur 64-Bit-COM-Server-EXE
Rückverweis CLSID → ProgID HKEY_CLASSES_ROOT\CLSID\{1C9B6F4D-…}\ProgID KomuraSoft.CalcService

Hier liegt genau die Falle, die das Kernthema dieses Artikels ist. HKEY_LOCAL_MACHINE\SOFTWARE\Classes wird von 32-Bit- und 64-Bit-Anwendungen gemeinsam genutzt, aber die darunterliegenden CLSID-Unterschlüssel (und ebenso Interface und Ähnliches) sind für die 32-Bit- und die 64-Bit-Seite getrennt (die 32-Bit-Ausprägung liegt physisch unter WOW6432Node) – das steht ausdrücklich in der Microsoft-Dokumentation. Der ProgID-Schlüssel muss also nur einmal geschrieben werden, um von beiden Seiten sichtbar zu sein, aber die Registrierung der CLSID muss sowohl in die 32-Bit- als auch in die 64-Bit-Ansicht geschrieben werden, sonst findet der 32-Bit-Client den Server nicht.

In einer Eingabeaufforderung mit Administratorrechten ist es zuverlässig, den reg-Befehl mit /reg:32 und /reg:64 zu verwenden (den Wow6432Node-Anteil selbst in den Pfad zu schreiben, rät Microsoft ausdrücklich ab).

:: In einer Eingabeaufforderung mit Administratorrechten ausführen
set CLSID={1C9B6F4D-1E9A-4E61-9A4F-6A0F1D2D9A11}
set PROGID=KomuraSoft.CalcService
set SERVER=C:\Program Files\KomuraSoft\CalcServer.exe

:: 1) ProgID -> CLSID (direkt unter HKLM\SOFTWARE\Classes wird von 32/64 gemeinsam genutzt)
reg add "HKLM\SOFTWARE\Classes\%PROGID%\CLSID" /ve /d "%CLSID%" /f

:: 2) CLSID -> LocalServer32 und ProgID (unter CLSID ist es für 32/64 getrennt, daher in beide schreiben)
::    In den Wert von LocalServer32 den Pfad zur ausführbaren Datei samt Anführungszeichen eintragen.
::    Schreibt man /d "%SERVER%", werden die Anführungszeichen bei der Argument-Analyse von
::    reg.exe entfernt, und der Wert wird als C:\Program Files\... ohne Anführungszeichen
::    gespeichert. COM interpretiert dies als Kommandozeile und sucht daher zuerst nach
::    C:\Program.exe, dem Teil vor dem Leerzeichen
reg add "HKLM\SOFTWARE\Classes\CLSID\%CLSID%\LocalServer32" /ve /d "\"%SERVER%\"" /f /reg:64
reg add "HKLM\SOFTWARE\Classes\CLSID\%CLSID%\ProgID"        /ve /d "%PROGID%"     /f /reg:64
reg add "HKLM\SOFTWARE\Classes\CLSID\%CLSID%\LocalServer32" /ve /d "\"%SERVER%\"" /f /reg:32
reg add "HKLM\SOFTWARE\Classes\CLSID\%CLSID%\ProgID"        /ve /d "%PROGID%"     /f /reg:32

:: Den gespeicherten Wert prüfen. Erscheint er als "C:\Program Files\..." mit Anführungszeichen, ist es korrekt
reg query "HKLM\SOFTWARE\Classes\CLSID\%CLSID%\LocalServer32" /ve /reg:64

Die Anführungszeichen bei LocalServer32 sind nicht bloß eine Stilfrage. Fehlen sie, lässt sich C:\Program Files\... auch so deuten, dass „C:\Program mit dem Argument Files\... aufgerufen wird“. Das Ergebnis: In einer Umgebung, in der eine Gegenpartei die Berechtigung hat, C:\Program.exe anzulegen, kann genau diese Datei zuerst gestartet werden. Bei Pfaden mit Leerzeichen sind Anführungszeichen deshalb unbedingt zu setzen.

Das Entfernen erfolgt einfach durch Löschen derselben Schlüssel.

set CLSID={1C9B6F4D-1E9A-4E61-9A4F-6A0F1D2D9A11}
set PROGID=KomuraSoft.CalcService

reg delete "HKLM\SOFTWARE\Classes\CLSID\%CLSID%" /f /reg:64
reg delete "HKLM\SOFTWARE\Classes\CLSID\%CLSID%" /f /reg:32
reg delete "HKLM\SOFTWARE\Classes\%PROGID%" /f

Zusätzlich muss sich die EXE beim Start bei COM als „zuständig für diese CLSID“ melden. In C/C++ übernimmt das CoRegisterClassObject, im .NET Framework RegistrationServices.RegisterTypeForComClients. Die Registrierung in der Registry kümmert sich nur darum, dass COM die EXE startet – fehlt diese Meldung, entsteht der schwer zu durchschauende Fehler, dass die EXE zwar startet, sich aber kein Objekt erzeugen lässt.

Zum Ausprobieren während der Entwicklung genügt es übrigens, statt in HKLM mit derselben Struktur unter HKCU\SOFTWARE\Classes zu schreiben – dann ist keine Administratorberechtigung nötig (HKEY_CLASSES_ROOT ist eine Zusammenführung von HKLM und HKCU). Allerdings ist auch HKCU\SOFTWARE\Classes\CLSID für 32-Bit/64-Bit getrennt, sodass sich daran nichts ändert: Es muss weiterhin in beide Ansichten geschrieben werden.

4.3. .NET Framework und .NET (5 und neuer) unterscheiden sich im Vorgehen

Der obige Code ist C#, aber je nachdem, welches .NET Sie verwenden, unterscheidet sich das Vorgehen erheblich. Wer das vermischt, gerät ins Stocken.

  .NET Framework .NET (Core 3.0 / 5 und neuer)
Registrierungswerkzeug RegAsm.exe ist vorhanden (erzeugt wird jedoch die für In-Proc gedachte InprocServer32-Registrierung, LocalServer32 muss also ohnehin selbst geschrieben werden) Ein zu RegAsm gleichwertiges Werkzeug gibt es nicht
Standardmäßige COM-Offenlegung Attribute an der Assembly plus RegAsm <EnableComHosting>true</EnableComHosting> erzeugt *.comhost.dll, registriert mit regsvr32 (nur In-Proc)
Erzeugung der TypeLib (.tlb) Erzeugbar mit TlbExp / RegAsm /tlb Nicht unterstützt. Die IDL wird von Hand geschrieben und mit MIDL kompiliert (ab .NET 6 lässt sich die fertige .tlb in den Comhost einbetten)
Angabe der CLSID Optional Für Klassen, die von COM erzeugt werden sollen, ist eine explizite CLSID zwingend
Umgang mit AnyCPU Von 32-Bit- und 64-Bit-Clients aus nutzbar Die zugehörige *.comhost.dll ist standardmäßig 64-Bit, deshalb nur von 64-Bit-Clients aus nutzbar

Der in diesem Artikel beschriebene Aufbau (EXE-Server) liegt unter .NET (5 und neuer) außerhalb dessen, was das Standard-EnableComHosting abdeckt – die Registrierungslogik muss also selbst geschrieben werden. Als offizielles Microsoft-Beispiel steht OutOfProcCOM zur Verfügung; wer auf .NET-Seite aufbaut, findet dort den Ausgangspunkt.

5. Vollständiger Beispielcode

Ein tatsächlich lauffähiges Beispiel, das die oben beschriebenen Konzepte umsetzt, ist auf GitHub veröffentlicht.

Call64bitDLLFrom32bitProc – GitHub

Das Repository enthält Folgendes:

  • Call64bitDLLFrom32bitProc/ – 64-Bit-COM-LocalServer (EXE)
  • X64DLL/ – 64-Bit-DLL (die eigentliche Verarbeitung)
  • X86App/ – 32-Bit-Client (WinForms)
  • scripts/ – Skripte zum Registrieren und Deregistrieren des COM-Servers

Folgt man den Schritten im README für Build und Registrierung, lässt sich tatsächlich beobachten, wie ein 32-Bit-Prozess eine 64-Bit-DLL aufruft.

6. Zusammenfassung

Eine COM-Bridge ist kein Allheilmittel – es zeigt sich klar, für welche Aufgaben sie geeignet ist und für welche nicht. Bevor Sie sich dafür entscheiden, ordnen Sie Ihren Fall anhand der folgenden Tabelle ein.

Geeignete Fälle Ungeeignete Fälle
Die 32-Bit-Anwendung selbst lässt sich nicht neu bauen (der Umbauaufwand lohnt sich nicht) Die 32-Bit-Seite lässt sich ohnehin auf 64-Bit neu kompilieren (das ist der kürzeste Weg)
Die Aufrufe sind grobkörnig (z. B. ein Bild oder eine Datei pro Aufruf) Zehntausende feingranulare Aufrufe mit hoher Frequenz, etwa Element für Element (der IPC-Overhead dominiert dann)
Ausgetauscht werden Zahlen, Zeichenketten, Arrays und Ähnliches, die sich leicht marshallen lassen Rohe Zeiger oder komplexe eigene Strukturen werden massenhaft hin- und hergeschickt
Auch wenn die 64-Bit-Seite abstürzt, soll die Hauptanwendung weiterlaufen (die Prozesstrennung ist hier ein Vorteil) Sie wollen keine Wiederherstellungslogik für Abstürze oder Neustarts der Serverseite schreiben
Der typsichere Aufruf (IntelliSense, Prüfung zur Kompilierzeit) soll erhalten bleiben Eine einmalige Stapelverarbeitung genügt, und der Austausch über Standard-E/A oder Dateien reicht aus

Als Kehrseite der letzten Zeile lohnt sich auch immer die schlichte Alternative, „die 64-Bit-Verarbeitung als reine Konsolen-EXE auszuführen und über Argumente und Dateien auszutauschen“. Eine COM-Bridge lohnt sich, wenn der typsichere Aufruf erhalten bleiben soll oder wenn ein zustandsbehafteter Server mehrfach aufgerufen werden soll.

Als nächste Schritte empfehlen wir diese Reihenfolge:

  1. Zunächst das Beispiel-Repository aus Kapitel 5 klonen, gemäß README bauen und registrieren, und so einen lauffähigen Zustand vor Ort herstellen.
  2. Eine einzige Funktion Ihrer eigenen 64-Bit-DLL auswählen und der zu ICalcService analogen Schnittstelle eine passende Methode hinzufügen, um sie durchzuspielen.
  3. Sobald das funktioniert, die Zahl der Aufrufe und die Datenmenge pro Aufruf messen. Trifft man die Entscheidung, sich auf grobkörnige Aufrufe zuzubewegen (mehrere Aufrufe zu einem zusammenzufassen), an dieser Stelle vorab, verringert das spätere Nacharbeit.

7. Quellen

  • Überblick über das Component Object Model (COM) https://learn.microsoft.com/en-us/windows/win32/com/component-object-model–com–portal
  • Registrierung von COM-LocalServer32 https://learn.microsoft.com/en-us/windows/win32/com/localserver32
  • Grundlagen der COM-Schnittstelle https://learn.microsoft.com/en-us/windows/win32/com/the-component-object-model
  • COM-Interop (Nutzung aus .NET) https://learn.microsoft.com/en-us/dotnet/standard/native-interop/cominterop
  • WOW64-Registrierungsumleitung (HKLM\SOFTWARE\Classes wird gemeinsam genutzt, der Bereich unter CLSID ist für 32/64 getrennt) https://learn.microsoft.com/en-us/windows/win32/winprog64/shared-registry-keys
  • Komponenten aus .NET (Core / 5 und neuer) gegenüber COM offenlegen https://learn.microsoft.com/en-us/dotnet/core/native-interop/expose-components-to-com

Aktuelle Artikel mit denselben Schlagwörtern führen zu verwandten Themen weiter.

Diese Seiten ordnen den Artikel in einen größeren Leistungs- und Entscheidungskontext ein.

Dieser Artikel ist direkt mit den folgenden Leistungen verbunden.

Häufige Fragen

Fragen, die in Beratungen zu diesem Artikelthema häufig gestellt werden.

Kann eine 32-Bit-Anwendung eine 64-Bit-DLL direkt aufrufen?
Nein. Ein 32-Bit-Prozess kann keine 64-Bit-DLL laden – das ist eine Einschränkung auf Betriebssystemebene, die sich nicht mit einem Kunstgriff umgehen lässt. Der Weg über den gleichen Prozess ist von vornherein versperrt, sodass die Verarbeitung auf der 64-Bit-Seite in einen separaten Prozess ausgelagert werden muss.
Wie nutzt man die Funktionen einer 64-Bit-DLL aus einer 32-Bit-Anwendung heraus?
Der Königsweg ist die Trennung über Out-of-Proc-COM (einen EXE-Server). Die 64-Bit-DLL wird von einem 64-Bit-COM-LocalServer (einer EXE) aus aufgerufen, und die 32-Bit-Anwendung nutzt ihn typsicher über eine COM-Schnittstelle. Die COM-Schnittstelle (IDL/TypeLib) wird geteilt, um die Typen offenzulegen, und die COM-Laufzeit überbrückt die Prozessgrenze mit Proxy/Stub und Interprozesskommunikation.
Worauf muss man bei einer COM-Bridge-Architektur achten?
Es gibt vor allem drei Punkte. Erstens sind die Registrierungen für 32-Bit und 64-Bit getrennt (einschließlich WOW6432Node). Zweitens erfordern eigene Strukturen ein durchdachtes Marshalling-Design. Drittens entsteht durch die Interprozesskommunikation ein Overhead, weshalb bei häufigen, feingranularen Aufrufen Vorsicht geboten ist – es ist besser, auf Sammelverarbeitung statt auf viele einzelne Aufrufe zu setzen.
Gibt es tatsächlich lauffähigen Beispielcode?
Ja. Im GitHub-Repository Call64bitDLLFrom32bitProc ist ein vollständiges Beispiel veröffentlicht, das einen 64-Bit-COM-LocalServer (EXE), eine 64-Bit-DLL, einen 32-Bit-Client (WinForms) sowie Skripte zum Registrieren und Deregistrieren des COM-Servers enthält. Folgt man den Schritten im README für Build und Registrierung, lässt sich beobachten, wie ein 32-Bit-Prozess tatsächlich eine 64-Bit-DLL aufruft.

Autorenprofil

Profilseite des Artikelautors.

Go Komura

Geschäftsführer von KomuraSoft LLC

Spezialisiert auf Windows-Softwareentwicklung, technische Beratung und Fehleranalyse, insbesondere bei bestehenden Systemen und schwer reproduzierbaren Störungen.

Zurück zum Blog