Обзор взаимодействия управляемого и неуправляемого кода
Соня Кесерович, руководитель программы
Дэвид Мортенсон, ведущий инженер по проектированию программного обеспечения
Адам Натан, ведущий инженер по проектированию программного обеспечения в тесте
Корпорация Майкрософт
Октябрь 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) и COM-API (такие объектные модели, как предоставляемые Microsoft® Word, Excel, Internet Explorer, Объекты данных 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-ИНТЕРФЕЙСы, так как они всегда использовались. Это более мощный, чем 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- интерфейсам, для которых не требуется перезапись, просто включение файла заголовка.
Вызов API-интерфейсов COM
Существует два способа вызова COM-компонентов из управляемого кода: через COM-взаимодействие (доступно на всех управляемых языках) или через взаимодействие C++ (доступно в C++).
Для вызова компонентов COM, совместимых с OLE Automation, рекомендуется использовать COM-взаимодействие. Среда CLR будет заботиться о активации компонента COM и маршалинге параметров.
Для вызова COM-компонентов на основе языка определения интерфейса (IDL) рекомендуется использовать взаимодействие C++. Слой C++ может быть очень тонким, и остальная часть управляемого кода может быть написана на любом управляемом языке. Взаимодействие COM использует сведения из библиотек типов для правильного вызова взаимодействия, но библиотеки типов обычно не содержат все сведения, присутствующих в файлах IDL. С помощью взаимодействия C++ эта проблема решается путем прямого доступа к этим COM-API.
Для компаний, использующих API-интерфейсы COM, которые уже были отправлены, важно рассмотреть вопрос о доставке основных сборок взаимодействия (PIA) для этих API, что упрощает их использование для управляемых клиентов.
Дерево принятия решений для вызова неуправляемых API
Рис. 1. Вызов дерева принятия решений неуправляемых API
Предоставление управляемых API неуправляемого кода
Существует два основных способа предоставления управляемого 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 в виде 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 поставляется с системой безопасности, безопасность доступа к коду (CAS), которая регулирует доступ к защищенным ресурсам на основе сведений о происхождении сборки. Вызов неуправляемого кода представляет собой серьезный риск безопасности. Без соответствующих проверок безопасности неуправляемый код может управлять любым состоянием любого управляемого приложения в процессе CLR. Кроме того, можно вызывать ресурсы в неуправляемом коде напрямую, без каких-либо проверок разрешений CAS. По этой причине любой переход в неуправляемый код считается высокозащищенной операцией и должен включать проверку безопасности. Эта проверка безопасности ищет неуправляемые разрешения кода, для которых требуется сборка, содержащая неуправляемый переход кода, а также все сборки, вызываемые в него, чтобы иметь право на фактический вызов неуправляемого кода.
Существуют некоторые ограниченные сценарии взаимодействия, в которых полные проверки безопасности являются ненужными и будут неоправданно ограничивать производительность или область действия компонента. Это происходит, если ресурс, предоставляемый из неуправляемого кода, не имеет релевантности безопасности (системное время, координаты окна и т. д.), или ресурс используется только внутри сборки и не предоставляется общедоступным для произвольных вызывающих объектов. В таких случаях можно отключить полную проверку безопасности для неуправляемого разрешения на код для всех вызывающих интерфейсов API. Это можно сделать, применяя SuppressUnmanagedCodeSecurity настраиваемый атрибут к соответствующему методу взаимодействия или классу. Обратите внимание, что это предполагает тщательную проверку безопасности, в которой вы определили, что частично доверенный код не может использовать такие API.
Надёжность
Управляемый код предназначен для повышения надежности и надежности, чем неуправляемый код. Одним из примеров функции CLR, которая способствует этим качествам, является сборка мусора, которая заботится о освобождении неиспользуемой памяти, чтобы предотвратить утечку памяти. Другим примером является безопасность управляемых типов, которая используется для предотвращения ошибок переполнения буфера и других ошибок, связанных с типом.
При использовании любой технологии взаимодействия код может быть не столь надежным или надежным, как чистый управляемый код. Например, вам может потребоваться выделить неуправляемую память вручную и помнить, чтобы освободить ее при завершении работы.
Написание любого нетривиального кода взаимодействия требует того же внимания к надежности и надежности, что и написание неуправляемого кода. Даже если весь код взаимодействия написан правильно, система будет как надежная, так и неуправляемые части.
Производительность
При каждом переходе из управляемого кода в неуправляемый код (и наоборот) есть некоторые затраты на производительность. Объем затрат зависит от типов используемых параметров. Уровень взаимодействия CLR использует три уровня оптимизации вызовов взаимодействия на основе типов переходов и типов параметров: JIT- встраивание, скомпилированные заглушки сборки и интерпретированные заглушки маршалинга (в порядке наиболее быстрого до самого медленного типа вызова).
Приблизительные затраты на вызов платформы: 10 инструкций на компьютере (на процессоре x86)
Приблизительные затраты на вызов com-взаимодействия: 50 инструкций на компьютере (на процессоре x86)
Работа, выполняемая этими инструкциями, показана в разделах приложения вызовов неструктурированного API: пошаговые инструкции и вызов COM-API: пошаговые инструкции. Кроме того, чтобы сборщик мусора не блокировал неуправляемые потоки во время вызова, а также обрабатывал соглашения о вызовах и неуправляемых исключениях, COM-взаимодействие выполняет дополнительную работу для преобразования вызова вызываемого оболочки среды выполнения (RCW) в указатель интерфейса COM, соответствующий текущему контексту.
Каждый вызов взаимодействия вводит некоторые издержки. В зависимости от частоты этих вызовов и важности работы, происходящих внутри реализации метода, затраты на вызовы могут отличаться от незначительных до очень заметных.
На основе этих рекомендаций в следующем списке приведены некоторые общие рекомендации по производительности, которые могут оказаться полезными.
Если вы управляете интерфейсом между управляемым и неуправляемным кодом, сделайте его "фрагментарным" вместо "чата", чтобы уменьшить общее количество переходов.
Чатые интерфейсы — это интерфейсы, которые делают много переходов без выполнения каких-либо значительных действий на другой стороне границы взаимодействия. Например, методы задания свойств и методы получения являются чатыми. Фрагментные интерфейсы — это интерфейсы, которые выполняют только несколько переходов, и объем работы, выполняемой на другой стороне границы, является значительным. Например, метод, который открывает подключение к базе данных и извлекает некоторые данные, является кускими. Блоковые интерфейсы включают меньше переходов взаимодействия, поэтому вы устраняете некоторые затраты на производительность.
Если это возможно, избегайте преобразований Юникода или ANSI.
Преобразование строк из Юникода в ANSI и наоборот является дорогостоящей операцией. Например, если строки необходимо передать, но их содержимое не имеет значения, можно объявить строковый параметр как intPtr, а маршалер взаимодействия не будет выполнять никаких преобразований.
В сценариях высокой производительности объявление параметров и полей как IntPtr может повысить производительность, хотя и за счет удобства использования и удобства обслуживания.
Иногда можно быстрее выполнять маршалинг вручную с помощью методов, доступных в классе маршал маршал, а не полагаться на маршалинг взаимодействия по умолчанию. Например, если большие массивы строк должны передаваться через границу взаимодействия, но потребуется только несколько элементов, объявляя массив как IntPtr и доступ только к этим немногим элементам вручную будет гораздо быстрее.
Используйте InAttribute и OutAttribute мудро, чтобы уменьшить ненужные маршалинги.
Маршалер взаимодействия использует правила по умолчанию при определении необходимости маршалирования определенного параметра перед вызовом и маршалированием после вызова. Эти правила основаны на уровне косвенного и типа параметра. Некоторые из этих операций могут не потребоваться в зависимости от семантики метода.
Используйте SetLastError=false на платформе, вызывая подписи, только если вы вызовете Marshal.GetLastWin32Error после этого.
Установка SetLastError=true при вызове подписей платформы требует дополнительной работы из слоя взаимодействия для сохранения последнего кода ошибки. Используйте эту функцию, только если вы используете эту информацию и будете использовать ее после вызова.
Если и только в том случае, если неуправляемые вызовы предоставляются в неиспользоваемом режиме, используйте SuppressUnmanagedCodeSecurityAttribute, чтобы уменьшить количество проверок безопасности.
Проверки безопасности очень важны. Если API не предоставляет защищенные ресурсы или конфиденциальную информацию, или они хорошо защищены, обширные проверки безопасности могут привести к ненужным издержкам. Однако стоимость не выполняет никаких проверок безопасности очень высока.
Приложение 1. Пересечение границы взаимодействия
Вызов неструктурированного API: пошаговые действия
Рис. 3. Вызов неструктурированного API
- Получение LoadLibrary и GetProcAddress.
- Создайте заглушку dllImport dllImport из подписи, содержащей целевой адрес.
- Отправка сохраненных регистров вызываемого абонента.
- Настройте кадр DllImport и отправьте его в стек кадров.
- Если выделена временная память, инициализировать список очистки для быстрого освобождения при завершении вызова.
- Параметры маршала. (Это может выделить память.)
- Измените режим сборки мусора из кооператива на упреждающий, поэтому сборка мусора может происходить в любое время.
- Загрузите целевой адрес и вызовите его.
- Если задан SetLastError бит, вызовите GetLastError и сохраните результат абстракции потока, хранящейся в локальном хранилище потоков.
- Вернитесь в режим совместной сборки мусора.
- Если PreserveSig=false и метод вернул ошибку HRESULT, вызовите исключение.
- Если исключение не было создано, обратное распространение и параметров по ссылке.
- Восстановите указатель расширенного стека на исходное значение, чтобы учитывать аргументы вызывающего пользователя.
Вызов COM-API. Пошаговые действия
Рис. 4. Вызов COM-API
- Создайте управляемую неуправляемую заглушку из подписи.
- Отправка сохраненных регистров вызываемого абонента.
- Настройте управляемый к неуправляемый кадр ВЗАИМОДЕЙСТВИЯ COM и отправьте его в стек кадров.
- Резервировать место для временных данных, используемых во время перехода.
- Если выделена временная память, инициализировать список очистки для быстрого освобождения при завершении вызова.
- Очистить флаги исключений с плавающей запятой (только x86).
- Параметры маршала. (Это может выделить память.)
- Извлеките правильный указатель интерфейса для текущего контекста внутри вызываемого оболочки среды выполнения. Если кэшированный указатель нельзя использовать, вызовите QueryInterface в компоненте COM, чтобы получить его.
- Измените режим сборки мусора из кооператива на упреждающий, поэтому сборка мусора может происходить в любое время.
- Из указателя vtable индекс по номеру слота, получите целевой адрес и вызовите его.
- Вызов выпуска на указатель интерфейса, если QueryInterface был вызван ранее.
- Вернитесь в режим совместной сборки мусора.
- Если подпись не помечена PreserveSig, проверьте наличие сбоя HRESULT и создайте исключение (потенциально заполнено сведениями об IErrorInfo).
- Если исключение не было создано, обратное распространение и параметров по ссылке.
- Восстановите указатель расширенного стека на исходное значение, чтобы учесть аргументы вызывающего пользователя.
Вызов управляемого API из COM: пошаговые действия
Рис. 5. Вызов управляемого API из COM
- Создайте неуправляемую заглушку из подписи.
- Отправка сохраненных регистров вызываемого абонента.
- Настройте неуправляемый кадр ВЗАИМОДЕЙСТВИЯ COM и отправьте его в стек кадров.
- Резервировать место для временных данных, используемых во время перехода.
- Измените режим сборки мусора из кооператива на предупреждающий, чтобы сборка мусора может происходить в любое время.
- Получите вызываемую оболочку COM (CCW) из указателя интерфейса.
- Извлеките управляемый объект внутри CCW.
- При необходимости переходить домены приложений.
- Если домен приложения не имеет полного доверия, выполните любые требования ссылки, которые метод может иметь к целевому домену приложения.
- Если выделена временная память, инициализировать список очистки для быстрого освобождения при завершении вызова.
- Параметры маршала. (Это может выделить память.)
- Найдите управляемый целевым методом для вызова. (Это включает вызовы интерфейса сопоставления в целевую реализацию.)
- Кэшируйте возвращаемое значение. (Если это возвращаемое значение с плавающей запятой, получите его из регистра с плавающей запятой.)
- Вернитесь в режим совместной сборки мусора.
- Если исключение было создано, извлеките его HRESULT для возврата и вызовите SetErrorInfo.
- Если исключение не было создано, обратное распространение и параметров по ссылке.
- Восстановите указатель расширенного стека на исходное значение, чтобы учесть аргументы вызывающего пользователя.
Приложение 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 с сигнатурами, которые трудно объявить на управляемом языке. Например, методы с параметрами структуры переменной размера трудно объявить, так как в системе управляемых типов нет эквивалентной концепции. |
взаимодействие | Общий термин, охватывающий любой тип взаимодействия между управляемым и неуправляемным (также называемым "машинным") кодом. Взаимодействие — это одна из многих служб, предоставляемых средой CLR. |
сборка взаимодействия | Специальный тип управляемой сборки, содержащей эквиваленты управляемого типа для типов COM, содержащихся в библиотеке типов. Обычно создается с помощью средства импорта библиотеки типов (Tlbimp.exe) в библиотеке типов. |
управляемый код | Код, выполняемый под контролем среды CLR, называется управляемым кодом. Например, любой код, написанный на C# или Visual Basic .NET, является управляемым кодом. |
Вызов платформы | Служба, предоставляемая уровнем взаимодействия CLR для вызова неуправляемых неуправляемых API из управляемого кода. Вызов платформы доступен на всех управляемых языках. |
RCW (вызываемый wapper среды выполнения) | Особый вид оболочки, созданной слоем взаимодействия CLR вокруг объектов COM, активируемых из управляемого кода. RcW скрывает различия между управляемыми и COM-объектными моделями, предоставляя маршалинг данных, управление временем существования, управление удостоверениями, обработку ошибок, правильные переходы квартиры и потоков и т. д. |
неуправляемый код | Код, который выполняется вне среды CLR, называется "неуправляемый код". Com-компоненты, компоненты ActiveX и функции API Win32 являются примерами неуправляемого кода. |