COM Wars – wenn .NET zu schlau ist…

Im Rahmen von DVB.NET 3.1 möchte ich in einen DirectShow (COM basiert) Filtergraphen eine .NET Klasse einbinden. Die Klasse klemmt sich in den Datenstrom zwischen einen TS Capture Filter und einen Demultiplexer – nicht wirklich ein Problem. Auf der Eingangsseite (Input Pin) der .NET Klasse werden über eine COM Schnittstelle IMemInputPin so genannte Media Samples (IMediaSample) entgegengenommen. Die Klasse macht damit was und gibt sie dann unverändert an die gleichartige Schnittstelle des Demultiplexers an der Ausgangsseite (Output Pin) weiter. Es kommt ein Fehler 0x80040155 (REGDB_E_IIDNOTREG)!

Dieser Fehler ist ein COM Marshalling Fehler. Er bedeutet, dass COM versucht hat, eine Schnittstelle aus einem Apartment in ein anderes zu transferieren, dabei aber keinen Eintrag in der Registery (HKCR\Interface) für die Schnittstelle gefunden hat und die betroffenen COM Klasse auch kein Custom Marshalling anbietet (IMarshal).

Nun gut, was könnte also hier passieren? Der Filtergraph wird im Hauptprogramm, einer Windows Form, angelegt, damit auch alle Filter (COM Komponenten). Der Natur der Sache nach wird dazu ein Single-Threaded-Apartment (STA) verwendet. Durch die Gegebenheiten in einem DirectShow Graphen muss ich allerdings die Weitergabe der Media Samples auf einem eigene Thread machen (sonst blockiert der Graph). Das kann natürlich nie dasselbe Apartment sein, wie das Heimatapartment der Komponenten.

Nun, wie macht Microsoft das? Ein Blick in das DirectShow SDK zeigt, dass in Graphen COM Regeln wohl etwas lockerer gesehen werden. Das Infinite Tee Beispiel von Microsoft nimmt COM Schnittstellen wie sie kommen und nutzt sie ohne eine Marshalling gnadenlos auf einem anderen Thread. Nun gut, machen wir das mit .NET. Geht aber nicht (so einfach)!

Der erste Versuch, die .NET Schnittstelle einfach im anderen Thread zu verwenden, hatten wir oben schon. Auch der Trick, im STA ein Marshal.QueryInterface zu machen, fruchtet nicht. Man erhält hier tatsächlich eine IntPtr auf die COM Schnittstelle zum direkten Zugriff. Packt man diese in dem Worker Thread aber via Marshal.GetObjectFor IUnknown aus, ist .NET wieder so clever, ein Marshalling anzustossen. Böse Falle.

Ok, es geht doch, aber was jetzt kommt ist zumindest verboten 🙂 Also mit Vorsicht geniessen! Das ganz fängt mit folgendem Code Fragment an (da gab es einige Zwischenschritte bis zu genau dieser Lösung, die es tut – Details erspare ich hier mal):


private delegate void MediaSampleSink(IntPtr classPointer, IntPtr[] sampleArray, Int32 sampleCount, out Int32 processed);

private MediaSampleSink m_MemSink = null;
private IntPtr m_MemPin = IntPtr.Zero;

m_MemPin = Marshal.GetComInterfaceForObject(m_Connected, typeof(Interfaces.IMemInputPin));

IntPtr comFunctionTable = Marshal.ReadIntPtr(m_MemPin);
IntPtr receiveMultiple = Marshal.ReadIntPtr(comFunctionTable, 28);

m_MemSink = (MediaSampleSink)Marshal.GetDelegateForFunctionPointer(receiveMultiple, typeof(MediaSampleSink));

Aus der Eingangsadresse des anderen Filters (m_Connected ist der IMemInputPin) wird die COM Schnittstelle ermittelt. Dann relativ trivial die Adresse der Funktionentablle und schließlich die Adresse der 7ten Methode (IMemInputPin:ReceiveMultiple). Der erstellte Delegate berücksichtig dabei, dass bei einem COM Aufruf der zusätzliche, normalerweise unsichtbare, erste Parameter die Adresse des COM Objektes ist.

Nun funktioniert folgender Aufruf wie gewünscht:

IntPtr[] toProcess = ...;
Int32 processed;
m_MemSink(m_MemPin, toProcess, toProcess.Length, out processed);

Wie gesagt: eigentlich werden hier COM Regeln auf das übelste verletzt. Es fragt sich allerdings, ob in einem DirectShow Graphen nicht wirklich andere Regeln gelten, bei denen COM Schnittstellen nur zur Zerlegung in Komponenten verwendet werden, Apartments aber keine Rolle spielen. Müßte ich mal recherchieren – ich hoffe im Moment einfach mal, dass es so ist (zudem alle Microsoft DirectShow Beispiele es auch so handhaben).

Immerhin, ein kleiner Schritt in Richtung DVB.NET 3.1!

Viel Spaß

Jochen

Über die Notwendigkeit der Deinstallation und andere Widrigkeiten der Installation

Inzwischen habe ich mich dazu durchgerungen bei VCR.NET zu empfehlen, vor einer Neuinstallation die vorherige Version zu deinstallieren – die verbleibenden Konfigurationsdateien sorgen dafür, dass die darauf folgende Installation mit den alten Einstellungen sofort loslegen kann.

Tatsächlich ist die Installation dafür vorgesehen, automatisch eine vorhandene Version zu deinstallieren und eine Installation unter Beibehaltung der Konfiguration durchzuführen. Das klappt auch – fast immer. Leider hat .NET ein paar Fallstricke, die ich zwar versuche, zu umgehen, aber ich bin mir nicht sicher, ob ich immer daran denke, denn wie gleich beschrieben ist es etwas lästig.

Die VCR.NET Installation erfolgt auf Basis eines MSI Setup Projektes, dass von Visual Studio 2005 angeboten wird. Da neben dem reinen Kopieren auch spezifischer Code ausgeführt werden muss, enthält das Projekt auch eine Installationskomponente und da liegt der Haken. Wenn eine Deinstallation ausgeführt wird, so muss diese Komponente geladen und ausgeführt werden – schön und gut. Genauso bei der Installation – auch klar, also wo liegt das Problem? Wenn Deinstallation und Installation in einem Schritt erfolgen und die Installationskomponente den gleichen Namen trägt, dann wird die Assembly im neuen MSI für die Installation nicht geladen – ist ja schon eine Assembly mit gleichem Namen im Speicher, wird schon die selbe sein, denkt sich .NET wohl. Ist es aber oft nicht! Darum muss ich bei jeder Änderung an der Installation (und das war für die 3.0 so einiges an Kleinkram) die Installationskomponente umbennen (der Name heute ist JMS.DVBVCR.Installer-14.dll, keine Ahnung, ob das im 3.0 Final auch so sein wird). Wenn ich das vergessen, passieren komische Dinge bei denjenigen, die genau ein Upgrade zwischen den Versionen machen, die den gleichen Namen aber unterschiedlichen Code für die Installationskomponente enthalten.

Genauso kann (aus anderen Gründen) keine Deinstallation auf Basis von .NET 1.1 (VCR.NET 2.6 oder früher) auf eine .NET 2.0 Version (2.7 oder später) in einem Schritt erfolgen. Wieder eine Ausnahme, die man erklären muss.

Wie gesagt: im Prinzip ist das Problem verstanden und im Griff, aber wer einfach nicht nachdenken möchte, macht erst mal die Deinstallation. Ich glaube sogar nicht mal, dass das viel langsamer ist.

Ja, und dann noch ein .NET Spielchen: bei der ersten Installation einer 3.0 Variante muss ein DVB.NET Geräteprofil ausgewählt werden. Um das zu vereinfachen, habe ich einen kleinen Assistenten gebastelt. Leider kennt die Installation nicht das Konzept des Hauptfensters einer Anwendung nicht (naja: vielleicht habe ich es auch nicht gefunden) und der Assistent wird ohne Owner / Parent geöffnet. Trotz diverser Anstrenungen (Top Fenster, ganz nach vorne geholt, …) passiert es ab und zu immer noch, dass der Assistent hinter dem Installationsfenster geöffnet ist und unsichtbar bleibt, bis man das Installationsfenster wegschiebt. Die Suche habe ich vorerst aufgegeben: der Assistent erscheint nun in der Windows Taskbar (wollte ich eigentlich vermeiden), so weiß man wenigstens, dass da was ist und kann ihn damit nach vorne holen.

So long

Jochen