Руководство. Использование пользовательских маршаллеров в созданных источником P/Invokes

В этом руководстве вы узнаете, как реализовать маршаллировщик и использовать его для пользовательского маршаллинга в источниках P/Invokes.

Вы реализуете маршаллеры для встроенного типа, настраиваете маршаллинг для определенного параметра и определяемого пользователем типа и укажите маршаллинг по умолчанию для определяемого пользователем типа.

Весь исходный код, используемый в этом руководстве, доступен в репозитории dotnet/samples.

Обзор исходного генератора LibraryImport

Тип System.Runtime.InteropServices.LibraryImportAttribute — это точка входа пользователя для генератора источников, представленного в .NET 7. Этот генератор источника предназначен для создания всего маршалинга кода во время компиляции, а не во время выполнения. Точки входа были исторически указаны с помощью DllImport, но этот подход поставляется с затратами, которые не всегда могут быть приемлемыми— дополнительные сведения см. в разделе "Создание источника P/Invoke". Генератор LibraryImport источника может создать весь код маршалинга и удалить внутреннее DllImportтребование создания времени выполнения.

Чтобы выразить подробные сведения, необходимые для создания кода маршалинга как для среды выполнения, так и для пользователей, чтобы настроить для своих собственных типов, необходимо несколько типов. В этом руководстве используются следующие типы:

  • MarshalUsingAttribute — Атрибут, который ищет генератор источника на сайтах использования и используется для определения типа маршаллатора для маршаллинга переменной атрибута.

  • CustomMarshallerAttribute — атрибут, используемый для указания маршала для типа и режима, в котором выполняются операции маршаллинга (например, путем ссылки от управляемого к неуправляемому).

  • NativeMarshallingAttribute — атрибут, используемый для указания того, какой маршаллатор используется для типа атрибута. Это полезно для авторов библиотек, предоставляющих типы и сопровождающие маршалеры для этих типов.

Однако эти атрибуты не являются единственными механизмами, доступными для пользовательского автора маршаллера. Генератор источника проверяет сам маршаллизатор для различных других признаков, которые сообщают о том, как должно происходить маршаллирование.

Полные сведения о дизайне можно найти в репозитории dotnet/runtime .

Анализатор генератора источника и средство исправления

Вместе с самим генератором источника предоставляются анализатор и средство исправления. Анализатор и средство исправления включены и доступны по умолчанию с .NET 7 RC1. Анализатор разработан, чтобы помочь разработчикам правильно использовать генератор источников. Средство исправления обеспечивает автоматическое преобразование из многих DllImport шаблонов в соответствующую LibraryImport сигнатуру.

Знакомство с собственной библиотекой

Использование исходного генератора LibraryImport означает использование собственной или неуправляемой библиотеки. Собственная библиотека может быть общей библиотекой (т.dll.so. е. или), которая dylibнапрямую вызывает API операционной системы, которая не предоставляется через .NET. Библиотека также может быть той, которая сильно оптимизирована на неуправляемом языке, который разработчик .NET хочет использовать. В этом руководстве вы создадите собственную общую библиотеку, которая предоставляет область API в стиле C. Следующий код представляет определяемый пользователем тип и два API, которые будут использоваться из C#. Эти два API представляют режим "in", но существуют дополнительные режимы для изучения в примере.

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);

Предыдущий код содержит два типа интересов и char32_t*error_data. char32_t* представляет строку, закодированную в UTF-32, которая не является строковой кодировкой, которая исторически маршалирует .NET. error_data — это определяемый пользователем тип, содержащий 32-разрядное целое число, логическое поле C++ и строковое поле UTF-32. Оба этих типа требуют предоставления способа создания исходного генератора кода маршаллинга.

Настройка маршаллинга для встроенного типа

Сначала рассмотрим char32_t* тип, так как маршалирование этого типа требуется определяемым пользователем типом. char32_t* представляет собственную сторону, но вам также требуется представление в управляемом коде. В .NET существует только один тип string. string Таким образом, вы будете маршалировать собственную строку в кодировке UTF-32 и из типа в управляемом коде string . Существует уже несколько встроенных маршаллеров для string типа, который маршалируют как UTF-8, UTF-16, ANSI и даже в качестве типа Windows BSTR . Тем не менее, нет одного для маршалинга, как UTF-32. Это то, что нужно определить.

Тип Utf32StringMarshaller помечается атрибутом CustomMarshaller , который описывает, что он делает с источником генератора. Первый аргумент типа атрибута — string тип, управляемый тип для маршалинга, второй — это режим, указывающий, когда следует использовать маршаллировщик, а третий — Utf32StringMarshallerтип, используемый для маршалинга. Можно применить CustomMarshaller несколько раз, чтобы дополнительно указать режим и тип маршаллера, используемый для этого режима.

В текущем примере показан маршализатор без отслеживания состояния, который принимает некоторые входные данные и возвращает данные в маршалловой форме. Метод Free существует для симметрии с неуправляемой маршалингом, и сборщик мусора является "бесплатной" операцией для управляемого маршаллера. Реализующий может выполнять любые операции, необходимые для маршалирования входных данных в выходные данные, но помните, что состояние не будет явно сохранено генератором источника.

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();
    }
}

Особенности того, как этот конкретный маршаллизатор выполняет преобразование из stringchar32_t* примера, можно найти в примере. Обратите внимание, что можно использовать любые API .NET (например, Encoding.UTF32).

Рассмотрим случай, когда состояние желательно. Обратите внимание на дополнительный CustomMarshaller и обратите внимание на более конкретный режим MarshalMode.ManagedToUnmanagedIn. Этот специализированный маршализатор реализуется как "с отслеживанием состояния" и может хранить состояние в вызове взаимодействия. Дополнительные специализации и разрешения состояния и оптимизация с учетом маршализации для режима. Например, генератор источника может быть указан для предоставления буфера, выделенного стека, который может избежать явного выделения во время маршаллинга. Чтобы указать поддержку выделенного стека буфера, маршаллизатор реализует BufferSize свойство и FromManaged метод, который принимает Spanunmanaged тип. Свойство BufferSize указывает объем пространства стека — длину SpanFromManagedпередаваемого маршалу — маршализатору хотелось бы получить во время вызова маршала.

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();
        }
    }
}

Теперь можно вызвать первую из двух собственных функций с помощью маршаллеров строк UTF-32. В следующем объявлении атрибут используется LibraryImport так же, как DllImportи атрибут, но он зависит MarshalUsing от атрибута, чтобы сообщить исходному генератору, который маршализатор будет использовать при вызове собственной функции. Нет необходимости уточнить, следует ли использовать маршаллировщик без отслеживания состояния или состояние. Это обрабатывается разработчиком, MarshalMode определяющим атрибуты маршаллера CustomMarshaller . Генератор источника выбирает наиболее подходящий маршализатор на основе контекста, в котором MarshalUsing применяется, с MarshalMode.Default тем чтобы быть резервным.

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

Настройка маршаллинга для определяемого пользователем типа

Маршалирование определяемого пользователем типа требует определения не только логики маршалинга, но и типа в C# для маршалирования в /из. Вспомните собственный тип, который мы пытаемся маршалировать.

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

Теперь определите, как это будет выглядеть в C#. Одинаковый int размер в современном C++ и в .NET. Является bool каноническим примером логического значения в .NET. На основе этого Utf32StringMarshallerможно маршалить char32_t* как .NET string. Учитывая стиль .NET, результатом является следующее определение в C#:

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

В соответствии с шаблоном именования назовите маршаллировщик ErrorDataMarshaller. Вместо указания маршаллатора для MarshalMode.Default, вы будете определять только маршаллеров для некоторых режимов. В этом случае, если маршаллизатор используется для режима, который не указан, генератор источника завершится ошибкой. Начните с определения маршала для направления "in". Это "маршализатор без отслеживания состояния", потому что сам маршализатор состоит только из static функций.

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 имитирует форму неуправляемого типа. Преобразование из объекта ErrorData в тривиальныйErrorDataUnmanaged.Utf32StringMarshaller

Маршаллирование объекта int является ненужным, так как его представление идентично в неуправляемом и управляемом коде. bool Двоичное представление значения не определено в .NET, поэтому используйте его текущее значение для определения нулевого и ненулевого значения в неуправляемом типе. Затем повторно используйте маршализатор UTF-32, чтобы преобразовать string поле в .uint*

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

Помните, что вы определяете этот маршаллизатор как "in", поэтому вы должны очистить все выделения, выполненные во время маршаллинга. bool Поля int и не выделили память, но Message это поле. Повторно используйте Utf32StringMarshaller еще раз, чтобы очистить маршаллированную строку.

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

Давайте кратко рассмотрим сценарий "вне". Рассмотрим случай, когда возвращаются один или несколько экземпляров 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, возвращающий один тип экземпляра, не являющийся коллекцией, классифицируется как .MarshalMode.ManagedToUnmanagedOut Как правило, вы используете коллекцию для возврата нескольких элементов, и в этом случае используется коллекция Array . Маршализатор для сценария сбора, соответствующего MarshalMode.ElementOut режиму, возвращает несколько элементов и описывается позже.

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();
        }
    }
}

Преобразование от ErrorDataUnmanagedErrorData к обратному тому, что вы сделали для режима "in". Помните, что также необходимо очистить все выделения, которые ожидается, что неуправляемая среда должна выполняться. Также важно отметить, что функции здесь помечены static и поэтому являются "без отслеживания состояния", то есть без отслеживания состояния является обязательным требованием для всех режимов "Элемент". Вы также заметите, что в режиме "in" есть ConvertToUnmanaged метод. Для всех режимов элемента требуется обработка как в, так и в режиме out.

Для управляемого неуправляемого маршаллера "out", вы собираетесь сделать что-то особенное. Имя типа данных, который вы маршалируете, называется error_data , и .NET обычно выражает ошибки в виде исключений. Некоторые ошибки более затронуты, чем другие, и ошибки, идентифицированные как "неустранимая", обычно указывают на катастрофическую или неустранимую ошибку. Обратите внимание, error_data что поле проверка, если ошибка неустранима. Вы будете маршалированы в управляемый error_data код, и если это неустранимо, вы создадите исключение, а не просто преобразовываете его в ErrorData и возвращаете его.

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();
        }
    }
}

Параметр out преобразуется из неуправляемого контекста в управляемый контекст, поэтому этот метод реализуется ConvertToManaged . Когда неуправляемый вызывающий объект возвращается и предоставляет ErrorDataUnmanaged объект, его можно проверить с помощью ElementOut маршаллера режима и проверка, если он помечен как неустранимая ошибка. Если да, это ваше указание бросать вместо того, чтобы просто вернуться ErrorData.

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

    return data;
}

Возможно, вы не только будете использовать собственную библиотеку, но и хотите поделиться своей работой с сообществом и предоставить библиотеку взаимодействия. Вы можете предоставить ErrorData подразумеваемый маршаллировщик всякий раз, когда он используется в P/Invoke, добавив [NativeMarshalling(typeof(ErrorDataMarshaller))] в ErrorData определение. Теперь любой пользователь, использующий определение этого типа в LibraryImport вызове, получит преимущество ваших маршаллеров. Они всегда могут переопределить маршаллеров с помощью MarshalUsing на сайте использования.

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

См. также