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


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

Среда CLR предоставляет службы вызова платформы или PInvoke, которые позволяют управляемому коду вызывать функции в собственных динамически связанных библиотеках (DLL). Маршалирование данных используется для взаимодействия COM с средой выполнения и механизма IJW.

Дополнительные сведения см. в разделе:

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

Примечание.

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

PInvoke и атрибут DllImport

В следующем примере показано использование PInvoke в программе Visual C++. Собственная функция помещается в msvcrt.dll. Атрибут DllImport используется для объявления puts.

// platform_invocation_services.cpp
// compile with: /clr
using namespace System;
using namespace System::Runtime::InteropServices;

[DllImport("msvcrt", CharSet=CharSet::Ansi)]
extern "C" int puts(String ^);

int main() {
   String ^ pStr = "Hello World!";
   puts(pStr);
}

Следующий пример эквивалентен предыдущему образцу, но использует IJW.

// platform_invocation_services_2.cpp
// compile with: /clr
using namespace System;
using namespace System::Runtime::InteropServices;

#include <stdio.h>

int main() {
   String ^ pStr = "Hello World!";
   char* pChars = (char*)Marshal::StringToHGlobalAnsi(pStr).ToPointer();
   puts(pChars);

   Marshal::FreeHGlobal((IntPtr)pChars);
}

Преимущества IJW

  • Нет необходимости записывать DLLImport объявления атрибутов для неуправляемых API, которые использует программа. Просто добавьте файл заголовка и ссылку на библиотеку импорта.

  • Механизм IJW немного быстрее (например, заглушки IJW не должны проверять необходимость закреплять или копировать элементы данных, так как это делается явным образом разработчиком).

  • Он четко иллюстрирует проблемы с производительностью. В этом случае тот факт, что вы преобразуете из строки Юникода в строку ANSI, и что у вас есть выделение памяти и распределение сделки. В этом случае разработчик, написав код с помощью IJW, понял, что вызовы _putws и использование PtrToStringChars будут лучше для производительности.

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

Недостатки IJW

  • Маршалинг должен быть явно указан в коде вместо атрибутов (которые часто имеют соответствующие значения по умолчанию).

  • Код маршалинга является встроенным, где он более инвазивный в потоке логики приложения.

  • Так как явные типы возвращаемых IntPtr API маршалинга для 32-разрядной 64-разрядной переносимости необходимо использовать дополнительные ToPointer вызовы.

Конкретный метод, предоставляемый C++, является более эффективным, явным методом в стоимости некоторой дополнительной сложности.

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

PInvoke с API Windows

PInvoke удобно для вызова функций в Windows.

В этом примере программа Visual C++ взаимодействует с функцией MessageBox, которая является частью API Win32.

// platform_invocation_services_4.cpp
// compile with: /clr /c
using namespace System;
using namespace System::Runtime::InteropServices;
typedef void* HWND;
[DllImport("user32", CharSet=CharSet::Ansi)]
extern "C" int MessageBox(HWND hWnd, String ^ pText, String ^ pCaption, unsigned int uType);

int main() {
   String ^ pText = "Hello World! ";
   String ^ pCaption = "PInvoke Test";
   MessageBox(0, pText, pCaption, 0);
}

Выходные данные — это поле сообщения с заголовком PInvoke Test и содержащее текст Hello World!.

Сведения о маршалинге также используются PInvoke для поиска функций в библиотеке DLL. В user32.dll на самом деле нет функции MessageBox, но CharSet=CharSet::Ansi позволяет PInvoke использовать MessageBoxA, версию ANSI вместо MessageBoxW, которая является версией Юникода. Как правило, рекомендуется использовать версии неуправляемых API Юникода, так как это устраняет затраты на перевод из собственного формата Юникода платформа .NET Framework строковых объектов в ANSI.

Если не использовать PInvoke

Использование PInvoke не подходит для всех функций в стиле C в библиотеках DLL. Например, предположим, что в mylib.dll объявлена функция MakeSpecial следующим образом:

char * MakeSpecial(char * pszString);

Если мы используем PInvoke в приложении Visual C++, мы можем написать примерно следующее:

[DllImport("mylib")]
extern "C" String * MakeSpecial([MarshalAs(UnmanagedType::LPStr)] String ^);

Сложность заключается в том, что мы не можем удалить память для неуправляемой строки, возвращаемой MakeSpecial. Другие функции, вызываемые через PInvoke, возвращают указатель на внутренний буфер, который не должен быть освобожден пользователем. В этом случае использование функции IJW является очевидным выбором.

Ограничения PInvoke

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

__declspec(dllexport)
char* fstringA(char* param) {
   return param;
}

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

// platform_invocation_services_5.cpp
// compile with: /clr /c
using namespace System;
using namespace System::Runtime::InteropServices;
#include <limits.h>

ref struct MyPInvokeWrap {
public:
   [ DllImport("user32.dll", EntryPoint = "CharLower", CharSet = CharSet::Ansi) ]
   static String^ CharLower([In, Out] String ^);
};

int main() {
   String ^ strout = "AabCc";
   Console::WriteLine(strout);
   strout = MyPInvokeWrap::CharLower(strout);
   Console::WriteLine(strout);
}

Маршалинг аргументов

Без PInvokeмаршалинга не требуется между управляемыми и собственными примитивными типами C++ с одной и той же формой. Например, маршалинг не требуется между Int32 и int или между Double и double.

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

wtypes.h Visual C++ Visual C++ с /clr Среда CLR
HANDLE void* void* IntPtr, UIntPtr
BYTE unsigned char unsigned char Байт
SHORT short short Int16
WORD unsigned short unsigned short UInt16
INT INT INT Int32
UINT unsigned int unsigned int UInt32
LONG длинный длинный Int32
BOOL длинный bool Логический
DWORD unsigned long unsigned long UInt32
ULONG unsigned long unsigned long UInt32
CHAR char char Char
LPSTR char * String ^ [in], StringBuilder ^ [in, out] String ^ [in], StringBuilder ^ [in, out]
LPCSTR const char* String^ Строка
LPWSTR wchar_t * String ^ [in], StringBuilder ^ [in, out] String ^ [in], StringBuilder ^ [in, out]
LPCWSTR const wchar_t* String^ Строка
FLOAT с плавающей запятой с плавающей запятой Одна
DOUBLE двойной точности двойной точности Двойной

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

В примере, приведенном ранее в этом разделе, параметр CharSet dllImport указывает, как следует маршалировать управляемые строки; В этом случае их следует маршалировать в строки ANSI для собственной стороны.

Вы можете указать сведения о маршалинге для отдельных аргументов собственной функции с помощью атрибута MarshalAs. Существует несколько вариантов маршалинг аргумента String * : BStr, ANSIBStr, TBStr, LPStr, LPWStr и LPTStr. Значение по умолчанию — LPStr.

В этом примере строка маршалируется как строка символов Юникода двойного байта, LPWStr. Выходные данные — это первая буква Hello World! Так как второй байт маршалированного строки имеет значение NULL, он интерпретирует его как маркер конца строки.

// platform_invocation_services_3.cpp
// compile with: /clr
using namespace System;
using namespace System::Runtime::InteropServices;

[DllImport("msvcrt", EntryPoint="puts")]
extern "C" int puts([MarshalAs(UnmanagedType::LPWStr)] String ^);

int main() {
   String ^ pStr = "Hello World!";
   puts(pStr);
}

Атрибут MarshalAs находится в пространстве имен System::Runtime::InteropServices. Атрибут можно использовать с другими типами данных, такими как массивы.

Как упоминалось ранее в разделе, библиотека маршалинга предоставляет новый оптимизированный метод маршалинга данных между собственными и управляемыми средами. Дополнительные сведения см. в разделе "Обзор маршалинга" в C++.

Performance Considerations (Приложения-функции Azure. Рекомендации по производительности)

PInvoke имеет затраты от 10 до 30 x86 инструкций для каждого вызова. В дополнение к этой фиксированной стоимости маршалинг создает дополнительные издержки. Без маршалинга между типами, имеющими одинаковое представление в управляемом и неуправляемом коде. Например, нет затрат на перевод между int и Int32.

Для повышения производительности меньше вызовов PInvoke, которые маршалировали как можно больше данных, а не больше вызовов, которые маршалировали меньше данных на вызов.

См. также

Взаимодействие исходного кода и платформы.NET