Бөлісу құралы:


Руководство: Использование пользовательских маршаллеров в исходно-сгенерированных платформах вызова функций (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();
    }
}

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

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

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", поэтому вы должны очистить все выделения, произведенные во время процесса маршалинга. Поля int и bool не выделили память, но 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();
        }
    }
}

Преобразование из ErrorDataUnmanaged в ErrorData является обратным тому, что вы сделали для режима "in". Помните, что также необходимо очистить все выделения, которые, как ожидается, вы должны выполнить в неуправляемой среде. Также важно отметить, что функции здесь помечены static и поэтому считаются "без сохранения состояния", что является обязательным требованием для всех режимов "Элемент". Вы также заметите, что в режиме «in» существует метод ConvertToUnmanaged. Для всех режимов "Element" требуется обработка как режима "in", так и режима "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 { ... }

См. также