Udostępnij za pośrednictwem


Samouczek: używanie niestandardowych marshallerów w generowanych przez źródło wywołaniach

W tym samouczku dowiesz się, jak zaimplementować marshaller i używać go do niestandardowego marshalingu w źródle generowanych wywołań P/Invoke.

Zaimplementujesz marshallers dla wbudowanego typu, dostosujesz marshalling dla określonego parametru i typu zdefiniowanego przez użytkownika i określisz domyślne marshalling dla typu zdefiniowanego przez użytkownika.

Cały kod źródłowy używany w tym samouczku jest dostępny w repozytorium dotnet/samples.

Omówienie generatora LibraryImport źródła

Typ System.Runtime.InteropServices.LibraryImportAttribute to punkt wejścia użytkownika dla generatora źródłowego wprowadzonego na platformie .NET 7. Ten generator źródła jest przeznaczony do generowania całego kodu marshalling w czasie kompilacji zamiast w czasie wykonywania. Punkty wejścia zostały historycznie określone przy użyciu metody DllImport, ale takie podejście wiąże się z kosztami, które nie zawsze mogą być akceptowalne — aby uzyskać więcej informacji, zobacz Generowanie źródła P/Invoke. Generator LibraryImport źródła może wygenerować cały kod marshallingu i usunąć wymaganie generowania w czasie wykonywania wewnętrznego elementu DllImport.

Aby wyrazić szczegóły potrzebne do wygenerowania kodu marshalling zarówno dla środowiska uruchomieniowego, jak i dla użytkowników w celu dostosowania ich własnych typów, potrzebne są kilka typów. W tym samouczku są używane następujące typy:

  • MarshalUsingAttribute — atrybut poszukiwany przez generator źródła w miejscach użycia i używany do określania typu marshallera do marshallingu zmiennej przypisanej.

  • CustomMarshallerAttribute — Atrybut używany do wskazania marshallera dla typu i trybu, w którym mają być wykonywane operacje marshallingu (na przykład przez ref z zarządzanego do niezarządzanego).

  • NativeMarshallingAttribute — atrybut używany do wskazywania, który marshaller ma być używany dla typu atrybutu. Jest to przydatne w przypadku autorów bibliotek, które udostępniają typy i towarzyszące im marshallers dla tych typów.

Te atrybuty nie są jednak jedynymi mechanizmami dostępnymi dla niestandardowego autora marshallera. Generator źródła sprawdza sam marshaller pod kątem różnych innych wskazówek, które informują o tym, jak należy przeprowadzić marshalling.

Szczegółowe informacje na temat projektu można znaleźć w repozytorium dotnet/runtime .

Analizator generatora źródła i poprawki

Wraz z samym generatorem źródłowym zapewniany jest analizator i fixer. Analizator i poprawki są domyślnie włączone i dostępne od czasu platformy .NET 7 RC1. Analizator został zaprojektowany w celu ułatwienia deweloperom prawidłowego używania generatora źródłowego. Fixer zapewnia automatyczne konwersje z wielu DllImport wzorców do odpowiedniego LibraryImport podpisu.

Wprowadzenie do biblioteki natywnej

Użycie generatora źródłowego LibraryImport oznaczałoby korzystanie z natywnej lub niezarządzanej biblioteki. Biblioteka natywna może być biblioteką udostępnioną (czyli , .dll.solub dylib), która bezpośrednio wywołuje interfejs API systemu operacyjnego, który nie jest udostępniany za pośrednictwem platformy .NET. Biblioteka może być również taka, która jest mocno zoptymalizowana w języku niezarządzanym, z którego chce korzystać deweloper platformy .NET. W tym samouczku utworzysz własną bibliotekę udostępnioną, która uwidacznia powierzchnię interfejsu API w stylu C. Poniższy kod reprezentuje typ zdefiniowany przez użytkownika i dwa interfejsy API, które będą używane z języka C#. Te dwa interfejsy API reprezentują tryb "w", ale istnieją dodatkowe tryby do eksplorowania w przykładzie.

struct error_data
{
    int code;
    bool is_fatal_error;
    char32_t* message;    /* UTF-32 encoded string */
};

extern "C" DLL_EXPORT void STDMETHODCALLTYPE PrintString(char32_t* chars);

extern "C" DLL_EXPORT void STDMETHODCALLTYPE PrintErrorData(error_data data);

Powyższy kod zawiera dwa typy zainteresowań i char32_t*error_data. char32_t* reprezentuje ciąg zakodowany w formacie UTF-32, który nie jest kodowaniem ciągów, który historycznie marshaluje platformę .NET. error_data jest typem zdefiniowanym przez użytkownika, który zawiera 32-bitowe pole całkowite, pole logiczne języka C++ i pole ciągów zakodowanych w formacie UTF-32. Oba te typy wymagają zapewnienia sposobu generowania kodu marshalling przez generator źródła.

Dostosowywanie marshalingu dla wbudowanego typu

char32_t* Najpierw należy wziąć pod uwagę typ, ponieważ typ ten jest wymagany przez typ zdefiniowany przez użytkownika. char32_t* reprezentuje stronę natywną, ale potrzebna jest również reprezentacja w kodzie zarządzanym. Na platformie .NET istnieje tylko jeden typ "ciąg" . string W związku z tym będziesz marshalling natywny ciąg zakodowany UTF-32 do i z typu w kodzie zarządzanym string . Istnieje już kilka wbudowanych marshallers dla string typu, który marshal jako UTF-8, UTF-16, ANSI, a nawet jako typ systemu Windows BSTR . Nie ma jednak jednego do marshallingu jako UTF-32. To właśnie musisz zdefiniować.

Typ Utf32StringMarshaller jest oznaczony atrybutem CustomMarshaller , który opisuje, co robi z generatorem źródłowym. Pierwszy argument typu do atrybutu jest typem string , typem zarządzanym do marshalingu, drugim jest tryb, który wskazuje, kiedy należy używać marshallera, a trzeci typ to Utf32StringMarshaller, typ do użycia do marshalingu. Można zastosować CustomMarshaller wiele razy, aby dodatkowo określić tryb i typ marshallera, który ma być używany dla tego trybu.

W bieżącym przykładzie pokazano marshaller "bezstanowy", który pobiera dane wejściowe i zwraca dane w postaci marshalled. Metoda Free istnieje dla symetrii z niezarządzanymi marshalling, a moduł odśmiecania pamięci jest operacją "bezpłatną" dla zarządzanego marshallera. Implementator może wykonać dowolne operacje, aby przeprowadzić marshaling danych wejściowych do danych wyjściowych, ale pamiętaj, że żaden stan nie zostanie jawnie zachowany przez generator źródła.

namespace CustomMarshalling
{
    [CustomMarshaller(typeof(string), MarshalMode.Default, typeof(Utf32StringMarshaller))]
    internal static unsafe class Utf32StringMarshaller
    {
        public static uint* ConvertToUnmanaged(string? managed)
            => throw new NotImplementedException();

        public static string? ConvertToManaged(uint* unmanaged)
            => throw new NotImplementedException();

        public static void Free(uint* unmanaged)
            => throw new NotImplementedException();
    }
}

Specyfika tego konkretnego marshallera wykonuje konwersję z string na char32_t* można znaleźć w przykładzie. Należy pamiętać, że można użyć dowolnych interfejsów API platformy .NET (na przykład Encoding.UTF32).

Rozważmy przypadek, w którym stan jest pożądany. Zwróć uwagę na dodatkowy CustomMarshaller tryb i zwróć uwagę na bardziej szczegółowy tryb . MarshalMode.ManagedToUnmanagedIn Ten wyspecjalizowany marshaller jest implementowany jako "stanowy" i może przechowywać stan w całym wywołaniu międzyoperacjowym. Większa specjalizacja i optymalizacje zezwól na stan oraz dostosowane marshalling dla trybu. Na przykład generator źródła można poinstruować, aby zapewnić bufor przydzielony do stosu, który może uniknąć jawnej alokacji podczas marshalingu. Aby wskazać obsługę buforu przydzielonego stosu, marshaller implementuje BufferSize właściwość i FromManaged metodę, która przyjmuje Spanunmanaged typ. Właściwość BufferSize wskazuje ilość miejsca w stosie — długość Span , do których ma zostać przekazana FromManaged— marshaller chciałby dostać się podczas wywołania marshala.

namespace CustomMarshalling
{
    [CustomMarshaller(typeof(string), MarshalMode.Default, typeof(Utf32StringMarshaller))]
    [CustomMarshaller(typeof(string), MarshalMode.ManagedToUnmanagedIn, typeof(ManagedToUnmanagedIn))]
    internal static unsafe class Utf32StringMarshaller
    {
        //
        // Stateless functions removed
        //

        public ref struct ManagedToUnmanagedIn
        {
            public static int BufferSize => 0x100;

            private uint* _unmanagedValue;
            private bool _allocated; // Used stack alloc or allocated other memory

            public void FromManaged(string? managed, Span<byte> buffer)
                => throw new NotImplementedException();

            public uint* ToUnmanaged()
                => throw new NotImplementedException();

            public void Free()
                => throw new NotImplementedException();
        }
    }
}

Teraz można wywołać pierwszą z dwóch funkcji natywnych przy użyciu marshallerów ciągów UTF-32. Poniższa deklaracja używa atrybutu , podobnie jak DllImport, ale opiera się na MarshalUsing atrybucieLibraryImport, aby poinformować generator źródła, który marshaller ma być używany podczas wywoływania funkcji natywnej. Nie ma potrzeby wyjaśnienia, czy należy używać bezstanowego lub stanowego marshallera. Jest to obsługiwane przez implementator definiujący MarshalMode atrybuty CustomMarshaller marshallera. Generator źródła wybierze najbardziej odpowiedni marshaller na podstawie kontekstu, w którym MarshalUsing jest stosowany, z MarshalMode.Default bycia rezerwowym.

// extern "C" DLL_EXPORT void STDMETHODCALLTYPE PrintString(char32_t* chars);
[LibraryImport(LibName)]
internal static partial void PrintString([MarshalUsing(typeof(Utf32StringMarshaller))] string s);

Dostosowywanie marshalingu dla typu zdefiniowanego przez użytkownika

Marshalling typu zdefiniowanego przez użytkownika wymaga zdefiniowania nie tylko logiki marshalingu, ale także typu w języku C# do marshalingu do/z. Przypomnij sobie typ natywny, który próbujemy przeprowadzić marshaling.

struct error_data
{
    int code;
    bool is_fatal_error;
    char32_t* message;    /* UTF-32 encoded string */
};

Teraz zdefiniuj, jak najlepiej będzie wyglądać w języku C#. Element int jest taki sam jak w nowoczesnym języku C++ i na platformie .NET. A bool to kanoniczny przykład wartości logicznej na platformie .NET. Opierając się na systemie Utf32StringMarshaller, możesz przeprowadzić marshaling char32_t* jako .NET string. Księgowość stylu platformy .NET jest następującą definicją w języku C#:

struct ErrorData
{
    public int Code;
    public bool IsFatalError;
    public string? Message;
}

Zgodnie ze wzorcem nazewnictwa nadaj nazwę marshaller ErrorDataMarshaller. Zamiast określać marshaller dla MarshalMode.Default, zdefiniujesz tylko marshallers dla niektórych trybów. W takim przypadku, jeśli marshaller jest używany dla trybu, który nie jest podany, generator źródła zakończy się niepowodzeniem. Zacznij od zdefiniowania marshallera dla kierunku "w". Jest to "bezstanowy" marshaller, ponieważ sam marshaller składa się tylko z static funkcji.

namespace CustomMarshalling
{
    [CustomMarshaller(typeof(ErrorData), MarshalMode.ManagedToUnmanagedIn, typeof(ErrorDataMarshaller))]
    internal static unsafe class ErrorDataMarshaller
    {
        // Unmanaged representation of ErrorData.
        // Should mimic the unmanaged error_data type at a binary level.
        internal struct ErrorDataUnmanaged
        {
            public int Code;        // .NET doesn't support less than 32-bit, so int is 32-bit.
            public byte IsFatal;    // The C++ bool is defined as a single byte.
            public uint* Message;   // This could be as simple as a void*, but uint* is closer.
        }

        public static ErrorDataUnmanaged ConvertToUnmanaged(ErrorData managed)
            => throw new NotImplementedException();

        public static void Free(ErrorDataUnmanaged unmanaged)
            => throw new NotImplementedException();
    }
}

ErrorDataUnmanaged naśladuje kształt niezarządzanego typu. Konwersja z elementu na ErrorData element ErrorDataUnmanaged jest teraz trywialna za pomocą polecenia Utf32StringMarshaller.

Marshalling elementu int jest niepotrzebny, ponieważ jego reprezentacja jest identyczna w niezarządzanym i zarządzanym kodzie. bool Reprezentacja binarna wartości nie jest zdefiniowana na platformie .NET, dlatego użyj jej bieżącej wartości, aby zdefiniować wartość zero i niezerową w typie niezarządzanym. Następnie ponownie użyj marshallera UTF-32, aby przekonwertować string pole na uint*.

public static ErrorDataUnmanaged ConvertToUnmanaged(ErrorData managed)
{
    return new ErrorDataUnmanaged
    {
        Code = managed.Code,
        IsFatal = (byte)(managed.IsFatalError ? 1 : 0),
        Message = Utf32StringMarshaller.ConvertToUnmanaged(managed.Message),
    };
}

Pamiętaj, że definiujesz ten marshaller jako "w", więc należy wyczyścić wszystkie alokacje wykonywane podczas marshallingu. Pola int i bool nie przydzieliły żadnej pamięci, ale Message pole. Ponownie użyj ponownie Utf32StringMarshaller , aby wyczyścić ciąg marshalled.

public static void Free(ErrorDataUnmanaged unmanaged)
    => Utf32StringMarshaller.Free(unmanaged.Message);

Pokrótce rozważmy scenariusz "out". Rozważmy przypadek, w którym zwracane jest jedno lub wiele wystąpień error_data .

extern "C" DLL_EXPORT error_data STDMETHODCALLTYPE GetFatalErrorIfNegative(int code)

extern "C" DLL_EXPORT error_data* STDMETHODCALLTYPE GetErrors(int* codes, int len)
[LibraryImport(LibName)]
internal static partial ErrorData GetFatalErrorIfNegative(int code);

[LibraryImport(LibName)]
[return: MarshalUsing(CountElementName = "len")]
internal static partial ErrorData[] GetErrors(int[] codes, int len);

P/Invoke, który zwraca pojedynczy typ wystąpienia, bez kolekcji, jest klasyfikowany jako MarshalMode.ManagedToUnmanagedOut. Zazwyczaj używa się kolekcji do zwracania wielu elementów, a w tym przypadku Array jest używany element . Marshaller dla scenariusza kolekcji, odpowiadający MarshalMode.ElementOut trybowi, zwróci wiele elementów i zostanie opisany później.

namespace CustomMarshalling
{
    [CustomMarshaller(typeof(ErrorData), MarshalMode.ManagedToUnmanagedIn, typeof(ErrorDataMarshaller))]
    [CustomMarshaller(typeof(ErrorData), MarshalMode.ElementOut, typeof(Out))]
    internal static unsafe class ErrorDataMarshaller
    {
        //
        // Other marshallers removed
        //

        public static class Out
        {
            public static ErrorData ConvertToManaged(ErrorDataUnmanaged unmanaged)
                => throw new NotImplementedException();

            public static ErrorDataUnmanaged ConvertToUnmanaged(ErrorData managed)
                => throw new NotImplementedException();

            public static void Free(ErrorDataUnmanaged unmanaged)
                => throw new NotImplementedException();
        }
    }
}

Konwersja z ErrorDataUnmanaged na ErrorData jest odwrotnością tego, co zrobiłeś dla trybu "w". Należy pamiętać, że należy również wyczyścić wszystkie alokacje, które środowisko niezarządzane oczekuje się wykonania. Ważne jest również, aby pamiętać, że funkcje tutaj są oznaczone static i dlatego są "bezstanowe", bycie bezstanowym jest wymaganiem dla wszystkich trybów "Element". Zauważysz również, że istnieje metoda podobna ConvertToUnmanaged do w trybie "w". Wszystkie tryby "Element" wymagają obsługi trybów "in" i "out".

Dla zarządzanego "out" marshaller, zamierzasz zrobić coś wyjątkowego. Nazwa typu danych, który ma być wywoływany error_data , i platforma .NET zwykle wyraża błędy jako wyjątki. Niektóre błędy są bardziej wpływające niż inne, a błędy zidentyfikowane jako "krytyczne" zwykle wskazują na katastrofalny lub nieodwracalny błąd. Zwróć uwagę, że pole error_data zawiera pole, aby sprawdzić, czy błąd jest krytyczny. Przeliczysz element do kodu zarządzanego error_data , a jeśli jest krytyczny, zgłosisz wyjątek, a nie tylko przekonwertowasz go na ErrorData element i zwrócisz go.

namespace CustomMarshalling
{
    [CustomMarshaller(typeof(ErrorData), MarshalMode.ManagedToUnmanagedIn, typeof(ErrorDataMarshaller))]
    [CustomMarshaller(typeof(ErrorData), MarshalMode.ElementOut, typeof(Out))]
    [CustomMarshaller(typeof(ErrorData), MarshalMode.ManagedToUnmanagedOut, typeof(ThrowOnFatalErrorOut))]
    internal static unsafe class ErrorDataMarshaller
    {
        //
        // Other marshallers removed
        //

        public static class ThrowOnFatalErrorOut
        {
            public static ErrorData ConvertToManaged(ErrorDataUnmanaged unmanaged)
                => throw new NotImplementedException();

            public static void Free(ErrorDataUnmanaged unmanaged)
                => throw new NotImplementedException();
        }
    }
}

Parametr "out" konwertuje z kontekstu niezarządzanego na kontekst zarządzany, aby zaimplementować metodę ConvertToManaged . Gdy niezarządzany obiekt wywoływany zwraca obiekt i udostępnia ErrorDataUnmanaged go, możesz sprawdzić go przy użyciu ElementOut marshallera trybu i sprawdzić, czy jest oznaczony jako błąd krytyczny. Jeśli tak, oznacza to, że należy zgłosić zamiast po prostu zwrócić ErrorDatawartość .

public static ErrorData ConvertToManaged(ErrorDataUnmanaged unmanaged)
{
    ErrorData data = Out.ConvertToManaged(unmanaged);
    if (data.IsFatalError)
        throw new ExternalException(data.Message, data.Code);

    return data;
}

Być może nie tylko zamierzasz korzystać z biblioteki natywnej, ale także chcesz udostępnić swoją pracę społeczności i udostępnić bibliotekę międzyoperacyjną. Możesz podać ErrorData dorozumiany marshaller za każdym razem, gdy jest używany w P/Invoke, dodając [NativeMarshalling(typeof(ErrorDataMarshaller))] do ErrorData definicji. Teraz każda osoba korzystająca z twojej definicji tego typu w LibraryImport wywołaniu uzyska korzyści ze swoich marshallers. Zawsze mogą zastąpić marshallers przy użyciu w MarshalUsing miejscu użytkowania.

[NativeMarshalling(typeof(ErrorDataMarshaller))]
struct ErrorData { ... }

Zobacz też