Domyślne zachowanie marshallingu
Interop marshalling działa na regułach, które określają, w jaki sposób dane skojarzone z parametrami metody zachowują się w miarę ich działania między zarządzaną i niezarządzaną pamięcią. Te wbudowane reguły kontrolują takie działania marshalling jak przekształcenia typów danych, czy obiekt wywoływany może zmienić przekazane do niego dane i zwrócić te zmiany do obiektu wywołującego, a w jakich okolicznościach marshaller zapewnia optymalizacje wydajności.
W tej sekcji przedstawiono domyślne cechy behawioralne międzyoperacyjnej usługi marshallingowej. Przedstawia szczegółowe informacje na temat tablic marshalling, typów logicznych, typów char, delegatów, klas, obiektów, ciągów i struktur.
Uwaga
Marshalling typów ogólnych nie jest obsługiwany. Aby uzyskać więcej informacji, zobacz Interoperating Using Generic Types (Współdziałanie przy użyciu typów ogólnych).
Zarządzanie pamięcią za pomocą programu marshaller międzyoperacyjnej
Marshaller międzyoperacyjny zawsze próbuje zwolnić pamięć przydzieloną przez niezarządzany kod. To zachowanie jest zgodne z regułami zarządzania pamięcią COM, ale różni się od reguł, które zarządzają natywnym językiem C++.
Może wystąpić zamieszanie, jeśli przewidujesz natywne zachowanie języka C++ (bez zwalniania pamięci) podczas korzystania z wywołania platformy, co automatycznie zwalnia pamięć dla wskaźników. Na przykład wywołanie następującej niezarządzanej metody z biblioteki DLL języka C++ nie powoduje automatycznego zwolnienia żadnej pamięci.
Podpis niezarządzany
BSTR MethodOne (BSTR b) {
return b;
}
Jeśli jednak zdefiniujesz metodę jako platformę wywołaj prototyp, zastąp każdy typ BSTR typem typem String i wywołaj MethodOne
metodę , środowisko uruchomieniowe języka wspólnego próbuje zwolnić b
dwa razy. Zachowanie marshalingu można zmienić przy użyciu IntPtr typów, a nie typów ciągów .
Środowisko uruchomieniowe zawsze używa metody CoTaskMemFree w systemie Windows i bezpłatnej metody na innych platformach do zwolnienia pamięci. Jeśli pamięć, z którą pracujesz, nie została przydzielona z metodą CoTaskMemAlloc w systemie Windows lub malloc na innych platformach, musisz użyć intPtr i zwolnić pamięć ręcznie przy użyciu odpowiedniej metody. Podobnie można uniknąć automatycznego zwalniania pamięci w sytuacjach, w których pamięć nigdy nie powinna być zwalniana, na przykład w przypadku korzystania z funkcji GetCommandLine z Kernel32.dll, która zwraca wskaźnik do pamięci jądra. Aby uzyskać szczegółowe informacje na temat ręcznego zwalniania pamięci, zobacz Przykład buforów.
Domyślne marshalling dla klas
Klasy mogą być marshalled tylko przez międzyoperacyjności COM i są zawsze marshalled jako interfejsy. W niektórych przypadkach interfejs używany do marshalingu klasy jest znany jako interfejs klasy. Aby uzyskać informacje o zastępowaniu interfejsu klasy wybranym interfejsem, zobacz Wprowadzenie do interfejsu klasy.
Przekazywanie klas do modelu COM
Gdy klasa zarządzana jest przekazywana do modelu COM, międzyoperator automatycznie opakowuje klasę za pomocą serwera proxy COM i przekazuje interfejs klasy utworzony przez serwer proxy do wywołania metody COM. Następnie serwer proxy deleguje wszystkie wywołania interfejsu klasy z powrotem do zarządzanego obiektu. Serwer proxy udostępnia również inne interfejsy, które nie są jawnie implementowane przez klasę. Serwer proxy automatycznie implementuje interfejsy, takie jak IUnknown i IDispatch w imieniu klasy.
Przekazywanie klas do kodu platformy .NET
Coclasses nie są zwykle używane jako argumenty metody w modelu COM. Zamiast tego interfejs domyślny jest zwykle przekazywany zamiast coclass.
Gdy interfejs jest przekazywany do kodu zarządzanego, międzyoperator jest odpowiedzialny za zawijanie interfejsu przy użyciu odpowiedniej otoki i przekazanie otoki do metody zarządzanej. Określenie, która otoka do użycia może być trudna. Każde wystąpienie obiektu COM ma jedną, unikatową otokę, niezależnie od tego, ile interfejsów implementuje obiekt. Na przykład pojedynczy obiekt COM, który implementuje pięć odrębnych interfejsów, ma tylko jedną otokę. Ta sama otoka uwidacznia wszystkie pięć interfejsów. Jeśli zostaną utworzone dwa wystąpienia obiektu COM, zostaną utworzone dwa wystąpienia otoki.
Aby otoka utrzymywała ten sam typ przez cały okres istnienia, międzyoperator musi zidentyfikować poprawną otokę przy pierwszym przekazaniu interfejsu uwidocznionego przez obiekt marshaller. Marshaller identyfikuje obiekt, patrząc na jeden z interfejsów, które implementuje obiekt.
Na przykład marshaller określa, że otoka klasy powinna być używana do opakowania interfejsu, który został przekazany do kodu zarządzanego. Gdy interfejs jest najpierw przekazywany przez marshaller, marshaller sprawdza, czy interfejs pochodzi ze znanego obiektu. Ta kontrola występuje w dwóch sytuacjach:
Interfejs jest implementowany przez inny zarządzany obiekt, który został przekazany do modelu COM gdzie indziej. Marshaller może łatwo identyfikować interfejsy uwidocznione przez obiekty zarządzane i jest w stanie dopasować interfejs z obiektem zarządzanym, który zapewnia implementację. Zarządzany obiekt jest następnie przekazywany do metody i nie jest wymagana żadna otoka.
Obiekt, który został już opakowany, implementuje interfejs. Aby ustalić, czy tak jest, marshaller wysyła zapytanie do obiektu dla jego interfejsu IUnknown i porównuje zwrócony interfejs z interfejsami innych obiektów, które są już opakowane. Jeśli interfejs jest taki sam jak w przypadku innej otoki, obiekty mają tę samą tożsamość, a istniejąca otoka jest przekazywana do metody .
Jeśli interfejs nie pochodzi ze znanego obiektu, marshaller wykonuje następujące czynności:
Marshaller odpytuje obiekt dla interfejsu IProvideClassInfo2 . Jeśli tak, marshaller używa identyfikatora CLSID zwróconego z IProvideClassInfo2.GetGUID , aby zidentyfikować klasę współklasy dostarczającą interfejs. Za pomocą identyfikatora CLSID marshaller może zlokalizować otokę z rejestru, jeśli zestaw został wcześniej zarejestrowany.
Marshaller wysyła zapytanie do interfejsu interfejsu IProvideClassInfo . Jeśli jest to podane, marshaller używa ITypeInfo zwróconego z IProvideClassInfo.GetClassinfo , aby określić CLSID klasy uwidaczniającej interfejs. Marshaller może użyć CLSID, aby zlokalizować metadane dla otoki.
Jeśli marshaller nadal nie może zidentyfikować klasy, opakowuje interfejs z ogólną klasą otoki o nazwie System.__ComObject.
Domyślne marshaling dla delegatów
Delegat zarządzany jest marshalled jako interfejs COM lub jako wskaźnik funkcji, na podstawie mechanizmu wywołującego:
W przypadku wywołania platformy delegat jest domyślnie rozdzielany jako wskaźnik funkcji niezarządzanej.
W przypadku międzyoperatorów COM delegat jest domyślnie rozdzielany jako interfejs COM typu _Delegate . Interfejs _Delegate jest zdefiniowany w bibliotece typów Mscorlib.tlb i zawiera Delegate.DynamicInvoke metodę, która umożliwia wywołanie metody, do której odwołuje się delegat.
W poniższej tabeli przedstawiono opcje marshalingu dla zarządzanego typu danych delegata. Atrybut MarshalAsAttribute zawiera kilka UnmanagedType wartości wyliczenia do marshalingu delegatów.
Typ wyliczenia | Opis formatu niezarządzanego |
---|---|
UnmanagedType.FunctionPtr | Wskaźnik funkcji niezarządzanej. |
UnmanagedType.Interface | Interfejs typu _Delegate zdefiniowany w pliku Mscorlib.tlb. |
Rozważmy następujący przykładowy kod, w którym metody DelegateTestInterface
są eksportowane do biblioteki typów MODELU COM. Zwróć uwagę, że tylko delegaty oznaczone słowem kluczowym ref (lub ByRef) są przekazywane jako parametry in/out.
using System;
using System.Runtime.InteropServices;
public interface DelegateTest {
void m1(Delegate d);
void m2([MarshalAs(UnmanagedType.Interface)] Delegate d);
void m3([MarshalAs(UnmanagedType.Interface)] ref Delegate d);
void m4([MarshalAs(UnmanagedType.FunctionPtr)] Delegate d);
void m5([MarshalAs(UnmanagedType.FunctionPtr)] ref Delegate d);
}
Reprezentacja biblioteki typów
importlib("mscorlib.tlb");
interface DelegateTest : IDispatch {
[id(…)] HRESULT m1([in] _Delegate* d);
[id(…)] HRESULT m2([in] _Delegate* d);
[id(…)] HRESULT m3([in, out] _Delegate** d);
[id()] HRESULT m4([in] int d);
[id()] HRESULT m5([in, out] int *d);
};
Wskaźnik funkcji może zostać wyłuszony, podobnie jak każdy inny niezarządzany wskaźnik funkcji może zostać wyłuszony.
W tym przykładzie, gdy dwa delegaty są marshalled jako UnmanagedType.FunctionPtr, wynik jest int
i wskaźnikiem int
do . Ponieważ typy delegatów są marshalled, int
tutaj reprezentuje wskaźnik do void (void*
), który jest adresem delegata w pamięci. Innymi słowy, ten wynik jest specyficzny dla 32-bitowych systemów Windows, ponieważ int
tutaj reprezentuje rozmiar wskaźnika funkcji.
Uwaga
Odwołanie do wskaźnika funkcji do zarządzanego delegata przechowywanego przez niezarządzany kod nie uniemożliwia środowisku uruchomieniowemu języka wspólnego wykonywania odzyskiwania pamięci na zarządzanym obiekcie.
Na przykład poniższy kod jest niepoprawny, ponieważ odwołanie do cb
obiektu, przekazane do SetChangeHandler
metody, nie utrzymuje cb
aktywności poza życiem Test
metody. Po zebraniu cb
pamięci obiektu wskaźnik funkcji przekazany do SetChangeHandler
elementu nie jest już prawidłowy.
public class ExternalAPI {
[DllImport("External.dll")]
public static extern void SetChangeHandler(
[MarshalAs(UnmanagedType.FunctionPtr)]ChangeDelegate d);
}
public delegate bool ChangeDelegate([MarshalAs(UnmanagedType.LPWStr) string S);
public class CallBackClass {
public bool OnChange(string S){ return true;}
}
internal class DelegateTest {
public static void Test() {
CallBackClass cb = new CallBackClass();
// Caution: The following reference on the cb object does not keep the
// object from being garbage collected after the Main method
// executes.
ExternalAPI.SetChangeHandler(new ChangeDelegate(cb.OnChange));
}
}
Aby zrekompensować nieoczekiwane odzyskiwanie pamięci, obiekt wywołujący musi upewnić się, że cb
obiekt jest nadal aktywny, o ile wskaźnik funkcji niezarządzanej jest używany. Opcjonalnie możesz mieć niezarządzany kod powiadamiający zarządzany kod, gdy wskaźnik funkcji nie jest już potrzebny, jak pokazano w poniższym przykładzie.
internal class DelegateTest {
CallBackClass cb;
// Called before ever using the callback function.
public static void SetChangeHandler() {
cb = new CallBackClass();
ExternalAPI.SetChangeHandler(new ChangeDelegate(cb.OnChange));
}
// Called after using the callback function for the last time.
public static void RemoveChangeHandler() {
// The cb object can be collected now. The unmanaged code is
// finished with the callback function.
cb = null;
}
}
Domyślne marshalling dla typów wartości
Większość typów wartości, takich jak liczby całkowite i liczby zmiennoprzecinkowe, jest blittable i nie wymaga marshalingu. Inne typy nienależące do blittable mają różne reprezentacje w zarządzanej i niezarządzanej pamięci i wymagają marshallingu. Nadal inne typy wymagają jawnego formatowania w granicach międzyoperacyjnych.
Ta sekcja zawiera informacje na temat następujących sformatowanych typów wartości:
Oprócz opisywania sformatowanych typów w tym temacie przedstawiono typy wartości systemowych, które mają nietypowe zachowanie marshallingu.
Sformatowany typ jest typem złożonym zawierającym informacje, które jawnie steruje układem jego elementów członkowskich w pamięci. Informacje o układzie elementu członkowskiego są udostępniane przy użyciu atrybutu StructLayoutAttribute . Układ może być jedną z następujących LayoutKind wartości wyliczenia:
LayoutKind.Auto
Wskazuje, że środowisko uruchomieniowe języka wspólnego może zmienić kolejność elementów członkowskich typu pod kątem wydajności. Jednak po przekazaniu typu wartości do kodu niezarządzanego układ elementów członkowskich jest przewidywalny. Próba automatycznego marshalingu takiej struktury powoduje wyjątek.
Layoutkind.sequential
Wskazuje, że składowe typu mają być określone w niezarządzanej pamięci w tej samej kolejności, w jakiej są wyświetlane w definicji typu zarządzanego.
Layoutkind.explicit
Wskazuje, że składowe są określone zgodnie z podanym FieldOffsetAttribute polem.
Typy wartości używane w wywołaniu platformy
W poniższym przykładzie typy Point
i Rect
zawierają informacje o układzie składowym przy użyciu atrybutu StructLayoutAttribute.
Imports System.Runtime.InteropServices
<StructLayout(LayoutKind.Sequential)> Public Structure Point
Public x As Integer
Public y As Integer
End Structure
<StructLayout(LayoutKind.Explicit)> Public Structure Rect
<FieldOffset(0)> Public left As Integer
<FieldOffset(4)> Public top As Integer
<FieldOffset(8)> Public right As Integer
<FieldOffset(12)> Public bottom As Integer
End Structure
using System.Runtime.InteropServices;
[StructLayout(LayoutKind.Sequential)]
public struct Point {
public int x;
public int y;
}
[StructLayout(LayoutKind.Explicit)]
public struct Rect {
[FieldOffset(0)] public int left;
[FieldOffset(4)] public int top;
[FieldOffset(8)] public int right;
[FieldOffset(12)] public int bottom;
}
Po przesłaniu do niezarządzanego kodu te sformatowane typy są rozdzielane jako struktury stylu C. Zapewnia to łatwy sposób wywoływania niezarządzanego interfejsu API, który ma argumenty struktury. Na przykład POINT
struktury i RECT
można przekazać do funkcji PtInRect interfejsu API systemu Microsoft Windows w następujący sposób:
BOOL PtInRect(const RECT *lprc, POINT pt);
Struktury można przekazywać przy użyciu następującej definicji wywołania platformy:
Friend Class NativeMethods
Friend Declare Auto Function PtInRect Lib "User32.dll" (
ByRef r As Rect, p As Point) As Boolean
End Class
internal static class NativeMethods
{
[DllImport("User32.dll")]
internal static extern bool PtInRect(ref Rect r, Point p);
}
Rect
Typ wartości musi zostać przekazany przez odwołanie, ponieważ niezarządzany interfejs API oczekuje przekazania wskaźnika do RECT
funkcji. Typ Point
wartości jest przekazywany przez wartość, ponieważ niezarządzany interfejs API oczekuje POINT
przekazania elementu na stosie. Ta subtelna różnica jest bardzo ważna. Odwołania są przekazywane do niezarządzanego kodu jako wskaźników. Wartości są przekazywane do niezarządzanego kodu na stosie.
Uwaga
Gdy sformatowany typ jest rozdzielany jako struktura, dostępne są tylko pola w danym typie. Jeśli typ zawiera metody, właściwości lub zdarzenia, są one niedostępne z niezarządzanego kodu.
Klasy mogą być również rozdzielane do niezarządzanego kodu jako struktur stylu C, pod warunkiem, że mają stały układ składowy. Informacje o układzie składowym dla klasy są również dostarczane z atrybutem StructLayoutAttribute . Główną różnicą między typami wartości ze stałym układem i klasami ze stałym układem jest sposób, w jaki są one rozdzielane do niezarządzanego kodu. Typy wartości są przekazywane przez wartość (na stosie), a w związku z tym wszelkie zmiany wprowadzone w elementach członkowskich typu przez obiekt wywołujący nie są widoczne dla obiektu wywołującego. Typy referencyjne są przekazywane przez odwołanie (odwołanie do typu jest przekazywane na stosie); w związku z tym wszystkie zmiany wprowadzone w elementach członkowskich typu blittable typu przez obiekt wywołujący są widoczne przez obiekt wywołujący.
Uwaga
Jeśli typ odwołania zawiera elementy członkowskie typów niezwiązanych z blittable, konwersja jest wymagana dwa razy: po raz pierwszy po przekazaniu argumentu do niezarządzanej strony i po raz drugi po powrocie z wywołania. Ze względu na to dodane obciążenie parametry we/wy muszą być jawnie stosowane do argumentu, jeśli obiekt wywołujący chce zobaczyć zmiany wprowadzone przez obiekt wywoływany.
W poniższym przykładzie SystemTime
klasa ma układ składowych sekwencyjnych i może zostać przekazana do funkcji GetSystemTime interfejsu API systemu Windows.
<StructLayout(LayoutKind.Sequential)> Public Class SystemTime
Public wYear As System.UInt16
Public wMonth As System.UInt16
Public wDayOfWeek As System.UInt16
Public wDay As System.UInt16
Public wHour As System.UInt16
Public wMinute As System.UInt16
Public wSecond As System.UInt16
Public wMilliseconds As System.UInt16
End Class
[StructLayout(LayoutKind.Sequential)]
public class SystemTime {
public ushort wYear;
public ushort wMonth;
public ushort wDayOfWeek;
public ushort wDay;
public ushort wHour;
public ushort wMinute;
public ushort wSecond;
public ushort wMilliseconds;
}
Funkcja GetSystemTime jest zdefiniowana w następujący sposób:
void GetSystemTime(SYSTEMTIME* SystemTime);
Równoważna definicja wywołania platformy dla polecenia GetSystemTime jest następująca:
Friend Class NativeMethods
Friend Declare Auto Sub GetSystemTime Lib "Kernel32.dll" (
ByVal sysTime As SystemTime)
End Class
internal static class NativeMethods
{
[DllImport("Kernel32.dll", CharSet = CharSet.Auto)]
internal static extern void GetSystemTime(SystemTime st);
}
Zwróć uwagę, że SystemTime
argument nie jest wpisany jako argument odwołania, ponieważ SystemTime
jest klasą, a nie typem wartości. W przeciwieństwie do typów wartości klasy są zawsze przekazywane przez odwołanie.
Poniższy przykład kodu przedstawia inną Point
klasę, która ma metodę o nazwie SetXY
. Ponieważ typ ma układ sekwencyjny, można go przekazać do niezarządzanego kodu i uruchomić jako strukturę. Jednak element SetXY
członkowski nie jest wywoływany z niezarządzanego kodu, mimo że obiekt jest przekazywany przez odwołanie.
<StructLayout(LayoutKind.Sequential)> Public Class Point
Private x, y As Integer
Public Sub SetXY(x As Integer, y As Integer)
Me.x = x
Me.y = y
End Sub
End Class
[StructLayout(LayoutKind.Sequential)]
public class Point {
int x, y;
public void SetXY(int x, int y){
this.x = x;
this.y = y;
}
}
Typy wartości używane w międzyoperacyjności modelu COM
Sformatowane typy można również przekazać do wywołań metod międzyoperacyjnej modelu COM. W rzeczywistości podczas eksportowania do biblioteki typów typy są automatycznie konwertowane na struktury. Jak pokazano w poniższym przykładzie, Point
typ wartości staje się definicją typu (typedef) o nazwie Point
. Wszystkie odwołania do Point
typu wartości w innej części biblioteki typów są zastępowane definicją Point
typedef.
Reprezentacja biblioteki typów
typedef struct tagPoint {
int x;
int y;
} Point;
interface _Graphics {
…
HRESULT SetPoint ([in] Point p)
HRESULT SetPointRef ([in,out] Point *p)
HRESULT GetPoint ([out,retval] Point *p)
}
Te same reguły używane do marshalingu wartości i odwołań do wywołań wywołań platformy są używane podczas marshalingu za pośrednictwem interfejsów COM. Na przykład gdy wystąpienie Point
typu wartości jest przekazywane z programu .NET Framework do modelu COM, Point
wartość jest przekazywana przez wartość. Point
Jeśli typ wartości jest przekazywany przez odwołanie, wskaźnik do elementu Point
jest przekazywany na stosie. Marshaller międzyoperacji nie obsługuje wyższego poziomu pośredniego (punkt **) w obu kierunkach.
Uwaga
Struktury o wartości wyliczenia ustawionej LayoutKind na Jawne nie mogą być używane w międzyoperacyjności MODELU COM, ponieważ wyeksportowana biblioteka typów nie może wyrazić jawnego układu.
Typy wartości systemowych
System Przestrzeń nazw ma kilka typów wartości reprezentujących pole typu pierwotnego środowiska uruchomieniowego. Na przykład struktura typu System.Int32 wartości reprezentuje postać pola ELEMENT_TYPE_I4. Zamiast marshalling tych typów jako struktur, jak inne sformatowane typy są, można je marshaling w taki sam sposób jak typy pierwotne, które są w polu. System.Int32 jest zatem marshalled jako ELEMENT_TYPE_I4 zamiast jako struktura zawierająca jeden element członkowski typu długi. Poniższa tabela zawiera listę typów wartości w przestrzeni nazw systemu , które są reprezentacjami typów pierwotnych.
Typ wartości systemowej | Typ elementu |
---|---|
System.Boolean | ELEMENT_TYPE_BOOLEAN |
System.SByte | ELEMENT_TYPE_I1 |
System.Byte | ELEMENT_TYPE_UI1 |
System.Char | ELEMENT_TYPE_CHAR |
System.Int16 | ELEMENT_TYPE_I2 |
System.UInt16 | ELEMENT_TYPE_U2 |
System.Int32 | ELEMENT_TYPE_I4 |
System.UInt32 | ELEMENT_TYPE_U4 |
System.Int64 | ELEMENT_TYPE_I8 |
System.UInt64 | ELEMENT_TYPE_U8 |
System.Single | ELEMENT_TYPE_R4 |
System.Double | ELEMENT_TYPE_R8 |
System.String | ELEMENT_TYPE_STRING |
System.IntPtr | ELEMENT_TYPE_I |
System.UIntPtr | ELEMENT_TYPE_U |
Niektóre inne typy wartości w przestrzeni nazw systemu są obsługiwane inaczej. Ponieważ niezarządzany kod ma już dobrze ugruntowane formaty dla tych typów, marshaller ma specjalne zasady dotyczące ich marshalingu. W poniższej tabeli wymieniono specjalne typy wartości w przestrzeni nazw systemowej , a także typ niezarządzany, do którego są one rozdzielane.
Typ wartości systemowej | Typ IDL |
---|---|
System.DateTime | DATA |
System.Decimal | DZIESIĘTNYCH |
System.Guid | Identyfikator GUID |
System.Drawing.Color | OLE_COLOR |
Poniższy kod przedstawia definicję typów niezarządzanych DATE, GUID, DECIMAL i OLE_COLOR w bibliotece typów Stdole2.
Reprezentacja biblioteki typów
typedef double DATE;
typedef DWORD OLE_COLOR;
typedef struct tagDEC {
USHORT wReserved;
BYTE scale;
BYTE sign;
ULONG Hi32;
ULONGLONG Lo64;
} DECIMAL;
typedef struct tagGUID {
DWORD Data1;
WORD Data2;
WORD Data3;
BYTE Data4[ 8 ];
} GUID;
Poniższy kod przedstawia odpowiednie definicje w interfejsie zarządzanym IValueTypes
.
Public Interface IValueTypes
Sub M1(d As System.DateTime)
Sub M2(d As System.Guid)
Sub M3(d As System.Decimal)
Sub M4(d As System.Drawing.Color)
End Interface
public interface IValueTypes {
void M1(System.DateTime d);
void M2(System.Guid d);
void M3(System.Decimal d);
void M4(System.Drawing.Color d);
}
Reprezentacja biblioteki typów
[…]
interface IValueTypes : IDispatch {
HRESULT M1([in] DATE d);
HRESULT M2([in] GUID d);
HRESULT M3([in] DECIMAL d);
HRESULT M4([in] OLE_COLOR d);
};