Wywoływanie funkcji natywnych z kodu zarządzanego

Środowisko uruchomieniowe języka wspólnego udostępnia usługi wywołania platformy (PInvoke), które umożliwiają kodowi zarządzanemu wywoływanie funkcji w stylu C w natywnych bibliotekach połączonych dynamicznie (DLL). To samo marshaling danych jest używane jako współdziałanie modelu COM ze środowiskiem uruchomieniowym i mechanizmem "It Just Works" lub IJW.

Aby uzyskać więcej informacji, zobacz:

Przykłady w tej sekcji ilustrują tylko sposób PInvoke użycia. PInvoke może uprościć dostosowywanie marshalingu danych, ponieważ udostępniasz informacje marshalingu deklaratywnie w atrybutach zamiast pisania kodu marshalingu proceduralnego.

Uwaga

Biblioteka marshalingowa zapewnia alternatywny sposób marshalingu danych między środowiskami natywnymi i zarządzanymi w zoptymalizowany sposób. Aby uzyskać więcej informacji na temat biblioteki marshalingowej, zobacz Omówienie marshalingu w języku C++ . Biblioteka marshalingowa może być dostępna tylko dla danych, a nie dla funkcji.

PInvoke i atrybut DllImport

W poniższym przykładzie pokazano użycie elementu PInvoke w programie Visual C++. Funkcja natywna jest definiowana w pliku msvcrt.dll. Atrybut DllImport jest używany do deklaracji put.

// 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);
}

Poniższy przykład jest odpowiednikiem poprzedniego przykładu, ale używa interfejsu 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);
}

Zalety IJW

  • Nie ma potrzeby pisania DLLImport deklaracji atrybutów dla niezarządzanych interfejsów API używanych przez program. Wystarczy dołączyć plik nagłówka i połączyć go z biblioteką importu.

  • Mechanizm IJW jest nieco szybszy (na przykład wycinki IJW nie muszą sprawdzać konieczności przypinania lub kopiowania elementów danych, ponieważ jest to wykonywane jawnie przez dewelopera).

  • Wyraźnie ilustruje problemy z wydajnością. W takim przypadku fakt, że tłumaczysz z ciągu Unicode na ciąg ANSI i że masz alokację pamięci i cofanie przydziału. W takim przypadku deweloper piszący kod przy użyciu interfejsu IJW zdaje sobie sprawę, że wywoływanie _putws i używanie PtrToStringChars będzie lepsze dla wydajności.

  • Jeśli wywołasz wiele niezarządzanych interfejsów API przy użyciu tych samych danych, marshaling go raz i przekazanie marshaled kopii jest znacznie bardziej wydajne niż ponowne przeprowadzanie marshalingu za każdym razem.

Wady IJW

  • Marshaling musi być jawnie określony w kodzie zamiast przez atrybuty (które często mają odpowiednie wartości domyślne).

  • Kod marshalingowy jest wbudowany, gdzie jest bardziej inwazyjny w przepływie logiki aplikacji.

  • Ponieważ jawne interfejsy API marshalingu zwracają IntPtr typy 32-bitowej do 64-bitowej przenośności, należy użyć dodatkowych ToPointer wywołań.

Konkretna metoda uwidoczniona przez język C++ jest bardziej wydajną, jawną metodą kosztem dodatkowej złożoności.

Jeśli aplikacja używa głównie niezarządzanych typów danych lub jeśli wywołuje więcej niezarządzanych interfejsów API niż interfejsy API programu .NET Framework, zalecamy użycie funkcji IJW. Aby wywołać okazjonalny niezarządzany interfejs API w głównie zarządzanej aplikacji, wybór jest bardziej subtelny.

Funkcja PInvoke z interfejsami API systemu Windows

Funkcja PInvoke jest wygodna w przypadku wywoływania funkcji w systemie Windows.

W tym przykładzie program Visual C++ współdziała z funkcją MessageBox, która jest częścią interfejsu 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);
}

Dane wyjściowe to pole komunikatu z tytułem PInvoke Test i zawiera tekst Hello World!.

Informacje dotyczące marshalingu są również używane przez funkcję PInvoke do wyszukiwania funkcji w dll. W pliku user32.dll nie ma w rzeczywistości żadnej funkcji MessageBox, ale CharSet=CharSet::Ansi umożliwia PInvoke używanie usługi MessageBoxA, wersji ANSI, a nie MessageBoxW, która jest wersją Unicode. Ogólnie rzecz biorąc, zalecamy używanie wersji Unicode niezarządzanych interfejsów API, ponieważ eliminuje obciążenie tłumaczenia z natywnego formatu Unicode obiektów ciągów .NET Framework do ANSI.

Kiedy nie należy używać funkcji PInvoke

Używanie funkcji PInvoke nie jest odpowiednie dla wszystkich funkcji w stylu C w bibliotekach DLL. Załóżmy na przykład, że istnieje funkcja MakeSpecial w pliku mylib.dll zadeklarowana w następujący sposób:

char * MakeSpecial(char * pszString);

Jeśli używamy funkcji PInvoke w aplikacji Visual C++, możemy napisać coś podobnego do następującego:

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

Trudność polega na tym, że nie możemy usunąć pamięci dla niezarządzanego ciągu zwróconego przez makeSpecial. Inne funkcje wywoływane za pośrednictwem funkcji PInvoke zwracają wskaźnik do buforu wewnętrznego, który nie musi zostać cofnięty przez użytkownika. W tym przypadku użycie funkcji IJW jest oczywistym wyborem.

Ograniczenia funkcji PInvoke

Nie można zwrócić tego samego dokładnego wskaźnika z funkcji natywnej, która przyjmowała jako parametr. Jeśli funkcja natywna zwraca wskaźnik, który został do niego przesłonięty przez funkcję PInvoke, może wystąpić uszkodzenie pamięci i wyjątki.

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

Poniższy przykład pokazuje ten problem, a mimo że program może wydawać się dać poprawne dane wyjściowe, dane wyjściowe pochodzą z pamięci, która została zwolniona.

// 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);
}

Przeprowadzanie marshalingu argumentów

W przypadku programu PInvokenie jest wymagane przeprowadzanie marshalingu między typami pierwotnymi zarządzanymi i natywnymi języka C++ z tym samym formularzem. Na przykład nie jest wymagane przeprowadzanie marshalingu między int32 a int lub między podwójne i podwójne.

Jednak należy marshalować typy, które nie mają tego samego formularza. Obejmuje to typy znaków, ciągów i struktur. W poniższej tabeli przedstawiono mapowania używane przez marshaler dla różnych typów:

wtypes.h Visual C++ Visual C++ z /clr Środowisko uruchomieniowe języka wspólnego
OBSŁUGI Void* Void* IntPtr, UIntPtr
BYTE unsigned char unsigned char Byte
KRÓTKI short short Int16
WORD unsigned short unsigned short UInt16
INT int int Int32
UINT unsigned int unsigned int UInt32
DŁUGI długi długi Int32
BOOL długi bool Wartość logiczna
DWORD unsigned long unsigned long UInt32
ULONG unsigned long unsigned long UInt32
CHAR char char Char
LPSTR Char* Ciąg ^ [in], StringBuilder ^ [in, out] Ciąg ^ [in], StringBuilder ^ [in, out]
LPCSTR const char * Ciąg^ Ciąg
LPWSTR Wchar_t* Ciąg ^ [in], StringBuilder ^ [in, out] Ciąg ^ [in], StringBuilder ^ [in, out]
LPCWSTR const wchar_t * Ciąg^ Ciąg
FLOAT liczba zmiennoprzecinkowa liczba zmiennoprzecinkowa Pojedynczy
PODWÓJNE double double Liczba rzeczywista

Marshaler automatycznie przypina pamięć przydzieloną na stercie środowiska uruchomieniowego, jeśli jego adres jest przekazywany do funkcji niezarządzanej. Przypinanie uniemożliwia modułowi odśmiecania pamięci przeniesienie przydzielonego bloku pamięci podczas kompaktowania.

W przykładzie przedstawionym wcześniej w tym temacie parametr CharSet dllImport określa sposób marshalingu zarządzanych ciągów; w tym przypadku powinny być marshalowane do ciągów ANSI dla strony natywnej.

Można określić marshaling informacji dla poszczególnych argumentów funkcji natywnej przy użyciu atrybutu MarshalAs. Istnieje kilka opcji marshalingu argumentu String *: BStr, ANSIBStr, TBStr, LPStr, LPWStr i LPTStr. Wartość domyślna to LPStr.

W tym przykładzie ciąg jest marshalingowany jako dwubajtowy ciąg znaków Unicode, LPWStr. Dane wyjściowe to pierwsza litera Hello World! ponieważ drugi bajt marshalowanego ciągu ma wartość null i interpretuje go jako znacznik końca ciągu.

// 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);
}

Atrybut MarshalAs znajduje się w przestrzeni nazw System::Runtime::InteropServices. Atrybut może być używany z innymi typami danych, takimi jak tablice.

Jak wspomniano wcześniej w tym temacie, biblioteka marshalingowa udostępnia nową, zoptymalizowaną metodę marshalingu danych między środowiskami natywnymi i zarządzanymi. Aby uzyskać więcej informacji, zobacz Omówienie marshalingu w języku C++.

Zagadnienia dotyczące wydajności

Funkcja PInvoke ma obciążenie z zakresu od 10 do 30 instrukcji x86 na połączenie. Oprócz tego kosztu stałego marshaling tworzy dodatkowe obciążenie. Nie ma kosztów marshalingu między typami blittable, które mają tę samą reprezentację w kodzie zarządzanym i niezarządzanym. Na przykład nie ma kosztów tłumaczenia między int i Int32.

Aby uzyskać lepszą wydajność, ma mniej wywołań PInvoke, które marshalują jak najwięcej danych, zamiast więcej wywołań, które marshalują mniej danych na wywołanie.

Zobacz też

Współdziałanie natywne i .NET