MSDN Magazin > Home > Ausgaben > 2008 > January >  Tiefe Einblicke in CLR: Marshalling zwischen ve...
Tiefe Einblicke in CLR
Marshalling zwischen verwaltetem und nicht verwaltetem Code
Yi Zhang and Xiaoying Guo

Zugegeben: Die Welt ist nicht perfekt. Sehr wenige Unternehmen führen Entwicklungsvorgänge vollständig in verwaltetem Code durch. Außerdem gibt es jede Menge veralteten, nicht verwalteten Code, der in die Arbeit mit einbezogen werden muss. Wie können Sie verwaltete und nicht verwaltete Projekte miteinander integrieren? Sollten Sie dazu nicht verwalteten Code von einer verwalteten Anwendung oder verwalteten Code von einer nicht verwalteten Anwendung aus aufrufen?
Glücklicherweise stellt die Microsoft® .NET Framework-Interoperabilität eine Verbindung zwischen verwaltetem und nicht verwaltetem Code her. Marshalling spielt dabei eine sehr wichtige Rolle, da es den Datenaustausch zwischen beiden ermöglicht (siehe Abbildung 1). Viele verschiedene Faktoren wirken sich darauf aus, wie die CLR Daten zwischen der nicht verwalteten und der verwalteten Umgebung marshallt, beispielsweise durch Attribute wie [MarshalAs], [StructLayout], [InAttribute] und [OutAttribute] sowie durch Sprachschlüsselwörter wie „out“ und „ref“ in C#.
Abbildung 1 Das Schließen der Lücke zwischen verwaltetem und nicht verwaltetem Code (Klicken Sie zum Vergrößern auf das Bild)
Aufgrund der großen Zahl dieser Faktoren kann richtiges Marshalling eine Herausforderung sein, da hierzu ein gutes Verständnis vieler Details in nicht verwaltetem Code und verwaltetem Code erforderlich ist. In diesem Artikel geht es um die grundlegenden, aber dennoch verwirrenden Themen, denen Sie sich bei normalen Marshallingvorgängen gegenüber sehen. Es geht dabei nicht um das benutzerdefinierte Marshalling, das Marshalling komplexer Strukturen oder andere Themen für Fortgeschrittene, doch mit fundierten Kenntnissen der Grundlagen werden Sie in der Lage sein, auch diese Themen in Angriff zu nehmen.

[InAttribute] und [OutAttribute]
Beim ersten hier erörterten Marshallingthema geht es um die Verwendung von InAttribute und OutAttribute, Attributtypen, die sich im System.Runtime.InteropServices-Namespace befinden. (Wenn Sie diese Attribute in Ihrem Code anwenden, ermöglichen C# und Visual Basic® die Verwendung der abgekürzten Formen [In] und [Out], doch hier werden die vollständigen Namen verwendet, um Verwirrung zu vermeiden.)
Beim Anwenden auf Methodenparameter und Rückgabewerte steuern diese Attribute die Marshallingrichtung und werden deshalb als direktionale Attribute bezeichnet. [InAttribute] teilt der CLR mit, Daten vom Aufrufer zum Aufgerufenen zu Beginn des Aufrufs zu marshallen, während [OutAttribute] der CLR mitteilt, bei der Rückgabe vom Aufgerufenen zurück zum Aufrufer zu marshallen. Beim Aufrufer sowie beim Aufgerufenen kann es sich um nicht verwalteten oder verwalteten Code handeln. In einem P/Invoke-Aufruf beispielsweise ruft verwalteter Code nicht verwalteten Code auf. Doch in einem umgekehrten P/Invoke-Aufruf könnte nicht verwalteter Code verwalteten Code über einen Funktionszeiger aufrufen.
Es gibt vier mögliche Kombinationen, in denen [InAttribute] und [OutAttribute] verwendet werden können: Nur [InAttribute], nur [OutAttribute], [InAttribute, OutAttribute] und weder das eine noch das andere Attribut. Wenn kein Attribut angegeben ist, wird die CLR damit aufgefordert, die direktionalen Attribute selbst herauszufinden, wobei [InAttribute] in der Regel standardmäßig angewendet wird. Doch im Fall der StringBuilder-Klasse wird sowohl [InAttribute] als auch [OutAttribute] angewendet, wenn keins der beiden Attribute angegeben ist. (Einzelheiten dazu finden Sie weiter unten im Abschnitt über StringBuilder.) Auch die Verwendung von Schlüsselwörtern wie „out“ und „ref“ in C# ändert die angewendeten Attribute, wie in Abbildung 2 dargestellt. Beachten Sie Folgendes: Wenn für einen Parameter kein Schlüsselwort angegeben wurde, bedeutet dies, dass es sich standardmäßig um einen Eingabeparameter handelt.

C#-Schlüsselwort Attribut
(nicht angegeben) [InAttribute]
out [OutAttribute]
ref [InAttribute], [OutAttribute]
Sehen Sie sich den Code in Abbildung 3 an. Es sind drei systemeigene C++-Funktionen vorhanden, die alle dieselbe Änderung am Argument vornehmen. Beachten Sie auch, dass die Verwendung von „strcpy“ für die Zeichenfolgenbearbeitung nur zur Veranschaulichung dient. Für Produktionscode sollten stattdessen die sicheren Versionen dieser Funktionen verwendet werden, die Sie unter msdn.microsoft.com/msdnmag/issues/05/05/SafeCandC finden.
MARSHALLIB_API void __stdcall Func_In_Attribute(char *arg)
{
    printf("Inside Func_In_Attribute: arg = %s\n", arg);
    strcpy(arg, "New");
}

MARSHALLIB_API void __stdcall Func_Out_Attribute(char *arg)
{
    printf("Inside Func_Out_Attribute: arg = %s\n", arg);
    strcpy(arg, "New");
}

MARSHALLIB_API void __stdcall Func_InOut_Attribute(char *arg)
{
    printf("Inside Func_InOut_Attribute: arg = %s\n", arg);
    strcpy(arg, "New");
}

Der einzige Unterschied besteht darin, wie sie aufgerufen werden, wobei direktionale Attribute in den P/Invoke-Signaturen verwendet werden, wie der folgende C#-Code zeigt:
[DllImport(@"MarshalLib.dll")]
public static extern void Func_In_Attribute([In]char[] arg);
[DllImport(@"MarshalLib.dll")]
public static extern void Func_Out_Attribute([Out]char[] arg);
[DllImport(@"MarshalLib.dll")]
public static extern void Func_InOut_Attribute([In, Out]char[] arg);
Wenn Sie diese Funktionen von verwaltetem Code aus mit „P/Invoke“ aufrufen und „Old“ als Zeichenarray an die Funktionen übergeben, erhalten Sie folgende Ausgabe (sie ist zu Demonstrationszwecken verkürzt):
Before Func_In_Attribute: arg = Old
Inside Func_In_Attribute: arg = Old
After Func_In_Attribute: arg = Old

Before Func_Out_Attribute: arg = Old
Inside Func_Out_Attribute: arg =
After Func_Out_Attribute: arg = New

Before Func_InOut_Attribute: arg = Old
Inside Func_InOut_Attribute: arg = Old
After Func_InOut_Attribute: arg = New
Im Folgenden werden die Ergebnisse näher untersucht. In „Func_In_Attribute“ wird der ursprüngliche Wert übergeben, doch die in „Func_In_Attribute“ durchgeführt Änderung wird nicht zurückgeleitet. In „Func_Out_Attribute“ wird der ursprüngliche Wert nicht übergeben, und die in „Func_Out_Attribute“ durchgeführte Änderung wird zurückgeleitet. In „Func_InOut_Attribute“ wird der ursprüngliche Wert übergeben, und die in „Func_Out_Attribute“ durchgeführte Änderung wird zurückgeleitet. Es könnte jedoch ganz anders aussehen, wenn Sie eine geringfügige Änderung vornehmen. Ändern Sie beispielsweise die systemeigene Funktion, sodass Unicode verwendet wird, wie nachstehend dargestellt:
MARSHALLIB_API void __stdcall Func_Out_Attribute_Unicode(wchar_t *arg)
{
    wprintf(L"Inside Func_Out_Attribute_Unicode: arg = %s\n", arg);
    printf("Inside Func_Out_Attribute_Unicode: strcpy(arg, \"New\")\n");
    wcscpy(arg, L"New");
}
Hier wird die C#-Funktion deklariert, es wird nur [OutAttribute] angewendet, und der Zeichensatz wird in „CharSet.Unicode“ umgeändert:
[DllImport(@"MarshalLib.dll", CharSet=CharSet.Unicode)]
public static extern void Func_Out_Attribute_Unicode([Out]char[] arg);
Hier ist die Ausgabe:
Before Func_Out_Attribute_Unicode: arg = Old
Inside Func_Out_Attribute_Unicode: arg = Old
After Func_Out_Attribute_Unicode: arg = New
Interessanterweise wird der ursprüngliche Wert übergeben, obwohl kein [InAttribute] vorhanden ist. Das [DllImportAttribute] teilt der CLR mit, Unicode zu marshallen, und da der Zeichenwert in der CLR ebenfalls Unicode ist, sieht die CLR eine Gelegenheit zum Optimieren des Marshallingprozesses durch Festhalten des Zeichenarrays und die direkte Übergabe der Adresse des Zeichens. (Das Kopieren und Festhalten wird später noch eingehend erläutert.) Dies bedeutet jedoch nicht, dass Sie sich auf dieses Verhalten verlassen sollten. Stattdessen sollte das richtige Marshalling direktionaler Attribute immer verwendet werden, wenn Sie sich nicht auf das standardmäßige Marshallingverhalten der CLR verlassen. Ein typisches Beispiel für dieses Standardverhalten ist der Fall eines Ganzzahlarguments. Die Angabe des Ganzzahlarguments [InAttribute] ist nicht erforderlich.
Es gibt Fälle, in denen [OutAttribute] ignoriert wird. [OutAttribute]int beispielsweise ergibt keinen Sinn, sodass [OutAttribute] einfach von der CLR ignoriert wird. Dasselbe gilt für die [OutAttribute]-Zeichenfolge, weil die Zeichenfolge unveränderlich ist.
Schnittstellendefinitionsdateien (IDL) verfügen ebenfalls über [in]- und [out]-Attribute, die genau wie [InAttribute] und [OutAttribute] in der CLR angesehen werden können.

Die Schlüsselwörter „out“ und „ref“ und Übergabe nach Verweis
Es wurde aufgezeigt, dass die C#-Schlüsselwörter „out“ und „ref“ [InAttribute] und [OutAttribute] direkt zugeordnet werden können. Außerdem können „out“ und „ref“ auch eine Änderung bezüglich des Datentyps vornehmen, in den die CLR hinein oder heraus marshallt. Die Übergabe von Daten als „out“ oder „ref“ ist identisch mit der Übergabe nach Verweis. Wenn Sie die entsprechende Funktionssignatur in der Zwischensprache (Intermediate Language, IL) mithilfe von ILDASM untersuchen, sehen Sie, dass sich neben dem Typ ein kaufmännisches Und-Zeichen (&) befindet, was bedeutet, dass das Argument nach Verweis übergeben werden sollte. Beim Übergeben nach Verweis wird von der CLR eine zusätzliche Dereferenzierungsebene hinzugefügt. Abbildung 4 zeigt einige Beispiele hierfür.

       
C#-Signatur Nicht verwaltete Signatur MSIL-Signatur Eigentliche, in der CLR angezeigte MSIL-Signatur
Grundlegende Typen    
int arg int arg int [In] int
out int arg int *arg [out] int & [out] int &
ref int arg int *arg int & [in, out] int &
Strukturen      
MyStruct arg MyStruct arg MyStruct [In] MyStruct
out MyStruct arg MyStruct *arg [out] MyStruct & [out] MyStruct &
ref MyStruct arg MyStruct *arg MyStruct & [in, out] MyStruct &
Zeichenfolgen      
string arg char *arg string [in] string
out string arg char **arg [out] string & [out] string &
ref string arg char **arg string & [in, out] string &
Klassen      
MyClass arg MyClass *arg MyClass [in] MyClass
out MyClass arg MyClass **arg [out] MyClass & [out] MyClass &
ref MyClass arg MyClass **arg MyClass & [in, out] Myclass &
Die Tabelle in Abbildung 5 enthält eine Zusammenfassung der Erläuterung von „out“ und „ref“.

C#-Signatur MSIL-Signatur Direktionale Standardattribute
<type> type [InAttribute]
out <type> [OutAttribute] type & [OutAttribute]
ref <type> type& [InAttribute, OutAttribute]
Wenn keine direktionalen Attribute angegeben sind, gilt es beim Übergeben nach Verweis zu beachten, dass die CLR [InAttribute] und [OutAttribute] automatisch anwendet, was der Grund dafür ist, dass nur „string &“ in der MSIL-Signatur (Microsoft Intermediate Language) in Abbildung 4 vorhanden ist. Wenn eins dieser Attribute angegeben ist, beachtet die CLR das betreffende Attribut anstelle des Standardverhaltens, wie dieses Beispiel zeigt:
public static extern void 
      PassPointerToComplexStructure(
        [In]ref ComplexStructure 
        pStructure);
Die vorstehende Signatur setzt das direktionale Standardverhalten von „ref“ außer Kraft, sodass es nur zu [InAttribute] wird. Wenn Sie einen P/Invoke-Aufruf durchführen, wird in diesem speziellen Fall ein Zeiger auf „ComplexStructure“ (wobei es sich um einen Werttyp handelt) von der CLR-Seite an die systemeigene Seite übergeben, doch der Aufgerufene kann für „ComplexStructure“, auf die mit dem pStructure-Zeiger gezeigt wird, keine Änderungen sichtbar machen. Abbildung 6 zeigt weitere Beispiele direktionaler Attribute und Schlüsselwortkombinationen.

     
C#-Signatur Nicht verwaltete IDL-Signatur MSIL-Signatur
Out    
[InAttribute] out int arg Kompilierfehler CS0036. Ein out-Parameter kann kein In-Attribut haben. Nicht zutreffend
[OutAttribute] out int arg [out] int *arg [out] int &
[InAttribute, OutAttribute] out int arg Kompilierfehler CS0036. Ein out-Parameter kann kein In-Attribut haben. Nicht zutreffend
Ref    
[InAttribute] ref int arg [in] int *arg [in] int &
[OutAttribute] ref int arg Kompilierfehler CS0662 kann nicht ausschließlich das Out-Attribut in einem ref-Parameter angeben. Verwenden Sie sowohl das In- als auch das Out-Attribut oder keines der beiden. Nicht zutreffend
[InAttribute, OutAttribute] ref int arg [in, out] int *arg [in] [out] int &

Rückgabewerte
Bisher wurden nur Argumente erläutert. Wie sieht es mit Werten aus, die von Funktionen zurückgegeben werden? Die CLR behandelt einen Rückgabewert automatisch so, als ob es sich um ein normales Argument handelt, das das [OutAttribute] verwendet. Außerdem kann die CLR die Transformation zur Funktionssignatur durchführen, ein Prozess, der vom PreserveSigAttribute gesteuert wird. Wenn [PreserveSigAttribute] bei Anwendung auf eine P/Invoke-Signatur auf „false“ gesetzt ist, ordnet die CLR verwalteten Ausnahmen HRESULT-Rückgabewerte und dem Rückgabewert der Funktion [out, retval]-Parameter zu. Die folgende verwaltete Funktionssignatur
public static string extern GetString(int id);
würde also zur nicht verwalteten Signatur werden:
HRESULT GetString([in]int id, [out, retval] char **pszString);
Wenn [PreserveSigAttribute] auf „true“ (die Standardeinstellung für P/Invoke) gesetzt ist, findet diese Transformation nicht statt. Beachten Sie, dass bei COM-Funktionen [PreserveSigAttribute] in der Regel standardmäßig auf „false“ gesetzt ist, obwohl es eine Reihe von Möglichkeiten gibt, dies zu ändern. Einzelheiten finden Sie in der MSDN®-Dokumentation für „TlbExp.exe“ und „TlbImp.exe“.

StringBuilder und Marshalling
Der CLR-Marshaller verfügt über internes Wissen zum StringBuilder-Typ und behandelt ihn anders als andere Typen. Standardmäßig wird „StringBuilder“ als [InAttribute, OutAttribute] übergeben. „StringBuilder“ ist etwas Besonderes, da er über eine Capacity-Eigenschaft verfügt, die die Größe des erforderlichen Puffers zur Laufzeit bestimmen kann. Zudem kann er dynamisch geändert werden. Daher kann die CLR StringBuilder während des Marshallingprozesses festhalten, die Adresse des in „StringBuilder“ verwendeten internen Puffers direkt übergeben und ermöglichen, dass der Inhalt dieses Puffers durch systemeigenen, vorhandenen Code geändert werden kann.
Um vollen Nutzen aus „StringBuilder“ zu ziehen, müssen alle folgenden Regeln beachtet werden:
  1. Übergeben Sie „StringBuilder“ nicht nach Verweis (mithilfe von „out“ oder „ref“). Andernfalls erwartet die CLR, dass die Signatur dieses Arguments wchar_t ** statt wchar_t * ist, sodass sie den internen Puffer von „StringBuilder“ nicht festhalten kann. Die Leistung wird deutlich verringert.
  2. Verwenden Sie „StringBuilder“, wenn der nicht verwaltete Code Unicode verwendet. Andernfalls muss die CLR eine Kopie der Zeichenfolge erstellen und sie zwischen Unicode und ANSI konvertieren, was die Leistung verringert. Normalerweise sollten Sie „StringBuilder“ als LPARRAY aus Unicode-Zeichen oder als LPWSTR marshallen.
  3. Geben Sie die Kapazität von „StringBuilder“ immer im Voraus an, und stellen Sie sicher, dass die Kapazität groß genug ist, um den Puffer aufzunehmen. Es gilt auf der Seite des nicht verwalteten Codes als bewährte Methode, die Größe des Zeichenfolgenpuffers als Argument zu akzeptieren, um Pufferüberläufe zu vermeiden. In COM können Sie auch „size_is“ in IDL zur Angabe der Größe verwenden.

Kopieren und Festhalten
Beim Datenmarshalling durch die CLR gibt es zwei Optionen: Kopieren und Festhalten (siehe msdn2.microsoft.com/23acw07k).
Standardmäßig erstellt die CLR eine Kopie, die während des Marshalling verwendet wird. Wenn beispielsweise verwalteter Code eine Zeichenfolge als ANSI-C-Zeichenfolge an nicht verwalteten Code übergibt, erstellt die CLR eine Kopie der Zeichenfolge, konvertiert sie in ANSI und übergibt dann den Zeiger der temporären Kopie an den nicht verwalteten Code. Dieser Kopiervorgang kann recht langsam sein und zu Leistungsproblemen führen.
In bestimmten Fällen kann die CLR den Marshallingprozess durch direktes Festhalten des verwalteten Objekts im GC-Heap (Garbage Collector) optimieren, sodass es während des Aufrufs nicht verschoben werden kann. Der Zeiger zum verwalteten Objekt (oder auf etwas im Inneren des verwalteten Objekts) wird direkt an den nicht verwalteten Code übergeben.
Das Festhalten wird durchgeführt, wenn alle folgenden Bedingungen erfüllt sind: Erstens muss verwalteter Code systemeigenen Code aufrufen, nicht umgekehrt. Zweitens muss der Typ für Blitvorgänge geeignet bzw. in der Lage sein, sich unter bestimmten Bedingungen für Blitvorgänge zu eignen. Drittens: Sie übergeben nicht nach Verweis („out“ oder „ref“), und viertens: der Aufrufer und der Aufgerufene befinden sich im selben Threadkontext oder im selben Apartment.
Die zweite Regel sollte näher erläutert werden. Ein für Blitvorgänge geeigneter Typ ist ein Typ, bei dem sowohl im verwalteten als auch im nicht verwalteten Speicher die gleiche Darstellung vorhanden ist. Daher ist bei für Blitvorgänge geeigneten Typen keine Konvertierung beim Marshalling erforderlich. Ein typisches Beispiel für einen Typ, der nicht für Blitvorgänge geeignet ist, sich aber für Blitvorgänge eignen kann, ist der Zeichenwert. Standardmäßig ist er nicht für Blitvorgänge geeignet, da er entweder Unicode oder ANSI zugeordnet werden kann. Doch da „char“ in der CLR immer Unicode ist, eignet er sich für Blitvorgänge, wenn Sie [DllImportAttribute(CharSet = Unicode)] oder [MarshalAsAttribute(UnmanagedType.LPWSTR)] angeben. Im folgenden Beispiel kann das Argument in „PassUnicodeString“ festgehalten werden, aber nicht in „PassAnsiString“:
[DllImport(@"MarshalLib.dll", CharSet = CharSet.Unicode)]
public static extern string PassUnicodeString(string arg);

[DllImport(@"MarshalLib.dll", CharSet = CharSet.Ansi)]
public static extern string PassAnsiString(string arg);

Speicherbesitz
Während eines Funktionsaufrufs kann eine Funktion zwei Arten von Änderungen an ihren Argumenten vornehmen: eine Verweisänderung oder eine direkte Änderung. Eine Verweisänderung umfasst eine Änderung der Stelle, auf die ein Zeiger zeigt. Wenn der Zeiger bereits auf einen zugeordneten Speicherbereich zeigt, muss dieser Speicher möglicherweise erst freigegeben werden, bevor der Zeiger auf ihn verloren geht. Eine direkte Änderung umfasst eine Änderung des Speichers an dem Speicherort, auf den der Verweis zeigt.
Welche dieser Änderungen vorgenommen wird, hängt vom Argumenttyp und – besonders wichtig – vom Vertrag zwischen dem Aufrufer und dem Aufgerufenen ab. Da die CLR den Vertrag jedoch nicht automatisch ermitteln kann, muss sie sich auf allgemeine Kenntnisse über Typen verlassen, wie in Abbildung 7 dargestellt.

IDL-Signatur Änderungstyp
[In]-Typ Keine Änderung erlaubt
[In]-Typ * Keine Änderung erlaubt
[Out]-Typ * Direkte Änderung
[In, Out]-Typ * Direkte Änderung
[In]-Typ ** Keine Änderung erlaubt
[Out]-Typ ** Verweisänderung
[In, Out]-Typ ** Verweisänderung oder direkte Änderung
Wie bereits erläutert, haben nur Verweistypen zwei Dereferenzierungsebenen beim Übergeben nach Verweis (es gibt jedoch ein paar Ausnahmen, beispielsweise [MarshalAs(UnmanagedType.LPStruct)]ref GUID), sodass nur der Zeiger oder Verweis auf einen Verweistyp geändert werden kann, wie in Abbildung 8 dargestellt.

C#-Signatur Änderungstyp
int arg Keine Änderung erlaubt
out int arg Direkte Änderung
ref int arg Direkte Änderung
string arg Keine Änderung erlaubt
out string arg Verweisänderung
ref string arg Verweisänderung oder direkte Änderung
[InAttribute, OutAttribute] StringBuilder arg Direkte Änderung
[OutAttribute] StringBuilder arg Direkte Änderung
Sie müssen sich wegen des Speicherbesitzes für eine direkte Änderung keine Gedanken machen, da der Aufrufer dem Aufgerufenen Speicher zugewiesen hat und der Aufrufer den Speicher besitzt. „[ OutAttribute] StringBuilder“ soll hier als Beispiel dienen. Der entsprechende systemeigene Typ ist „char *“ (wobei von ANSI ausgegangen wird), da keine Übergabe nach Verweis erfolgt. Daten werden hinaus, nicht herein gemarshallt. Speicher wird vom Aufrufer (in diesem Fall von der CLR) zugeordnet. Die Größe des Speichers wird von der Kapazität des StringBuilder-Objekts bestimmt. Der Aufgerufene muss sich nicht um den Speicher kümmern.
Zum Ändern der Zeichenfolge nimmt der Aufgerufene die Änderung direkt am Speicher selbst vor. Doch beim Durchführen einer Verweisänderung muss unbedingt erkannt werden, wer welchen Speicher besitzt, da es sonst zu einer ganzen Reihe unerwarteter Ergebnisse kommen kann. In Bezug auf Besitzprobleme befolgt die CLR COM-Stilkonventionen:
  • Speicher, der als [in] übergeben wird, ist im Besitz des Aufrufers und sollte sowohl vom Aufrufer zugeordnet als auch von ihm freigegeben werden. Der Aufgerufene sollte nicht versuchen, diesen Speicher freizugeben oder zu ändern.
  • Speicher, der vom Aufgerufenen zugeordnet wurde und als [out] übergeben oder zurückgegeben wird, ist im Besitz des Aufrufers und sollte von ihm freigegeben werden.
  • Der Aufgerufene kann Speicher, der vom Aufrufer als [in, out] übergeben wird, freigeben, neuen Speicher zuordnen und den alten Zeigerwert überschreiben, wodurch er übergeben wird. Der neue Speicher ist im Besitz des Aufrufers. Dafür sind zwei Dereferenzierungsebenen wie beispielsweise „char **“ erforderlich.
In der Welt der Interoperabilität wird der Aufrufer/Aufgerufene zu CLR-/systemeigenem Code. Bei aufgehobenem Festhalten bedeuten die vorstehenden Regeln Folgendes: Wenn Sie im systemeigenen Code einen Zeiger auf einen Speicherblock erhalten, der Ihnen als [out] von der CLR übergeben wurde, muss dieser freigegeben werden. Wenn die CLR andererseits einen Zeiger erhält, der von systemeigenem Code als [out] übergeben wird, muss dieser von der CLR freigegeben werden. Natürlich muss im ersten Fall der systemeigene Code die Zuordnung aufheben, während die Zuordnung im zweiten Fall vom verwalteten Code aufgehoben werden muss.
Da dies eine Speicherzuordnung und die Aufhebung der Zuordnung umfasst, besteht das größte Problem darin, welche Funktion verwendet werden sollte. Sie können unter vielen auswählen: „HeapAlloc/HeapFree“, „malloc/free“, „new/delete“ und so weiter. Doch da die CLR „CoTaskMemAlloc/CoTaskMemFree“ im Nicht-BSTR-Fall und „SysStringAlloc/SysStringAllocByteLen/SysStringFree“ im BSTR-Fall verwendet, müssen diese Funktionen verwendet werden. Andernfalls kommt es wahrscheinlich zu einem Speicherverlust oder Absturz in bestimmten Windows®-Versionen. Es sind Fälle bekannt, in denen Malloc-Speicher an die CLR übergeben wurde und das Programm in Windows XP nicht abgestürzt ist, während es in Windows Vista® abgestürzt ist.
Neben diesen Funktionen funktioniert eine systemimplementierte IMalloc-Schnittstelle, die von „CoGetMalloc“ zurückgegeben wird, ebenfalls korrekt, weil intern derselbe Heap verwendet wird. Es ist jedoch am besten, „CoTaskMemAlloc/CoTaskMemFree“ und „SysStringAlloc/SysStringAllocByteLen/SysStringFree“ zu verwenden, da „CoGetMalloc“ zukünftigen Änderungen unterliegt.
Nachfolgend sehen Sie ein Beispiel. Der „GetAnsiStringFromNativeCode“ nimmt ein char **-Argument als [in, out] an und gibt ein „char *“ als [out, retval] zurück. Für das char **-Argument kann dann „CoTaskMemFree“ zur Freigabe des Speichers, der von der CLR zugeordnet wird, aufgerufen, dann mithilfe von „CoTaskMemAlloc“ neuer Speicher zugeordnet und der Zeiger mit einem neuen Speicherzeiger überschrieben werden. Später gibt die CLR den Speicher frei und erstellt eine Kopie für die verwaltete Zeichenfolge. Was den Rückgabewert betrifft, muss dieser nur einen neuen Speicherbereich mithilfe von „CoTaskMemAlloc“ zuordnen und an den Aufrufer zurückgeben. Nach der Rückgabe ist dieser neu zugeordnete Speicher nun im Besitz der CLR. Die CLR erstellt daraus zuerst eine neue verwaltete Zeichenfolge und ruft dann „CoTaskMemFree“ auf, um sie freizugeben.
Im Folgenden soll die erste Option näher betrachtet werden (siehe Abbildung 9). Die entsprechende C#-Funktionsdeklaration sieht folgendermaßen aus:
MARSHALLIB_API char *__stdcall GetAnsiStringFromNativeCode(char **arg)
{
    char *szRet = (char *)::CoTaskMemAlloc(255);
    strcpy(szRet, "Returned String From Native Code");

    printf("Inside GetAnsiStringFromNativeCode: *arg = %s\n", *arg);
    printf("Inside GetAnsiStringFromNativeCode: CoTaskMemFree(*arg); 
        *arg = CoTaskMemAlloc(100); strcpy(*arg, \"Changed\")\n");

    ::CoTaskMemFree(*arg);
    *arg = (char *)::CoTaskMemAlloc(100);
    strcpy(*arg, "Changed");

    return szRet;
}

class Lib
{
    [DllImport(@"MarshalLib.dll", CharSet= CharSet.Ansi)]
    public static extern string GetAnsiStringFromNativeCode(
        ref string inOutString);
}
Wenn der folgende C#-Code GetAnsiStringFromNativeCode aufruft,
string argStr = "Before";
Console.WriteLine("Before GetAnsiStringFromNativeCode : argStr = \"" + 
    argStr + "\"");
string retStr = Lib.GetAnsiStringFromNativeCode(ref argStr);
Console.WriteLine("AnsiStringFromNativeCode() returns \"" + retStr + 
    "\"" );
Console.WriteLine("After GetAnsiStringFromNativeCode : argStr = \"" + 
    argStr + "\"");
sieht die Ausgabe folgendermaßen aus:
Before GetAnsiStringFromNativeCode : argStr = "Before"
Inside GetAnsiStringFromNativeCode: *arg = Before
Inside GetAnsiStringFromNativeCode: CoTaskMemFree(*arg); *arg = CoTaskMemAlloc(100); strcpy(*arg, "Changed")
AnsiStringFromNativeCode() returns "Returned String From Native Code"
After GetAnsiStringFromNativeCode : argStr = "Changed"
Wenn die systemeigene Funktion, die Sie aufrufen wollen, dieser Konvention nicht folgt, müssen Sie das Marshalling selbst durchführen, um eine Speicherbeschädigung zu vermeiden. Dazu könnte es leicht kommen, da die Funktion für eine nicht verwaltete Funktion beliebigen Speicher zurückgeben könnte. Sie kann jedes Mal denselben Speicherbereich zurückgeben oder einen neuen, von „malloc/new“ zugeordneten Speicherblock und so weiter, was wieder vom Vertrag abhängt.
Neben der Speicherzuordnung ist die Größe des übergebenen Speichers ebenfalls von Bedeutung. Wie im StringBuilder-Fall erläutert, ist es sehr wichtig, die Capacity-Eigenschaft zu ändern, sodass die CLR einen Speicherbereich zuordnen kann, der für die Aufnahme der Ergebnisse groß genug ist. Außerdem ist das Marshalling einer Zeichenfolge wie [InAttribute, OutAttribute] (ohne „out“ oder „ref“ und andere Attribute) grundsätzlich nicht sinnvoll, da Sie nicht wissen, ob die Zeichenfolge groß genug sein wird. Sie können SizeParamIndex- und SizeConst-Felder in MarshalAsAttribute verwenden, um die Größe des Puffers anzugeben. Diese Attribute können jedoch beim Übergeben nach Verweis nicht verwendet werden.

P/Invoke-Umkehrung und Delegatlebensdauer
Die CLR ermöglicht die Übergabe eines Delegaten an die nicht verwaltete Umgebung, sodass sie den Delegaten als nicht verwalteten Funktionszeiger aufrufen kann. Tatsächlich erstellt die CLR einen Thunk, der die Aufrufe von systemeigenem Code an den eigentlichen Delegaten und dann an die echte Funktion weiterleitet (siehe Abbildung 10).
Abbildung 10 Verwenden eines Thunk (Klicken Sie zum Vergrößern auf das Bild)
In der Regel müssen Sie sich bezüglich der Lebensdauer von Delegaten keine Gedanken machen. Jedes Mal, wenn Sie einen Delegaten an nicht verwalteten Code übergeben, stellt die CLR sicher, dass der Delegat während des Aufrufs aktiv ist.
Wenn der systemeigene Code jedoch eine Kopie des Zeigers über die Zeitspanne des Aufrufs hinaus beibehält und beabsichtigt, später über diesen Zeiger einen Rückruf auszuführen, müssen Sie möglicherweise „GCHandle“ verwenden, um explizit zu verhindern, dass der Garbage Collector den Delegaten bereinigt. Es sollte darauf hingewiesen werden, dass ein festgehaltenes „GCHandle“ beträchtliche negative Auswirkung auf die Programmleistung haben könnte. Glücklicherweise müssen Sie in diesem Fall kein festgehaltenes GC-Handle zuordnen, weil der Thunk im nicht verwalteten Heap zugeordnet ist und durch einen dem Garbage Collector bekannten Verweis indirekt auf den Delegaten verweist. Daher ist es dem Thunk nicht möglich, sich zu bewegen, und systemeigener Code sollte immer in der Lage sein, den Delegaten über den nicht verwalteten Zeiger aufzurufen, wenn der Delegat selbst aktiv ist.
Marshall.GetFunctionPointerForDelegate kann einen Delegaten in einen Funktionszeiger konvertieren, garantiert jedoch in keiner Weise die Lebensdauer des Delegaten. Betrachten Sie die folgende Funktionsdeklaration:
public delegate void PrintInteger(int n);

[DllImport(@"MarshalLib.dll", EntryPoint="CallDelegate")]
public static extern void CallDelegateDirectly(
    IntPtr printIntegerProc);
Wenn Sie dafür „Marschal.GetFunctionPointerForDelegate“ aufrufen und den zurückgegebenen „IntPtr“ speichern, übergeben Sie der Funktion, die Sie aufrufen wollen, den „IntPtr“ folgendermaßen:
IntPtr printIntegerCallback = Marshal.GetFunctionPointerForDelegate(
    new Lib.PrintInteger(MyPrintInteger));
            

GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();

CallDelegateDirectly(printIntegerCallback);
Es ist möglich, dass der Delegat bereinigt wird, bevor Sie „CallDelegateDirectly“ aufrufen. Sie erhalten dann einen MDA-Fehler, der meldet, dass „CallbackOnCollectedDelegate“ erkannt wurde. Um dieses Problem zu beheben, können Sie entweder einen Verweis zum Delegaten im Speicher speichern oder ein GC-Handle zuordnen.
Wenn systemeigener Code einen nicht verwalteten Funktionszeiger an die CLR zurückgibt, ist der systemeigene Code verantwortlich dafür, den eigentlichen Funktionscode beizubehalten. In der Regel ist dies kein Problem, es sei denn, der Code ist in einer dynamisch geladenen DLL enthalten oder wird dynamisch generiert.

P/Invoke-Interop-Assistent
Alle bisher beschriebenen Attribute und Regeln zu verstehen und im Gedächtnis zu behalten, kann eine schwierige Aufgabe sein. Schließlich müssen die meisten Entwickler von verwaltetem Code nur in der Lage sein, schnell die P/Invoke-Signatur für eine Win32®-API-Funktion herauszufinden, diese in ihren Code einfügen, und fertig! Dabei kann der P/Invoke-Interop-Assistent helfen (dieser ist auf der MSDN Magazin-Website verfügbar). Dieses Tool ist ein effizientes Hilfsmittel bei Konvertierungen aus C++ in verwaltete P/Invoke-Signaturen sowie bei Konvertierungen in umgekehrter Richtung. Es verfügt sogar über eine Datenbank mit Win32-Funktionen, Datentypen und Konstanten, sodass das Hinzufügen eines Win32-P/Invoke zu Ihrer C#- oder Visual Basic-Quelldatei (eine häufige Aufgabe) sehr erleichtert wird. Das Toolpaket umfasst zwei Befehlszeilenprogramme, „SigImp“ und „SigExp“, die zur Batchdateiverarbeitung verwendet werden können. Ein GUI-Tool ist ebenfalls Teil des Pakets. Es enthält die Funktionalitäten beider Tools.
Das GUI-Tool ist für einfache Konvertierungen praktisch. Es enthält drei Registerkarten: „SigExp“, „SigImp Search“ und „SigImp Translate Snippet“.
„SigExp“ konvertiert eine verwaltete Signatur in eine nicht verwaltete Signatur. Es reflektiert über verwaltete Assemblys, um alle P/Invoke-Deklarationen und importierten COM-Typen zu finden. Aus dieser Eingabe erstellt es die entsprechenden systemeigenen C-Signaturen (siehe Abbildung 11).
Abbildung 11 GUI-Tool des P/Invoke-Interop-Assistenten – SigExp (Klicken Sie zum Vergrößern auf das Bild)
„SigImp Search“ und „SigImp Translate Snippet“ konvertieren nicht verwaltete Signaturen in verwaltete Signaturen. Sie generieren verwaltete Signaturen und Definitionen in C# oder Visual Basic aus systemeigenen Typen, Funktionen, Konstanten und Ausschnitten von manuell eingegebenen systemeigenen Funktionssignaturen.
Mithilfe von „SigImp Search“ können Benutzer die verwaltete Codesprache auswählen, in der Code generiert werden soll, und dann einen systemeigenen Typ, ein Verfahren oder eine Konstante, aus der die Generierung durchgeführt werden soll. Das Tool zeigt eine Liste unterstützter Typen, Methoden und Konstanten an, die in Windows SDK-Headerdateien gesammelt wurden (siehe Abbildung 12).
Abbildung 12 GUI-Tool des P/Invoke-Interop-Assistenten – SigImp Search (Klicken Sie zum Vergrößern auf das Bild)
Mithilfe von „SigImp Translate Snippet“ können Benutzer ihren eigenen systemeigenen Codeausschnitt im Tool schreiben. Anschließend generiert das Tool die Entsprechung in verwaltetem Code und zeigt sie im Hauptfenster an, wie in Abbildung 13 dargestellt.
Abbildung 13 GUI-Tool des P/Invoke-Interop-Assistenten – SigImp Translate Snippet (Klicken Sie zum Vergrößern auf das Bild)
Einzelheiten zum GUI-Tool und den Befehlszeilenprogrammen im P/Invoke-Interop-Assistenten finden Sie in der dem Tool beigefügten Dokumentation.

Versuchen Sie es selbst
Marshalling ist offensichtlich ein komplexes Thema, und es gibt viele Verfahren, die Sie zum Anpassen des Marshallingprozesses an Ihre eigenen Anforderungen verwenden können. Probieren Sie einige der hier vorgestellten Ideen aus. Auf diese Weise werden Sie sich besser im Marshallinglabyrinth zurechtfinden können.

Senden Sie Ihre Fragen und Kommentare (in englischer Sprache) an clrinout@microsoft.com.


Yi Zhang ist Softwareentwickler im Silverlight Shanghai-Team, das von Server and Tool Business in China ist. Er befasst sich derzeit hauptsächlich mit den Bereichen Interoperabilität und CLR.

Xiaoying Guo ist Programmmanager im Silverlight Shanghai-Team, das Teil des Server and Tool Business in China ist. Sie beschäftigt sich derzeit hauptsächlich mit den Bereichen nicht verwaltete/verwaltete Interoperabilität innerhalb der CLR sowie CoreCLR unter Mac. Xiaoying Guo ist zudem auf dem Gebiet der lokalen Silverlight-Kundenbindung aktiv.

Page view tracker