Inhaltsverzeichnis         

© Martin Korneffel, Stuttgart 2005 +++ email: trac@n.zgs.de +++ web: www.s-line.de/homepages/trac

ComInterop

Einführung

Die .NET Laufzeitumgebung weist deutliche Unterschiede zu den klassischen Laufzeitumgebungen wie C/C++ oder COM auf. So hat z.B. die COM- Laufzeitumgebung eine völlig andere Speicherverwaltung wie die .NET Laufzeitumgebung. Auch die fundamentalen Datentypen wie Strings haben in beiden Laufzeitumgebungen unterschiedliche Binärformate. Ein COM- Objekt ist deshalb aus der .NET - Welt nicht direkt ansteuerbar. Umgekehrt gilt das gleiche. Es müssen zwischen der .NET und den klassischen Laufzeitumgebungen Brücken geschlagen werden.

PInvoke vs. COM Interop

Die zwei Mechanismen zum Datentransfer zwischen verwalteter und unverwalteter Welt sind der Plattformaufruf (PInvoke) für klassische DLL's und die COM Interop.

Der PInvoke- Mechanismus ist eingeschränkter als die COM Interop, wie folgende Übersicht zeigt:

Plattformaufruf

COM Interop





  • Nur .NET Client kann eine unverwaltete Methode aufrufen, nicht umgekehrt

  • Kein Aufruf von unverwalteten Klassen oder Objekten möglich

  • .NET Client kann COM Objekte instanziieren und über Wrapper- Klassen steuern

  • COM Client kann .NET Objekte instanziieren und über COM- Schnittstellen steuern, die ein Wrapper bereitstellt

Interop- Marshalling: Daten zwischen verwalteter und unverwalteter Welt austauschen

Unter marshalling (engl. rangieren) wird der Transfer der Daten über Methodenparameter zwischen verwalteter und unverwalteter Welt verstanden. Dieser erfolgt automatisiert nach einem Regelwerk. Durch spezielle Attribute kann das Regelwerk vom Entwickler übersteuert werden.

Richtung des Datenfluss definieren

Eine Methode kann reine Eingabe, Ausgabe und Ein/Ausgabeparamter besitzen. Dabei werden Parameterspezifikationen wie folgt interpretiert:


In

Out

In/Out

VB.NET

ByVal

ByRef

ByRef

C#

-

out

ref

Die Datenflussrichtung beeinflusst den Aufwand und die Sicherheit beim Marshalling. Ein In- Wertetyp muß höchstens in den Stack/Heap vom Server kopiert werden. In/Out Wertetypen erfordern zusätzlich das Zurückkopieren des Ergebnisses in den Client- Stack/Heap.

Der Entwickler kann den Datenfluss mittels Attribute feiner steuern:

InAttribute()
OutAttribute()

Blitfähige Typen

Wertetypen in der verwalteten Welt unterliegen einer ähnlichen Speicherverwaltung (Stack) wie in der unverwalteten. Wenn die Binärformate sich zwischen beiden Welten nicht unterscheiden, dann können die verwalteten Werte durch eine 1:1 Kopie aus den nichtverwalteten Werten erstellt werden und umgekehrt. Man spricht auch von blitfähigen Typen.

Folgende verwaltete Typen sind blitfähig:

void, byte, int, float, *(Pointer)

MarshalAsAttribute

Das Resultat eines Datentransfers zwischen verwalteter und unverwalteter Welt ist nicht immer eindeutig. So kann ein verwaltetes Boolean in verschiedene unverwaltete Darstellungen überführt werden. Mit dem MarshalAsAttribute kann das Zielformat definiert werden

void TesteZahl([In] string zahl, [Out] [MarshalAs(UnmanagedType.VariantBool)] bool gueltig)

PInvoke- Plattformaufruf

Soll eine Methode aus einer unverwalteten DLL aufgerufen werden, dann muß der Einsprungpunkt zuvor deklariert werden mittels eines Attributes. Im folgenden Beispiel wird der Aufruf einer Messagebox aus der user32.dll des Betriebssystems demonstriert:

using System;
using System.Collections.Generic;
using System.Text;


using System.Runtime.InteropServices;

namespace u18_Interop
{
    class Program
    {
        // Einsprungpunkt wird deklariert
        [DllImport("user32.dll")]
        //private static extern Int32 MessageBox(IntPtr hwnd, String ptext, String pcaption, Int32 utype);
        private static extern Int32 MessageBox(IntPtr hwnd, StringBuilder ptext, StringBuilder pcaption, Int32 utype);
        
        static void Main(string[] args)
        {

            // Messagebox aufrufen
            //MessageBox(new IntPtr(0), "Hallo Welt", "Win32- MsgBox", 0);

            StringBuilder b1 = new StringBuilder("Hallo Welt");
            StringBuilder b2 = new StringBuilder("bla");

            MessageBox(new IntPtr(0), b1, b2, 0);
        }
    }
}

COM Interop

In COM werden Objekte instanziiert und über Schnittstellen gesteuert. COM Anwendungen stellen Server als auch Clients dar. Schnittstellen und Objekte haben unter COM ein ähnlich Bedeutung wie unter .NET. Der Interop- Mechanismus muss COM Schnittstellen in die .NET Welt einbetten, und .NET Klassen über Schnittstellen aus der COM- Welt steuern.

Dies geschieht durch sog. Wrapper.

COM- Schnittstellen werden in die .NET Welt über Runtime Callable Wrapper (RCW) als Klassen eingebettet.

.NET Objekte werden durch Com Callable Wrapper (CCW) in der COM Welt über Schnittstellen steuerbar.

RCW

RCW steht für Runtime Callable Wrapper. Ein RCW ist eine .Net Objekt, das eine COM Schnittstelle in der verwalteten Welt kapselt.

Generiert wird der RCW durch die Common Language Runtime (CLR) zur Laufzeit beim Zugriff auf eine COM- Schnittstelle. Dabei werden IL- Metadaten über die COM- Schnittstelle ausgewertet, die zuvor mit dem tlbimp.exe Tool aus den COM Typbibliotheken des COM Servers gewonnen wurden. Das tlbimp.exe Tool erzeugt eine sog. Interop Assembly.




Die Interop- Assembly wird in Visual- Studio automatisch erstellt, wenn ein Verweis auf eine mittels tlbimp.exe erstellte Interop-Assembly gesetzt wird.

Beispiel 1: Aufruf des Internet- Explorers

  1. Verweis im Projekt auf Com- Komponente Microsoft Internet Controls anlegen-> Erzeugt RCW unter Namespace SHDocVw

  2. Instanziieren der RCW- Klasse SHDocVw.InternetExplorerClass

  3. Laden der Website "www.tracs.de" mittels der Navigate- Methode

  4. Warten, bis Seite geladen wurde mittels Busy- Methode

  5. Explorerfenster anzeigen durch setzen von Visible auf true

VB

   Module Module1

    Sub Main()

        Try
            Dim ie As New SHDocVw.InternetExplorer()

            ' Vollbildansicht
            ie.TheaterMode = True

            ' Webseite aufrufen
            ie.Navigate("http://www.tracs.de")

            ' Warten, bis der Ladevorgang beendet wurde
            While (ie.Busy)
                System.Threading.Thread.Sleep(100)
            End While

            ' Browserfenster sichbar machen
            ie.Visible = True

            Console.WriteLine("Beenden ?")
            Console.ReadLine()

            ' Browser schließen
            ie.Quit()
        Catch ex As Exception
            Console.WriteLine("Fehler: {0}", ex.Message)
        End Try

    End Sub

End Module

C#

using System;
using SHDocVm;

namespace ComIeTest {

  class Class1 {
     
     [STAThread]
     static void Main(string args[]) {
    
         InternetExplorerClass ie = new InternetExplorerClass();

         string empty = String.Empty;

         ie.navigate("http://www.google.de", ref empty, ref empty, ref empty, ref empty);
         ie.visible = true;

         Console.WriteLine("Gelesen ?");
         Console.ReadLine();

         ie.Quit();        

     }
  }
}

Beispiel 2: Auflisten von Browserfenstern

SHDocVw.InternetExplorer browser = null;
  string filnam;
  
  SHDocVw.ShellWindows shellWindows =
    new SHDocVw.ShellWindowsClass();
    
  foreach (SHDocVw.InternetExplorer ie
      in shellWindows)
  {
    filnam = Path.GetFileNameWithoutExtension(
      ie.FullName).ToLower();
    
    if (filnam.Equals("iexplore"))
    {
      browser = ie;
      break;  // i hate 'break' but it's easy here
    }
  }

Einbinden des Webbrowsers als ActiveX- Control

ActiveX Controls werden im Visual Studio der Toolbox hinzugefügt. Z.B. kann der Microsoft Webbrowser als Control eingefügt werden, um mit einer Winform- Anwendung einen spezialisierten Browser zu entwickeln.

In der Form- Load- Methode der WinForm kann dann der Webserver wie folgt mit einer Startseite geladen werden:

private AxSHDocVw.AxWebBrowser axWebBrowser1;

private void Form1_Load(object sender, System.EventArgs e)
{
   object empty = String.Empty;
   this.axWebBrowser1.Navigate("http://localhost/trac", ref empty, ref empty, ref empty, ref empty);
   this.axWebBrowser1.Visible=true;
}

Debuggen vom COM Servern über .NET Clients

Um aus .NET Clients Com- Server zu debuggen, muß im .NET Projekteigenschaften folgender Schalter gesetzt werden:

Konfigurationseigenschaften/Debuggen/Nicht verwaltetes debuggen aktiv = true

Office- Automation mit .NET

Die MS- Office Anwendungen bieten ihre Funktionalität über COM- Komponenten zwecks Automatisierung an. So sind z.B. über eine Dialogbox Messwerte eingegeben werden. Anschließend kann per Knopfdruck eine Auswertung in einem Excel- Arbeitsblatt erstellt werden.

Um die Zusammenarbeit mit .NET zu Optimieren, wird das Officepaket mit optimierten COM Wrapper- Assemblies ausgeliefert, genannt Primary Interop Assemblies (PIA). Sie können mit dem Installationsprogramm von Office 2003 nachträglich hinzugefügt werden über [Features hinzufügen oder entfernen]/[Erweiterte Anpassung von Anwendungen]/Excel/.NET-Programmunterstützung.

Namespaces

Die PIA für Excel spannt folgenden Namespace auf:

Microsoft.Office.Interop.Excel

Beispiel

Imports Microsoft.Office.Interop.Excel
Module Module1

    Sub Main()
        Dim ExApp As New Application()
        With ExApp
            .Visible = True
            .Workbooks.Add()
            .Range("A1").Value = "Messwerte"
            For n As Short = 2 To 13
                ExApp.ActiveSheet.Cells(1, n).Value = New Random().Next(1, 100)
            Next
        End With

        Dim R As Range = ExApp.ActiveSheet.Range("A1:L1")
        Dim ch As ChartObject = ExApp.ActiveSheet.ChartObjects.Add(10, 30, 500, 300)
        With ch.Chart
            .ChartType = XlChartType.xl3DColumn
            .SetSourceData(R)
            .HasTitle = True
            .ChartTitle.Characters.Text = "Ein Zufallschart"
        End With
    End Sub

End Module

CCW

Wenn bestehende COM- oder VB6 Applikationen mit .NET Komponenten aufgerüstet werden sollen, müssen diese für COM- Interop in der Registry verzeichnet werden. Dazu werden in der Registry unter Prog- und Class- ID's der Speicherort der Assembly mit den NET- Klassen beschrieben.

Wenn eine registrierte .NET- Klasse von einem COM- Client instanziiert wird, dann instanziiert die CLR ein .NET Objekt. Anhand der Metadaten der .NET Klasse wird für das .NET Objekt (.NET Server) ein Com Callable Wrapper (CCW) generiert. Die COM- Clients können über eine gewöhnlichen Schnittstellenzeiger auf den CCW zugreifen, wie auf jedes andere COM- Objekt. Der CCW ist ein Proxy, der jeden Methodenaufruf an das .NET Objekt in der CLR weiterleitet.



Klassenschnittstellen

Der CCW wird von der CLR aus den Metadaten der Klasse automatisch erzeugt. Diese Metadaten werden auch als Klassenschnittstelle bezeichnet. Die Klassenschnittstelle kann vom Entwickler beeinflusst werden mittels des Attributes ClassInterfaceType:

<ClassInterface(ClassInterfaceType.None|AutoDispatch|AutoDual)>
Public Class DirTreeVb    
    '…
End Class

Wird ClassInterfaceType.AutoDispatch oder AutoDual eingestellt, dann erzeugt die CLR den CCW aus den aktuellen Metadaten der Klassenimplementierung.

Wird hingegen ClassInterfaceType.None eingestellt, dann wird der CCW aus den Metadaten der Schnittstellen gewonnen, die die .NET Klasse explizit implementiert. Dies ist die empfohlene Einstellung, da so die Implementierungsdetails der .NET Klasse vor den COM- Clients verborgen wird. Dies fördert die Stabilität im Mix COM/.NET, da früh bindende COM- Clients nicht mehr sicher laufen können, wenn die Klassenimplementierung geändert wird.

Schnittstellen für Ereignissenken

COM und .NET unterscheiden sich bei der Implementierung von Ereignissen und Ereignishandler.

In .NET sind Events Objekte, deren Typ von der System.Delegate Klasse abgeleitet ist. Diese verwalten die Einsprungpunkte der Eventhandler.

In COM werden Events durch Objekte mit der Schnittstelle IConnectionPoint implementiert. Diese verwalten Schnittstellenzeiger vom Typ ISinkEvents. Dieser Mechanismus wird auch als Verbindungspunkt bezeichnet.

Um COM- Eventhandler an .NET Events zu binden, muß ein COM Verbindungspunkt im .NET Objekt eingerichtet werden. Dies erfolgt durch das Attribut ComSourceInterfaces :

<ClassInterface(ClassInterfaceType.None), _
 ComSourceInterfaces(GetType(Schnittstellenname)), _
 ProgId("VBKurs.DirTreeVb")> _
Public Class DirTreeVb
    Implements IDirTreeVb
    '…
End Class

Mittels ComSourceInterfaces können bis zu vier Schnittstellen für COM- Ereignissenken definiert werden. Auch die Schnittstellen der Ereignissenken werden in .NET definiert. Um sie in der COM- Welt identifizierbar zu machen, muss ihnen jeweils eine GUID zugewiesen werden:

Imports System.Runtime.InteropServices

' Com Event Sink Schnittstelle definieren
' Durch diese Schnittstelle können Com- Clients Eventhandler registrieren für die DirTreeVb
' Ereignisse
<Guid("8AAE6562-EA03-410b-9D4F-CF42CF0CCDB0"), InterfaceType(ComInterfaceType.InterfaceIsIDispatch)> _
Public Interface DirTreeVbEvents

    Sub EventProgressCom(ByVal CountDirs As Integer, ByVal CountFiles As Integer)
    Sub EventEndScanDirCom(ByVal CountDirs As Integer, ByVal CountFiles As Integer)

End Interface

Insgesamt ergibt sich folgendes Schema für die Implementierung einer .NET Klasse, die auch von COM Clients nutzbar ist:


Beispiel:

' Empfohlene Vorgehensweise: Automatische Generierung der Com- Schnittstelle abschalten
' Stattdessen explizite Implementierung einer Verwalteten Schnittstelle, die als Vorlage
' für die ComSchnittstelle dient
' Über ComSourceInterface wird die Schnittstelle der Ereignisquelle für die Com- Automatisierung 
' definiert.
<ClassInterface(ClassInterfaceType.None), _
 ComSourceInterfaces(GetType(DirTreeVbEvents)), _
 ProgId("VBKurs.DirTreeVb")> _
Public Class DirTreeVb
    Implements IDirTreeVb
    '-----------------------------------------------------------------------------
    ' Member zur Ausgabe des Arbeitsfortschrittes

    ' Ereignis: Arbeitsfortschritt    
    Public Event EventProgressCom As DGEventProgressComClient

    ' Generator für Arbeitsfortschrittmeldungen: Kann in abgeleiteten Klassen 
    ' überschrieben werden, um detailiertere Arbeitsfortschrittmeldungen, die von
    ' DirTreeProgressInfo abgeleitet sind, zu erzeugen
    Protected Overridable Function MakeProgressInfo() As DirTreeProgressInfo
        Return New DirTreeProgressInfo(m_dir_count, m_file_count)
    End Function

    'Ereignis: Scan beendet 
    Public Event EventEndScanDirCom As DGEventProgressComClient

    '-----------------------------------------------------------------------------------
    ' Konstruktoren
    
    ' Defaultkonstruktor. Wurde zwecks Com- Interoperabilität hinzugefügt
    Private logHnd As mko.SystemEventLogHnd
    Public Sub New()
        log = New mko.CLogVb  
        ' ...      
    End Sub

    ' Routine, die den rekursiven Dateibaumdurchlauf in einem gesicherten Kontext startet
    Public Function scanDir(ByVal root_path As String) As Boolean Implements IDirTreeVb.scanDir
        ' ...
    End Function

End Class

Einschränkungen

Klassen, die einen CCW- erhalten sollen, müssen folgende Einschränkungen genügen:

  1. sie müssen Public sein

  2. sie müssen einen parameterlosen Konstruktor besitzen (Sub New() in VB.NET)

  3. sie müssen über öffentliche Eigenschaften und Methoden verfügen

Assemblies mit COM- sichtbaren Klassen müssen:

  1. mittels dem Tool regasm registriert werden

  2. optional: signiert werden (snk)

  3. optional: Installation im GAC (Achtung: Registrierungsschlüssel mit Angabe des Speicherortes der Assembly nicht vergessen)

Registrierung

Die Registrierung kann in Visual Studio 2005 ab der Professional- Version automatisch erfolgen, indem die Projekteinstellung Kompilieren/Für Com Interop registrieren gesetzt wird.

Alternativ kann die Registrierung einer CCW- Klasse auch mittels dem Tool regasm.exe erfolgen. Diese bietet sich auch an, wenn die bei der Registrierung vorgenommenen Einstellungen schnell geprüft werden sollen, indem diese in eine Textdatei geschrieben werden:

//Fall 1: Registrieren, wird aber später im GAC installiert
c:\..\regasm MyComLib.dll 

//Fall 2: Registrieren und definieren des Speicherortes. Kann anschliessend sofort verwendet werden
c:\..\regasm MyComLib.dll /codebase

//Fall 3: Wie Fall 2, jedoch sind alle Registrierungseinträge in eine Datei geschrieben worden zwecks Analyse
// wenn Anschließend in den GAC
c:\..\regasm MyComLib.dll /regfile:MyComLib.reg 

// oder ohne GAC
c:\..\regasm MyComLib.dll /codebase /regfile:MyComLib.reg 

Bei der Registrierung werden die CCW- Klassen einer sog. ProgID zugeordnet. Diese besteht aus zwei Teilen, die durch einen Punkt getrennt sind. Der erste Teil ist der Namespace, und der zweite der Klassenname.

ProgID :<=> Namespace.Classname

Die Klassen selber sind z.B. unter VB6 im Objektbrowser unter einem Namespace=Name der Assembly zu finden.