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:
Używanie jawnej funkcji PInvoke w języku C++ (atrybut DllImport)
Korzystanie z międzyoperacyjności języka C++ (niejawna funkcja PInvoke)
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 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żywaniePtrToStringChars
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ć dodatkowychToPointer
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 user32.dll w rzeczywistości nie ma funkcji MessageBox, ale CharSet=CharSet::Ansi umożliwia funkcji PInvoke korzystanie z 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 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 PInvoke
nie 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 |
---|---|---|---|
UCHWYT | nieważny* | nieważny* | 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 * | Struna^ | String |
LPWSTR | wchar_t * | Ciąg ^ [in], StringBuilder ^ [in, out] | Ciąg ^ [in], StringBuilder ^ [in, out] |
LPCWSTR | const wchar_t * | Struna^ | String |
SPŁAWIK | liczba zmiennoprzecinkowa | liczba zmiennoprzecinkowa | Pojedynczy |
PODWÓJNY | 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.