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

Может показаться удивительным, что P/Invoke практически не изменился — если вообще изменился — с момента его появления в .NET 1.1. Разработчики, желающие использовать мощь и свободу низкоуровневого программирования на C#, вынуждены либо использовать статические классы и DllImport атрибуты, либо создавать собственные ветхие решения с делегатами и Marshal.GetDelegateForFunctionPointer(IntPtr ptr, Type delegateType).

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

Чтобы решить большинство — если не все — этих проблем, мы с другом (BlackCentipede) разработали новое решение для нативного взаимодействия в среде CLR — AdvancedDLSupport (или сокращенно ADL).

Библиотека создавалась с учетом трех вещей: гибкости, современности и скорости. Он использует новый подход к привязке к собственному коду, используя знакомые инструменты по-новому. Кроме того, он ориентирован на .NET Standard 2.0, что обеспечивает широкую совместимость с существующими проектами и средами выполнения.

Библиотека доступна бесплатно на Github и Nuget — читайте дальше, чтобы узнать, как вы можете использовать ее для упрощения своей жизни с помощью P/Invoke.

Оглавление

  1. Основное использование
  2. Смешанные занятия
  3. "Под капотом"
  4. Привязка на основе делегата
  5. Непрямые звонки
  6. "Представление"

Основное использование

Возьмем эту простую библиотеку C.

math.h

math.c

В вашем типичном взаимодействии, управляемом DllImport, вы можете объявить такой статический класс и импортировать функции из библиотеки.

Это все хорошо, но теперь вы столкнулись с некоторыми раздражающими ограничениями. Чтобы использовать это на нескольких платформах, вы вынуждены полагаться на специфичную для платформы логику для определения местоположения вашей библиотеки: math.dllв Windows и libmath.soили libmath.dylibв *nix и macOS.

Кроме того, класс является статическим, и его сложно использовать в современных сценариях; из-за своей статической природы класс нельзя передавать, его нельзя создать, он не может наследоваться от какого-либо класса, другие классы не могут наследовать от него и т. д. Наконец, он медленный. DllImport влечет за собой некоторые накладные расходы, которые могут быть болезненно заметны в приложениях с высокой текучестью.

ADL использует другой подход. Вместо объявления класса мы объявляем интерфейс.

Используя этот интерфейс, мы можем затем создать экземпляр типа, который реализует интерфейс и привязывается к собственным функциям. Следует отметить свойство, которое будет привязываться к глобальной переменной — то, чего DllImport вообще не может делать.

Это имеет несколько преимуществ.

  1. Вы полностью контролируете, когда и как загружать и выгружать собственные функции.
  2. Вы решаете, во время выполнения, местоположение или имя вашей библиотеки.
  3. Библиотека теперь является экземпляром и не статична. Вы можете внедрить его в свои типы, создать макеты для модульных тестов, использовать его для общих ограничений и т. д.

Помимо базового использования, подобного этому, ADL поддерживает все типичные шаблоны и механизмы P/Invoke (передача структур по ссылке или по значению, StringBuilder, передача классов и т. д.), описанные в документации.

ADL также предлагает несколько новых функций:

  1. Смешанные классы
  2. Проверка утилизации каждого символа
  3. Встроенная поддержка механизма Mono DllMap.
  4. Лениво загруженные символы
  5. Поддержка T?и ref T?параметров как граждан первого класса
  6. Поддержка привязки к глобальным переменным

Они описаны в Документации по расширенной конфигурации ADL, но классы смешанного режима сами по себе довольно интересны. Давайте взглянем.

Смешанные классы

Помимо простых интерфейсов, ADL также позволяет легко смешивать управляемый и собственный код с помощью классов смешанного режима. Короче говоря, управляемый класс может реализовывать неуправляемый интерфейс, делая собственные методы доступными прямо внутри вашего собственного кода.

Используя ранее описанную нативную библиотеку, мы можем создать класс в том же духе:

Как видите, управляемые функции могут сосуществовать с неуправляемыми, а управляемый код может переопределять реализации неуправляемых функций — как показано в Subtract. Это упрощает создание оболочки объектно-ориентированного управляемого кода или предоставление смешанного доступа к собственному коду. Возможно, вы хотите иметь более подробную проверку ввода или, возможно, хотите изолировать определенные части своего кода.

Чтобы создать класс смешанного режима, просто объявите абстрактный класс, который наследуется от NativeLibraryBase и реализует интерфейс, который вы хотите активировать. Любые члены интерфейса могут иметь явные управляемые реализации или оставаться абстрактными, чтобы их можно было перенаправить к соответствующим собственным реализациям.

Есть несколько ограничений:

  • Класс смешанного режима должен наследоваться от NativeLibraryBase.
  • Классы смешанного режима должны быть абстрактными
  • Свойства могут быть только полностью управляемыми или полностью неуправляемыми — нельзя смешивать геттеры и сеттеры.

Когда у вас есть определение класса, его экземпляры можно создавать почти так же, как и экземпляры интерфейса:

Созданный класс будет наследоваться от базового класса и реализовывать данный собственный интерфейс.

Под капотом

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

По своей сути ADL проверяет интерфейс, который вы ему передаете, и генерирует новый тип во время выполнения, который реализует интерфейс, перенаправляя вызовы методов интерфейса их собственным аналогам.

Прежде всего, он использует собственный метод платформы для загрузки динамических библиотек во время выполнения и поиска в них указателей на неуправляемые функции. В Unix и BSD это означает libdl, а в Windows — LoadLibrary/GetProcAddressметоды из kernel32.

После этого, в зависимости от того, как вы его настроите, есть два основных пути.

Привязка на основе делегата

В C# существуют методы для преобразования указателя неуправляемой функции в делегат — Marshal.GetDelegateForFunctionPointer(IntPtr ptr, Type delegateType). Это лежит в основе подхода ADL, основанного на делегатах, и генерирует соответствующий тип делегата для ваших методов.

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

Это просто, но надежно. Конечно, все это можно сделать вручную, но в итоге получается громоздкий шаблонный код, который нужно написать, а затем поддерживать. Благодаря тому, что ADL генерирует это автоматически, вы значительно экономите время и затраты на техническое обслуживание.

Косвенные звонки

Однако, если вы хотите выжать дополнительную скорость из вашего взаимодействия, ADL также предлагает другой способ привязки — с помощью calliopcode. Это довольно неизвестный код операции в CLR, но он с большим успехом используется крупными проектами, такими как OpenTK и SharpDX, для ускорения их собственного взаимодействия.

calli, в двух словах, напрямую вызывает указатель неуправляемой функции, описанный сайтом вызова, минуя все накладные расходы, связанные с проверкой типов, генерацией делегатов и проверкой кода во время выполнения.

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

Этот способ вызова неуправляемого указателя дает огромные преимущества в скорости, в результате чего скорость от 2 до 8 раз превышает скорость обычных делегатов DllImportor.

Однако он не лишен недостатков. Код с calli по своей сути не поддается проверке и не будет работать в Windows с частичным доверием. Однако политика безопасности по умолчанию заключается в запуске исполняемых файлов с полным доверием, поэтому, если у вас нет ограниченной платформы, это не будет проблемой.

Кроме того, в .NET Core отсутствует способ установить соглашение о неуправляемых вызовах во время выполнения, что приводит к ненадежным результатам при работе в качестве 32-разрядного процесса в Windows. К счастью, это очень редкая конфигурация.

Этот метод обычно недоступен разработчикам — различные компиляторы CLR (C#, F#, VB.NET) никогда не создают код операции calliopcode самостоятельно.

Представление

Итак, мы уже некоторое время говорим о производительности — давайте посмотрим на некоторые цифры. Это тест Matrix2инверсий, выполненный с использованием BenchmarkDotNet, нацеленный на ADL, управляемый код и традиционный DllImport. Тесты проводились в Mono, .NET Core и полной версии .NET Framework (v4.7.1).

Тесты Mono и .NET Core проводились на Linux Mint 18.3 с использованием i7–4790K с 16 ГБ ОЗУ.

Полные тесты FX проводились в Windows 10 с использованием i7–7600K с 16 ГБ ОЗУ.

Каждый тестовый пример выглядит следующим образом:

Managed                       : Managed code, no interop
DllImport                     : Traditional DllImport
Delegates                     : Delegates, with disposal checks
DelegatesWithoutDisposeChecks : Delegates, no disposal checks
calli                         : Using the calli opcode

Моно

.NET Core

.NET FX

На всех платформах calli остается довольно стабильным, в то время как делегаты и DllImport видят некоторые довольно тревожные колебания. Делегаты в Mono кажутся исключением и значительно медленнее, чем другие методы.

Посмотреть исходный код и самостоятельно запустить тест можно здесь: AdvancedDLSupport.Benchmark

ADL доступен бесплатно на Github и Nuget. Для компаний, созданных проектов с открытым исходным кодом или частных лиц, которым нужна нестандартная лицензия, отправьте нам электронное письмо.

Первоначально опубликовано на сайте sharkman.asuscomm.com 13 апреля 2018 г.