Поделиться через


Общие сведения о взаимодействии управляемого и неуправляемого кода

 

Соня Кесерович, руководитель программы
Дэвид Мортенсон (David Mortenson), ведущий инженер по проектированию программного обеспечения
Адам Натан (Adam Nathan), ведущий инженер по разработке программного обеспечения в области тестирования

Microsoft Corporation

Октябрь 2003 г.

Область применения:
   Microsoft® платформа .NET Framework
   COM-взаимодействие

Сводка: В этой статье приведены основные сведения о взаимодействии между управляемым и неуправляемым кодом, а также рекомендации и распространенные методики по доступу к неуправляемого API и переносу из управляемого кода, а также для предоставления управляемых API неуправляемых вызывающим элементам. Также рассматриваются вопросы безопасности и надежности, данные о производительности и общие методики для процессов разработки. (14 печатных страниц)

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

Содержимое

Общие сведения о взаимодействии
Рекомендации по взаимодействию
Безопасность
Надежность
Производительность
Приложение 1. Пересечение границы взаимодействия
Приложение 2. Ресурсы
Приложение 3. Глоссарий терминов

Общие сведения о взаимодействии

Среда CLR способствует взаимодействию управляемого кода с com-компонентами, службами COM+, API Win32® и другими типами неуправляемого кода. Типы данных, механизмы обработки ошибок, правила создания и уничтожения, а также рекомендации по проектированию зависят от управляемых и неуправляемых объектных моделей. Чтобы упростить взаимодействие между управляемым и неуправляемым кодом и упростить путь миграции, уровень взаимодействия CLR скрывает различия между этими объектными моделями как от клиентов, так и от серверов.

Взаимодействие ("взаимодействие") является двунаправленным, что позволяет:

  • Вызов неуправляемых API из управляемого кода

    Это можно сделать как для неструктурированных API (статических экспортов DLL, таких как API Win32, который предоставляется из библиотек DLL, таких как kernel32.dll и user32.dll), так и для API COM (объектные модели, предоставляемые Microsoft® Word, Excel, Интернет-Обозреватель, объекты данных ActiveX® (ADO) и т. д.).

  • Предоставление управляемых API для неуправляемого кода

    Примерами этого являются создание надстройки для com-приложения, например проигрывателя Windows Media®, или внедрение управляемого элемента управления Windows Forms в форму MFC.

Три дополнительные технологии позволяют использовать управляемые и неуправляемые взаимодействия:

  • Platform Invoke (иногда называется P/Invoke) позволяет вызывать любую функцию на любом неуправляемом языке, если ее подпись повторно объявлена в управляемом исходном коде. Это похоже на функциональные возможности, предоставляемые инструкцией Declare в Visual Basic® 6.0.
  • COM-взаимодействие позволяет вызывать com-компоненты на любом управляемом языке так же, как и обычные управляемые компоненты, и наоборот. COM-взаимодействие состоит из основных служб, предоставляемых средой CLR, а также некоторых средств и API в пространстве имен System.Runtime.InteropServices .
  • Взаимодействие C++ (иногда называемое It Just Works (IJW)) — это функция, предназначенная для C++, которая позволяет напрямую использовать неструктурированные API и COM-API, так как они использовались всегда. Это более мощное, чем COM-взаимодействие, но требует гораздо большей осторожности. Перед использованием этой технологии убедитесь, что вы проверка ресурсы C++.

Рекомендации по взаимодействию

Вызов неуправляемых API из управляемого кода

Существует несколько типов неуправляемых API и несколько типов технологий взаимодействия, доступных для их вызова. Рекомендации по использованию этих технологий описаны в этом разделе. Обратите внимание, что эти предложения являются очень общими и не охватывают все сценарии. Следует тщательно оценить свои сценарии и применить методики разработки и (или) решения, которые имеют смысл для вашего сценария.

Вызов неуправляемых неструктурированных API

Существует два механизма вызова неуправляемых неструктурированных API из управляемого кода: с помощью вызова платформы (доступно на всех управляемых языках) или взаимодействия C++ (доступно в C++).

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

Для вызова всего нескольких неуправляемых методов или для вызова простых неструктурированных API рекомендуется использовать вызов платформы вместо взаимодействия C++. Написание объявлений вызовов платформы для простых неструктурированных API очень просто. Среда CLR позаботится о загрузке библиотеки DLL и маршалинге всех параметров. Даже создание нескольких объявлений вызовов платформы для сложных неструктурированных API незначительно по сравнению с затратами на использование взаимодействия C++ и внедрение совершенно нового модуля, написанного на C++.

Для упаковки сложных неуправляемых неуправляемых неструктурированных API или оболочки неуправляемых неструктурированных API, которые изменяются во время разработки управляемого кода, рекомендуется использовать взаимодействие C++ вместо вызова платформы. Слой C++ может быть очень тонким, а остальная часть управляемого кода может быть написана на любом другом управляемом языке по своему выбору. Использование вызова платформы в этих сценариях потребует много усилий для повторного объявления сложных частей API в управляемом коде и синхронизации их с неуправляемыми API. Использование взаимодействия C++ решает эту проблему, предоставляя прямой доступ к неуправляемым API, что не требует перезаписи, а только включения файла заголовка.

Вызов ИНТЕРФЕЙСОВ COM API

Существует два способа вызова com-компонентов из управляемого кода: через COM-взаимодействие (доступно на всех управляемых языках) или через взаимодействие C++ (доступно в C++).

Для вызова com-компонентов, совместимых с OLE-автоматизацией, рекомендуется использовать COM-взаимодействие. Среда CLR будет выполнять активацию com-компонентов и маршалинг параметров.

Для вызова com-компонентов на основе языка определения интерфейса (IDL) рекомендуется использовать взаимодействие C++. Слой C++ может быть очень тонким, а остальная часть управляемого кода может быть написана на любом управляемом языке. Com-взаимодействие использует сведения из библиотек типов для выполнения правильных вызовов взаимодействия, но библиотеки типов обычно не содержат всю информацию, содержащуюся в IDL-файлах. Использование взаимодействия C++ решает эту проблему, предоставляя прямой доступ к этим API-интерфейсам COM.

Для компаний, владеющих УЖЕ отправленными COM-API, важно рассмотреть возможность доставки основных сборок взаимодействия (PIA) для этих API, что упрощает их использование для управляемых клиентов.

Дерево принятия решений для вызова неуправляемых API

Рис. 1. Дерево принятия решений о вызове неуправляемых API

Предоставление управляемых API неуправляемого кода

Существует два main способа предоставления управляемого API исключительно неуправляемых вызывающих элементов: в виде COM-API или в виде неструктурированного API. Для неуправляемых клиентов C++, которые готовы перекомпилировать свой код с помощью Visual Studio® .NET, существует третий вариант: прямой доступ к управляемым функциям с помощью взаимодействия C++. Рекомендации по использованию этих параметров описаны в этом разделе.

Прямой доступ к управляемому API

Если неуправляемый клиент написан на C++, его можно скомпилировать с помощью компилятора Visual Studio .NET C++ в виде "образа смешанного режима". После этого неуправляемый клиент сможет напрямую обращаться к любому управляемому API. Однако некоторые правила кодирования применяются к доступу к управляемым объектам из неуправляемого кода. проверка документации по C++ для получения дополнительных сведений.

Прямой доступ является предпочтительным вариантом, так как он не требует особого внимания со стороны разработчиков управляемых API. Они могут проектировать свой управляемый API в соответствии с рекомендациями по проектированию управляемых API (DG) и быть уверены, что API по-прежнему будет доступен для неуправляемых вызывающих абонентов.

Предоставление управляемого API в качестве COM-API

Каждый открытый управляемый класс может быть доступен для неуправляемых клиентов с помощью COM-взаимодействия. Этот процесс очень прост в реализации, так как уровень COM-взаимодействия берет на себя всю сантехнику COM. Так, например, каждый управляемый класс реализует IUnknown, IDispatch, ISupportErrorInfo и несколько других стандартных COM-интерфейсов.

Несмотря на то, что предоставление управляемых API в качестве API-интерфейсов COM очень простое, управляемые и объектные модели COM сильно отличаются. Таким образом, предоставление управляемого API для COM всегда должно быть явным конструктивным решением. Некоторые функции, доступные в управляемом мире, не имеют эквивалента в мире COM и не будут использоваться клиентами COM. Из-за этого часто возникает напряженность между рекомендациями по проектированию управляемых API (DG) и совместимостью с COM.

Если com-клиенты важны, напишите управляемый API в соответствии с рекомендациями по проектированию управляемых API, а затем напишите тонкую управляемую оболочку, удобную для COM, вокруг управляемого API, которая будет доступна для COM.

Предоставление управляемого API в виде неструктурированного API

Иногда неуправляемые клиенты не могут использовать COM. Например, они могут быть уже записаны для использования неструктурированных API и не могут быть изменены или перекомпилированы. C++ — это единственный высокоуровневый язык, который позволяет предоставлять управляемые API в виде неструктурированных API. Это не так просто, как предоставление управляемого API в качестве COM API. Это очень сложный метод, который требует расширенных знаний о взаимодействии C++ и различиях между управляемым и неуправляемым мирами.

Предоставляйте управляемый API как неструктурированный API, только если это абсолютно необходимо. Если у вас нет выбора, обязательно проверка документацию по C++ и полностью учитывайте все ограничения.

Дерево принятия решений для предоставления управляемых API

Рис. 2. Предоставление дерева принятия решений управляемых API

Безопасность

Среда CLR поставляется с системой безопасности Code Access Security (CAS), которая регулирует доступ к защищенным ресурсам на основе информации о происхождении сборки. Вызов неуправляемого кода представляет серьезную угрозу безопасности. Без соответствующих проверок безопасности неуправляемый код может управлять любым состоянием любого управляемого приложения в процессе CLR. Кроме того, можно напрямую вызывать ресурсы в неуправляемом коде без каких-либо проверок разрешений CAS. По этой причине любой переход в неуправляемый код рассматривается как операция с высокой степенью защиты и должен включать проверка безопасности. Этот проверка безопасности ищет разрешение на неуправляемый код, требующий, чтобы сборка, содержащая переход неуправляемого кода, а также все сборки, вызывающие его, имели право на фактический вызов неуправляемого кода.

Существуют некоторые ограниченные сценарии взаимодействия, в которых полные проверки безопасности не нужны и неоправданно ограничивают производительность или область компонента. Это происходит в том случае, если ресурс, предоставляемый из неуправляемого кода, не имеет значения безопасности (системное время, координаты окна и т. д.) или ресурс используется только внутри сборки и не предоставляется произвольным вызывающим абонентам. В таких случаях можно отключить полный проверка безопасности для разрешения неуправляемого кода для всех вызывающих соответствующих API. Для этого примените настраиваемый атрибут SuppressUnmanagedCodeSecurity к соответствующему методу или классу взаимодействия. Обратите внимание, что предполагается тщательная проверка безопасности, когда вы определили, что ни в каком частично доверенном коде не может использоваться такие API.

надежность;

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

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

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

Производительность

При каждом переходе с управляемого кода на неуправляемый код (и наоборот) возникают некоторые издержки на производительность. Объем накладных расходов зависит от типов используемых параметров. Уровень взаимодействия CLR использует три уровня оптимизации вызова взаимодействия на основе типа перехода и типов параметров: JIT-встраивание, скомпилированные заглушки сборок и интерпретируемые заглушки маршалинга (в порядке от самого быстрого до самого медленного типа вызова).

Приблизительные издержки для вызова вызова платформы: 10 машинных инструкций (на процессоре x86)

Приблизительные издержки для вызова COM-взаимодействия: 50 инструкций компьютера (на процессоре x86)

Работа, выполненная с помощью этих инструкций, показана в разделах приложения Вызов неструктурированного API: шаг за шагом и Вызов COM API: шаг за шагом. Помимо того, что сборщик мусора не будет блокировать неуправляемые потоки во время вызова, а также обрабатывать соглашения о вызовах и неуправляемые исключения, COM-взаимодействие выполняет дополнительную работу для преобразования вызова текущей вызываемой оболочки среды выполнения (RCW) в указатель интерфейса COM, соответствующий текущему контексту.

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

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

  • Если вы управляете интерфейсом между управляемым и неуправляемым кодом, сделайте его "фрагментарным", а не "болтливым", чтобы уменьшить общее количество выполненных переходов.

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

  • По возможности избегайте преобразований Юникода и ANSI.

    Преобразование строк из Юникода в ANSI и наоборот является дорогостоящей операцией. Например, если необходимо передать строки, но их содержимое не имеет значения, можно объявить строковый параметр как IntPtr , и маршалер взаимодействия не будет выполнять никаких преобразований.

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

    Иногда выполнять маршалинг вручную с помощью методов, доступных в классе Marshal , а не полагаться на маршалинг взаимодействия по умолчанию. Например, если большие массивы строк должны передаваться через границу взаимодействия, но потребуется только несколько элементов, объявление массива как IntPtr и доступ только к этим элементам вручную будет выполняться гораздо быстрее.

  • Используйте InAttribute и OutAttribute с умом, чтобы уменьшить ненужную маршалинг.

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

  • Используйте SetLastError=false для сигнатур вызова платформы только в том случае, если после этого вы вызовете Marshal.GetLastWin32Error .

    Установка SetLastError=true для подписей вызова платформы требует дополнительных работ на уровне взаимодействия, чтобы сохранить код последней ошибки. Используйте эту функцию только в том случае, если вы полагаетесь на эти сведения и будете использовать ее после выполнения вызова.

  • Если и только в том случае, если неуправляемые вызовы предоставляются неуправляемыми способами, используйте SuppressUnmanagedCodeSecurityAttribute , чтобы уменьшить количество проверок безопасности.

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

Приложение 1. Пересечение границы взаимодействия

Вызов неструктурированного API: шаг за шагом

Рис. 3. Вызов неструктурированного API

  1. Получите LoadLibrary и GetProcAddress.
  2. Создайте заглушку DllImport из подписи, содержащей целевой адрес.
  3. Отправка сохраненных регистров вызываемого абонента.
  4. Настройте кадр DllImport и отправьте его в стек кадров.
  5. Если выделена временная память, инициализируйте список очистки для быстрого освобождения после завершения вызова.
  6. Маршалирование параметров. (Это может выделить память.)
  7. Измените режим сборки мусора с совместного на упреждающий, чтобы сборка мусора может произойти в любое время.
  8. Загрузите целевой адрес и вызовите его.
  9. Если задан бит SetLastError , вызовите Метод GetLastError и сохраните результат в абстракции потока, хранящейся в локальном хранилище потока.
  10. Вернитесь в режим совместной сборки мусора.
  11. Если PreserveSig=false и метод вернул ошибку HRESULT, создайте исключение.
  12. Если исключение не было создано, параметры back-propagat out и by-ref .
  13. Восстановите исходное значение указателя расширенного стека, чтобы учесть аргументы, выброщенные вызывающим абонентом.

Вызов COM API: пошаговые инструкции

Рис. Вызов COM API

  1. Создайте управляемую или неуправляемую заглушку из подписи.
  2. Отправка сохраненных регистров вызываемого абонента.
  3. Настройте управляемый и неуправляемый кадр COM-взаимодействия и отправьте его в стек кадров.
  4. Зарезервируйте место для временных данных, используемых во время перехода.
  5. Если выделена временная память, инициализируйте список очистки для быстрого освобождения после завершения вызова.
  6. Снимите флажки исключений с плавающей запятой (только x86).
  7. Маршалирование параметров. (Это может выделить память.)
  8. Получение правильного указателя интерфейса для текущего контекста внутри вызываемой оболочки среды выполнения. Если не удается использовать кэшированный указатель, вызовите QueryInterface в компоненте COM, чтобы получить его.
  9. Измените режим сборки мусора с совместного на упреждающий, чтобы сборка мусора может произойти в любое время.
  10. Из указателя vtable выполните индексирование по номеру слота, получите целевой адрес и вызовите его.
  11. Вызовите release для указателя интерфейса, если ранее был вызван Метод QueryInterface .
  12. Вернитесь в режим совместной сборки мусора.
  13. Если подпись не помечена как PreserveSig, проверка за сбой HRESULT и вызвать исключение (потенциально заполненное сведениями IErrorInfo).
  14. Если исключение не было создано, параметры back-propagat out и by-ref .
  15. Восстановите указатель расширенного стека на исходное значение, чтобы учесть аргументы вызывающего объекта.

Вызов управляемого API из COM: пошаговые инструкции

Рис. 5. Вызов управляемого API из COM

  1. Создайте неуправляемую заглушку из подписи.
  2. Отправка сохраненных регистров вызываемого абонента.
  3. Настройте неуправляемый кадр COM-взаимодействия и отправьте его в стек кадров.
  4. Зарезервируйте место для временных данных, используемых во время перехода.
  5. Измените режим сборки мусора с координативного на упреждающий, чтобы сборка мусора происходила в любое время.
  6. Получите вызываемую оболочку COM (CCW) из указателя интерфейса.
  7. Получите управляемый объект внутри CCW.
  8. При необходимости переведите домены приложений.
  9. Если домен приложения не имеет полного доверия, выполните любые запросы ссылок, которые может иметь метод к целевому домену приложения.
  10. Если выделена временная память, инициализируйте список очистки для быстрого освобождения после завершения вызова.
  11. Маршалирование параметров. (Это может выделить память.)
  12. Найдите метод, управляемый целевым объектом для вызова. (Это включает сопоставление вызовов интерфейса с целевой реализацией.)
  13. Кэшируйте возвращаемое значение. (Если это возвращаемое значение с плавающей запятой, получите его из регистра с плавающей запятой.)
  14. Вернитесь в режим совместной сборки мусора.
  15. Если возникло исключение, извлеките его HRESULT для возврата и вызовите SetErrorInfo.
  16. Если исключение не было создано, параметров обратного распространения ипо ссылке .
  17. Восстановите указатель расширенного стека на исходное значение, чтобы учесть аргументы, которые вызываются.

Приложение 2. Ресурсы

Необходимо прочитать!.NET и COM: полное руководство по взаимодействию Адама Натана

Взаимодействие с неуправляемым кодом, Руководство разработчика Microsoft платформа .NET Framework

Примеры взаимодействия, Microsoft платформа .NET Framework

Блог Адама Натана

Блог Криса Брумма

Приложение 3. Глоссарий терминов

AppDomain (домен приложения) Домен приложения может рассматриваться как упрощенный процесс ОС и управляется средой CLR.
CCW (вызываемая оболочка COM) Особый тип оболочки, созданной слоем взаимодействия CLR для управляемых объектов, активируемых из COM-кода. CCW скрывает различия между управляемыми и COM-объектными моделями, предоставляя маршалинг данных, управление жизненным циклом, управление удостоверениями, обработку ошибок, правильные переходы по квартирам и потокам и т. д. CCW предоставляют функциональные возможности управляемых объектов в удобном для COM способе, не требуя от разработчика управляемого кода знать что-либо о com-сантехнике.
CLR Среда CLR.
COM-взаимодействие Служба, предоставляемая уровнем взаимодействия CLR для использования API COM из управляемого кода или предоставления управляемых API в качестве API-интерфейсов COM для неуправляемых клиентов. COM-взаимодействие доступно на всех управляемых языках.
Взаимодействие C++ Служба, предоставляемая компилятором языка C++ и средой CLR, используется для непосредственного смешивания управляемого и неуправляемого кода в одном исполняемом файле. Взаимодействие C++ обычно включает файлы заголовков в неуправляемые API и соблюдение определенных правил программирования.
Сложный неструктурированный API API с сигнатурами, которые трудно объявить на управляемом языке. Например, методы с параметрами структуры переменной величины трудно объявить, так как в системе управляемых типов нет эквивалентной концепции.
Interop Общий термин, охватывающий любой тип взаимодействия между управляемым и неуправляемным (также называемым "машинным") кодом. Взаимодействие — это одна из многих служб, предоставляемых средой CLR.
Сборка взаимодействия Особый тип управляемой сборки, которая содержит эквиваленты управляемых типов для COM-типов, содержащихся в библиотеке типов. Обычно создается путем запуска средства импорта библиотек типов (Tlbimp.exe) в библиотеке типов.
Управляемый код Код, выполняемый под управлением среды CLR, называется управляемым кодом. Например, любой код, написанный на C# или Visual Basic .NET, является управляемым кодом.
Вызов неуправляемого кода Служба, предоставляемая уровнем взаимодействия CLR для вызова неуправляемых неструктурированных API из управляемого кода. Вызов платформы доступен на всех управляемых языках.
RCW (вызываемый wapper среды выполнения) Особый тип оболочки, созданной слоем взаимодействия CLR вокруг COM-объектов, активируемых из управляемого кода. RcW скрывает различия между управляемыми и COM-объектными моделями, предоставляя маршалинг данных, управление жизненным циклом, управление удостоверениями, обработку ошибок, правильные переходы между подразделениями и потоками и т. д.
Неуправляемый код Код, который выполняется за пределами среды CLR, называется "неуправляемый код". Com-компоненты, компоненты ActiveX и функции API Win32 являются примерами неуправляемого кода.