Dostosowywanie marshalingu struktury

Czasami domyślne reguły marshalingu dla struktur nie są dokładnie tym, czego potrzebujesz. Środowiska uruchomieniowe platformy .NET udostępniają kilka punktów rozszerzenia, które umożliwiają dostosowanie układu struktury i sposobu rozmieszczenia pól. Dostosowywanie układu struktury jest obsługiwane we wszystkich scenariuszach, ale dostosowywanie marshalingu pól jest obsługiwane tylko w scenariuszach, w których włączono marshalling środowiska uruchomieniowego. Jeśli marshalling środowiska uruchomieniowego jest wyłączony, należy ręcznie wykonać dowolne marshalling pola.

Uwaga

W tym artykule nie omówiono dostosowywania marshalingu dla międzyoperacjności generowanej przez źródło. Jeśli używasz międzyoperacyjności generowanej przez źródło dla wywołań P/Invoke lub COM, zobacz Dostosowywanie marshalingu.

Dostosowywanie układu struktury

Platforma .NET udostępnia System.Runtime.InteropServices.StructLayoutAttribute atrybut i System.Runtime.InteropServices.LayoutKind wyliczenie umożliwiające dostosowanie sposobu umieszczania pól w pamięci. Poniższe wskazówki pomogą Uniknąć typowych problemów.

✔️ ROZWAŻ użycie zawsze LayoutKind.Sequential , gdy jest to możliwe.

✔️ Funkcja DO używać LayoutKind.Explicit tylko w marshalingu, gdy struktura natywna ma również jawny układ, taki jak unia.

❌ UNIKAJ używania klas do wyrażania złożonych typów natywnych za pośrednictwem dziedziczenia.

❌ UNIKAJ używania LayoutKind.Explicit podczas określania struktury marshalingu na platformach innych niż Windows, jeśli musisz kierować środowiska uruchomieniowe przed platformą .NET Core 3.0. Środowisko uruchomieniowe platformy .NET Core przed 3.0 nie obsługuje przekazywania jawnych struktur według wartości do funkcji natywnych w systemach Intel lub AMD 64-bitowych systemów innych niż Windows. Jednak środowisko uruchomieniowe obsługuje przekazywanie jawnych struktur przez odwołanie na wszystkich platformach.

Dostosowywanie marshalingu pól logicznych

Kod natywny ma wiele różnych reprezentacji logicznych. W systemie Windows istnieją trzy sposoby reprezentowania wartości logicznych. Środowisko uruchomieniowe nie zna natywnej definicji struktury, więc najlepszym rozwiązaniem może być zgadywanie sposobu marshalowania wartości logicznych. Środowisko uruchomieniowe platformy .NET umożliwia wskazanie sposobu marshalingu pola logicznego. W poniższych przykładach pokazano, jak marshalować platformę .NET bool do różnych natywnych typów logicznych.

Wartości logiczne domyślnie są domyślnie marshaling jako natywna wartość Win32 BOOL 4-bajtowa, jak pokazano w poniższym przykładzie:

public struct WinBool
{
    public bool b;
}
struct WinBool
{
    public BOOL b;
};

Jeśli chcesz być jawne, możesz użyć UnmanagedType.Bool wartości , aby uzyskać takie samo zachowanie jak powyżej:

public struct WinBool
{
    [MarshalAs(UnmanagedType.Bool)]
    public bool b;
}
struct WinBool
{
    public BOOL b;
};

Korzystając z poniższych UnmanagedType.U1 wartości lub UnmanagedType.I1 , możesz poinformować środowisko uruchomieniowe, aby marshaling b pola jako typ natywny bool 1-bajtowy.

public struct CBool
{
    [MarshalAs(UnmanagedType.U1)]
    public bool b;
}
struct CBool
{
    public bool b;
};

W systemie Windows możesz użyć UnmanagedType.VariantBool wartości , aby poinformować środowisko uruchomieniowe o przesłaniu wartości logicznej do wartości 2-bajtowej VARIANT_BOOL :

public struct VariantBool
{
    [MarshalAs(UnmanagedType.VariantBool)]
    public bool b;
}
struct VariantBool
{
    public VARIANT_BOOL b;
};

Uwaga

VARIANT_BOOL różni się od większości typów logicznych w tym VARIANT_TRUE = -1 i VARIANT_FALSE = 0. Ponadto wszystkie wartości, które nie są równe VARIANT_TRUE , są uznawane za fałszywe.

Dostosowywanie marshalingu pól tablicy

Platforma .NET zawiera również kilka sposobów dostosowywania marshalingu tablicy.

Domyślnie platforma .NET marshals tablice jako wskaźnik do ciągłej listy elementów:

public struct DefaultArray
{
    public int[] values;
}
struct DefaultArray
{
    int32_t* values;
};

Jeśli łączysz się z interfejsami API MODELU COM, może być konieczne przeprowadzanie marshalingu tablic jako SAFEARRAY* obiektów. Możesz użyć System.Runtime.InteropServices.MarshalAsAttribute wartości i UnmanagedType.SafeArray , aby poinformować środowisko uruchomieniowe o marshalingu tablicy jako SAFEARRAY*:

public struct SafeArrayExample
{
    [MarshalAs(UnmanagedType.SafeArray)]
    public int[] values;
}
struct SafeArrayExample
{
    SAFEARRAY* values;
};

Jeśli musisz dostosować typ elementu w elemecie SAFEARRAY, możesz użyć MarshalAsAttribute.SafeArraySubType pól iMarshalAsAttribute.SafeArrayUserDefinedSubType, aby dostosować dokładny typ elementu .SAFEARRAY

Jeśli musisz przeprowadzić marshaling tablicy w miejscu, możesz użyć UnmanagedType.ByValArray wartości , aby poinformować marshaller o przesłaniu tablicy w miejscu. W przypadku korzystania z tego marshalingu należy również podać wartość MarshalAsAttribute.SizeConst pola dla liczby elementów w tablicy, aby środowisko uruchomieniowe mógł poprawnie przydzielić miejsce dla struktury.

public struct InPlaceArray
{
    [MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)]
    public int[] values;
}
struct InPlaceArray
{
    int values[4];
};

Uwaga

Platforma .NET nie obsługuje marshalingu zmiennej długości pola tablicy jako elementu członkowskiego tablicy elastycznej C99.

Dostosowywanie marshalingu pól ciągów

Platforma .NET udostępnia również szeroką gamę dostosowań dotyczących pól ciągów marshalingowych.

Domyślnie platforma .NET marshaluje ciąg jako wskaźnik do ciągu zakończonego wartością null. Kodowanie zależy od wartości StructLayoutAttribute.CharSet pola w obiekcie System.Runtime.InteropServices.StructLayoutAttribute. Jeśli nie określono żadnego atrybutu, kodowanie jest domyślnie kodujące kodowanie ANSI.

[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
public struct DefaultString
{
    public string str;
}
struct DefaultString
{
    char* str;
};
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
public struct DefaultString
{
    public string str;
}
struct DefaultString
{
    char16_t* str; // Could also be wchar_t* on Windows.
};

Jeśli musisz użyć różnych kodowań dla różnych pól lub po prostu wolisz być bardziej jawne w definicji struktury, możesz użyć UnmanagedType.LPStr wartości lub UnmanagedType.LPWStr dla atrybutu System.Runtime.InteropServices.MarshalAsAttribute .

public struct AnsiString
{
    [MarshalAs(UnmanagedType.LPStr)]
    public string str;
}
struct AnsiString
{
    char* str;
};
public struct UnicodeString
{
    [MarshalAs(UnmanagedType.LPWStr)]
    public string str;
}
struct UnicodeString
{
    char16_t* str; // Could also be wchar_t* on Windows.
};

Jeśli chcesz przeprowadzić marshaling ciągów przy użyciu kodowania UTF-8, możesz użyć UnmanagedType.LPUTF8Str wartości w pliku MarshalAsAttribute.

public struct UTF8String
{
    [MarshalAs(UnmanagedType.LPUTF8Str)]
    public string str;
}
struct UTF8String
{
    char* str;
};

Uwaga

Korzystanie z programu UnmanagedType.LPUTF8Str wymaga programu .NET Framework 4.7 (lub nowszych wersji) lub .NET Core 1.1 (lub nowszych wersji). Nie jest ona dostępna w programie .NET Standard 2.0.

Jeśli pracujesz z interfejsami API modelu COM, może być konieczne przeprowadzanie marshalingu ciągu jako .BSTR UnmanagedType.BStr Za pomocą wartości można marshaling ciągu jako BSTR.

public struct BString
{
    [MarshalAs(UnmanagedType.BStr)]
    public string str;
}
struct BString
{
    BSTR str;
};

W przypadku korzystania z interfejsu API opartego na technologii WinRT może być konieczne przeprowadzanie marshalingu ciągu jako elementu HSTRING. UnmanagedType.HString Za pomocą wartości można marshaling ciągu jako HSTRING. HSTRING obsługa marshallingu jest obsługiwana tylko w środowiskach uruchomieniowych z wbudowaną obsługą winRT. Obsługa winRT została usunięta na platformie .NET 5, więc HSTRING obsługa marshallingu nie jest obsługiwana na platformie .NET 5 lub nowszym.

public struct HString
{
    [MarshalAs(UnmanagedType.HString)]
    public string str;
}
struct BString
{
    HSTRING str;
};

Jeśli interfejs API wymaga przekazania ciągu w miejscu w strukturze, możesz użyć UnmanagedType.ByValTStr wartości . Należy pamiętać, że kodowanie dla ciągu marshalled przez ByValTStr jest określane z atrybutu CharSet . Ponadto wymaga, aby długość ciągu została przekazana MarshalAsAttribute.SizeConst przez pole.

[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
public struct DefaultString
{
    [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 4)]
    public string str;
}
struct DefaultString
{
    char str[4];
};
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
public struct DefaultString
{
    [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 4)]
    public string str;
}
struct DefaultString
{
    char16_t str[4]; // Could also be wchar_t[4] on Windows.
};

Dostosowywanie marshalingu pól dziesiętnych

Jeśli pracujesz w systemie Windows, możesz napotkać niektóre interfejsy API korzystające z natywnej CY lub CURRENCY struktury. Domyślnie typ platformy .NET decimal marshals do struktury natywnej DECIMAL . Można jednak użyć wartości MarshalAsAttribute z wartością UnmanagedType.Currency , aby poinstruować marshaller, aby przekonwertować decimal wartość na wartość natywną CY .

public struct Currency
{
    [MarshalAs(UnmanagedType.Currency)]
    public decimal dec;
}
struct Currency
{
    CY dec;
};

Unie

Związek to typ danych, który może zawierać różne typy danych na szczycie tej samej pamięci. Jest to typowa forma danych w języku C. Unii można wyrazić na platformie .NET przy użyciu polecenia LayoutKind.Explicit. Zaleca się używanie struktur podczas definiowania unii na platformie .NET. Użycie klas może powodować problemy z układem i powodować nieprzewidywalne zachowanie.

struct device1_config
{
    void* a;
    void* b;
    void* c;
};
struct device2_config
{
    int32_t a;
    int32_t b;
};
struct config
{
    int32_t type;

    union
    {
        device1_config dev1;
        device2_config dev2;
    };
};
public unsafe struct Device1Config
{
    void* a;
    void* b;
    void* c;
}

public struct Device2Config
{
    int a;
    int b;
}

public struct Config
{
    public int Type;

    public _Union Anonymous;

    [StructLayout(LayoutKind.Explicit)]
    public struct _Union
    {
        [FieldOffset(0)]
        public Device1Config Dev1;

        [FieldOffset(0)]
        public Device2Config Dev2;
    }
}

Marszałek System.Object

W systemie Windows można marshalować object-typed pól do kodu natywnego. Te pola można marshalingować do jednego z trzech typów:

Domyślnie objectpole -typed zostanie ustawione na obiekt IUnknown* , który opakowuje obiekt.

public struct ObjectDefault
{
    public object obj;
}
struct ObjectDefault
{
    IUnknown* obj;
};

Jeśli chcesz przesłonić pole obiektu do IDispatch*obiektu , dodaj element MarshalAsAttribute z wartością UnmanagedType.IDispatch .

public struct ObjectDispatch
{
    [MarshalAs(UnmanagedType.IDispatch)]
    public object obj;
}
struct ObjectDispatch
{
    IDispatch* obj;
};

Jeśli chcesz przeprowadzić marshaling go jako VARIANTelement , dodaj element MarshalAsAttribute z wartością UnmanagedType.Struct .

public struct ObjectVariant
{
    [MarshalAs(UnmanagedType.Struct)]
    public object obj;
}
struct ObjectVariant
{
    VARIANT obj;
};

W poniższej tabeli opisano różne typy środowiska uruchomieniowego mapowania obj pól na różne typy przechowywane w obiekcie VARIANT:

Typ platformy .NET TYP WARIANTU
byte VT_UI1
sbyte VT_I1
short VT_I2
ushort VT_UI2
int VT_I4
uint VT_UI4
long VT_I8
ulong VT_UI8
float VT_R4
double VT_R8
char VT_UI2
string VT_BSTR
System.Runtime.InteropServices.BStrWrapper VT_BSTR
object VT_DISPATCH
System.Runtime.InteropServices.UnknownWrapper VT_UNKNOWN
System.Runtime.InteropServices.DispatchWrapper VT_DISPATCH
System.Reflection.Missing VT_ERROR
(object)null VT_EMPTY
bool VT_BOOL
System.DateTime VT_DATE
decimal VT_DECIMAL
System.Runtime.InteropServices.CurrencyWrapper VT_CURRENCY
System.DBNull VT_NULL