Lessons Learned – COM Interop

Beim Fernsteuern von Office-Anwendungen ist generell Vorsicht angesagt. Das gilt aber insbesondere, wenn die Fernsteuerung aus .Net heraus erfolgt. Wieviel Vor- und Umsicht tatsächlich notwendig ist, mußten wir in der letzten Phase vor Auslieferung der ersten Version einer WinForms-Anwendung kürzlich schmerzlich erfahren. Um diesen Schmerz anderen Entwicklern in Zukunft zu ersparen, hier ein paar „Lessons Learned“:

COM/COM Interop – Kurze Einführung

COM ist lange, lange Jahre Microsofts Objektmodell gewesen und was die Menge an existierendem COM-Code anbelangt auch sehr erfolgreich. Das später dazugekommene DCOM zur verteilten Komponentenentwicklung (als Gegenstück zu CORBA) war aus mancherlei Gründen weniger erfolgreich und Microsoft bastelte bald an einer neuen Variante. Diese neue Variante entwickelte sich intern schnell weiter und es wurde daraus das, was wir heute unter der „.Net-Plattform“ kennen. Es ist also kein Wunder, daß Microsoft einige Anstrengung unternommen hat, die alte COM-Welt nicht einfach zu vergessen und damit die unzähligen existierenden Komponenten unbrauchbar zu machen. Stattdessen ist tief in .Net die Möglichkeit integriert, COM-Objekten (beinahe) so zu verwenden wie native .Net-Komponenten. Dieser Mechanismus wird „COM Interop“ genannt.
Es gibt jedoch einen entscheidenden Unterschied zwischen beiden Modellen: .Net-Code ist sogenannter „managed Code“, wird von einer Runtime, der sogenannten CLR (Common Language Runtime) verwaltet, was in etwa der Java VM entspricht. Zu den Aufgaben dieser Runtime gehört unter anderem die Speicherverwaltung, auch Garbage Collection (GC) genannt. In .Net (wie in Java) hat man als Entwickler wenig Kontrolle darüber, wie, wann und ob überhaupt der GC nicht mehr benötigte Objekte freigibt und damit auch von diesen benutzte Ressourcen. Ganz anders bei COM. Der überwiegende Teil der COM-Komponenten wurde sicher in C/C++ erstellt, also in unmanaged Code. Hier ist der Entwickler selbst für Speicher-Allokation und -Freigabe zuständig. Treffen diese beiden Welten aufeinander stellt sich die Frage, wer denn nun die Verwaltung der COM-Objekte übernimmt. Nach wie vor kann man das als Entwickler von .Net-Code der CLR bzw. dem GC überlassen. Aber da sind wir auch schon bei dem nächsten Problem:

Wenn man in .Net eine Referenz auf eine COM-Bibliothek anlegt, wird ein sogenannter RCW (Runtime Callable Wrapper) erstellt, eine Art Proxy für das COM-Objekt. Hier findet das Marshalling der Daten statt. Pro COM-Objekt gibt es immer nur einen RCW, unabhängig von der Anzahl der Instanzen. Hierbei spielt das sogenannte „Appartment Model“ eine Rolle. Unter COM gibt es verschiedener solcher Modelle, im wesentlichen die beiden MTA und STA, Multi Threaded Appartment und Single Threaded Appartment. Das Appartment besagt, ob eine COM-Komponente von verschiedenen Threads (MTA) oder nur von einem einzigen (STA) angesprochen werden kann. Um diese beiden Varianten in .Net abzubilden, hat dort jeder Thread ebenfalls eines der beiden Appartments, default ist „MTA“. Durch ein Attribut an der Main()-Methode wird das Appartment aber eigentlich immer auf STA gestellt, um Probleme mit eben diesen STA-COM-Objekten zu vermeiden.

Pitfalls

Solange man direkt aus dem Haupt-Thread mit COM-Objekten arbeitet und das nicht exzessiv, hat man eigentlich keine Probleme. Die eigentliche Arbeit wird einem von der CLR bzw. dem RCW abgenommen. Fängt man jedoch an, etwas komplexere Aufgaben zu erledigen und dies auch durch Einsatz von Multi-Threading wird es schnell unangenehm. Bei den ersten etwas umfangreicheren Tests einer unserer WinForms-Anwendungen zeigte sich, daß es zu sehr unschönen Effekten kommen kann. In bestimmten Fällen werden dort zwei Hintergrund-Threads erzeugt, die jeweils über COM Outlook fernsteuern. Leider stürzte die Anwendung dabei auf einigen Rechnern regelmäßig nach einigen hundert Aktionen ab. Manchmal mit einer „MemoryAccessViolation“ und manchmal einfach ohne irgendeine Nachricht, da war die Anwendung einfach mal eben weg. Wenn wir das Glück hatten, zumindest eine Fehlermeldung zu bekommen, deutete der Stacktrace auf ein Problem in der Anzeigekomponente, genauer gesagt im TreeView hin. Tagelange Suche nach Problemen dort zeigte aber keinerlei Auffälligkeiten in diesem Teil des Codes.
Da das Problem auf zumindest einem Rechner mehr oder weniger deterministisch auftrat deutete das Bauchgefühl relativ schnell auf COM-Interop hin. Nach tagelangen Recherchen ergaben folgende Maßnahmen eine Lösung:

  • Sicherstellung, daß immer nur ein Thread mit den Outlook COM-Objekten arbeitet (mittels „lock()“-Statements)
  • Sicherstellung, daß der ausführende Thread das Appartment-Modell des COM-Objektes verwendet (in diesem Fall STA). Daraus folgt auch, daß man leider keine ThreadPools verwenden kann, da diese mit MTA-Threads arbeiten und man das Appartment nachträglich nicht ändern kann.
  • Sicherstellung, daß jedes im Code erzeugte COM-Objekt (durch „new“) durch einen Aufruf von „Marshal.ReleaseComObject()“ freigegeben wird, bevor der GC aktiv werden muß. Sicherstellen kann man das durch „try/finally“-Blöcke.

Ich hoffe, diese Informationen werden irgendwann einmal jemandem helfen, die Fehler, die wir gemacht haben, zu vermeiden. Vielleicht helfen sie ja mir selbst beim nächsten Projekt… 🙂

Tags: , , ,

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.