Volání nativních funkcí ze spravovaného kódu

Modul CLR (Common Language Runtime) poskytuje služby volání platformy nebo PInvoke, které umožňují spravovanému kódu volat funkce ve stylu jazyka C v nativních dynamicky propojených knihovnách (DLL). Stejné zařazování dat se používá jako pro interoperabilitu modelu COM s modulem runtime a pro mechanismus "It Just Works" nebo IJW.

Další informace naleznete v tématu:

Ukázky v této části ukazují, jak PInvoke se dá použít. PInvoke může zjednodušit zařazování přizpůsobených dat, protože informace o zařazování zadáte deklarativním způsobem v atributech místo psaní procedurálního zařazování kódu.

Poznámka:

Knihovna zařazování poskytuje alternativní způsob zařazování dat mezi nativním a spravovaným prostředím optimalizovaným způsobem. Další informace o knihovně zařazování najdete v tématu Přehled zařazování v jazyce C++ . Knihovna zařazování je použitelná jenom pro data a ne pro funkce.

PInvoke a atribut DllImport

Následující příklad ukazuje použití PInvoke v programu Visual C++. Nativní funkce se definuje v msvcrt.dll. Atribut DllImport se používá pro deklaraci 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);
}

Následující ukázka je ekvivalentní předchozí ukázce, ale používá 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);
}

Výhody IJW

  • Pro nespravovaná rozhraní API, která program používá, není nutné zapisovat DLLImport deklarace atributů. Stačí zahrnout soubor záhlaví a odkaz s knihovnou importu.

  • Mechanismus IJW je o něco rychlejší (například zástupné procedury IJW nemusí kontrolovat potřebu připnout nebo kopírovat datové položky, protože to dělá vývojář explicitně).

  • Jasně ilustruje problémy s výkonem. V tomto případě se jedná o skutečnost, že překládáte z řetězce Unicode na řetězec ANSI a že máte přidělení a uvolnění paměti v telefonickém systému. V tomto případě by vývojář, který píše kód pomocí IJW, zjistil, že volání _putws a použití PtrToStringChars by bylo lepší pro výkon.

  • Pokud voláte mnoho nespravovaných rozhraní API pomocí stejných dat, zařazování jedné kopie a předání zařazované kopie je mnohem efektivnější než opakované zařazování pokaždé.

Nevýhody IJW

  • Zařazování musí být explicitně zadáno v kódu místo atributů (které mají často odpovídající výchozí hodnoty).

  • Zařazování kódu je vložené, kde je invaznější v toku aplikační logiky.

  • Vzhledem k tomu, že explicitní zařazování rozhraní API vracejí IntPtr typy pro 32bitovou až 64bitovou přenositelnost, musíte použít další ToPointer volání.

Konkrétní metoda vystavená jazykem C++ je efektivnější, explicitnější metoda za cenu větší složitosti.

Pokud aplikace používá hlavně nespravované datové typy nebo pokud volá více nespravovaných rozhraní API než rozhraní API rozhraní .NET Framework, doporučujeme použít funkci IJW. Pokud chcete volat občasné nespravované rozhraní API ve většinou spravované aplikaci, je volba mnohem jemnější.

PInvoke s rozhraními API systému Windows

PInvoke je vhodný pro volání funkcí ve Windows.

V tomto příkladu spolupracuje program Visual C++ s funkcí MessageBox, která je součástí rozhraní 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);
}

Výstup je pole zprávy s názvem PInvoke Test a obsahuje text Hello World!.

Informace o zařazování také používá PInvoke k vyhledání funkcí v knihovně DLL. Ve user32.dll ve skutečnosti neexistuje žádná funkce MessageBox, ale CharSet=CharSet::Ansi umožňuje PInvoke používat MessageBoxA, verzi ANSI místo MessageBoxW, což je verze Unicode. Obecně doporučujeme používat verze Unicode nespravovaných rozhraní API, protože eliminují režii překladu z nativního formátu Unicode objektů řetězců rozhraní .NET Framework do ANSI.

Kdy použít PInvoke

Použití PInvoke není vhodné pro všechny funkce ve stylu jazyka C v knihovnách DLL. Předpokládejme například, že funkce MakeSpecial v mylib.dll deklarována takto:

char * MakeSpecial(char * pszString);

Pokud používáme PInvoke v aplikaci Visual C++, můžeme napsat něco podobného jako následující:

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

Zde je obtížné odstranit paměť pro nespravovaný řetězec vrácený makeSpecial. Jiné funkce volané prostřednictvím PInvoke vrátí ukazatel na interní vyrovnávací paměť, která nemusí být uvolněna uživatelem. V tomto případě je použití funkce IJW jasnou volbou.

Omezení PInvoke

Nelze vrátit stejný přesný ukazatel z nativní funkce, kterou jste vzali jako parametr. Pokud nativní funkce vrátí ukazatel, který byl do něj zařazen pomocí PInvoke, může dojít k poškození paměti a výjimky.

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

Následující ukázka tento problém vykazuje a i když se může zdát, že program dává správný výstup, výstup pochází z paměti, která byla uvolněna.

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

Zařazování argumentů

Bez PInvokezařazování není potřeba mezi spravovanými a nativními primitivními typy C++ se stejným formulářem. Například mezi int32 a int není vyžadováno zařazování, nebo mezi dvojitou a dvojitou.

Je však nutné zařadovat typy, které nemají stejný formulář. To zahrnuje typy znaků, řetězců a struktur. Následující tabulka ukazuje mapování používaná zařazovačem pro různé typy:

wtypes.h Visual C++ Visual C++ s /clr Modul CLR (Common Language Runtime)
KLIKA prázdnota* prázdnota* IntPtr, UIntPtr
BYTE unsigned char unsigned char Byte
KRÁTKÝ short short Int16
WORD unsigned short unsigned short UInt16
INT int int Int32
UINT unsigned int unsigned int UInt32
DLOUHÝ long long Int32
BOOL long bool Logická hodnota
DWORD unsigned long unsigned long UInt32
ULONG unsigned long unsigned long UInt32
UKLÍZEČKA char char Char
LPSTR uklízečka* Řetězec ^ [in], StringBuilder ^ [in, out] Řetězec ^ [in], StringBuilder ^ [in, out]
LPCSTR const char * Řetězec^ String
LPWSTR wchar_t * Řetězec ^ [in], StringBuilder ^ [in, out] Řetězec ^ [in], StringBuilder ^ [in, out]
LPCWSTR const wchar_t * Řetězec^ String
FLOAT float (číslo s plovoucí řádovou čárkou) float (číslo s plovoucí řádovou čárkou) Jeden
DVOJITÝ double double Hodnota s dvojitou přesností

Zařazovač automaticky připne paměť přidělenou haldě modulu runtime, pokud se její adresa předá nespravované funkci. Připnutí zabraňuje uvolnění paměti v přesunutí přiděleného bloku paměti během komprimace.

V příkladu uvedeném dříve v tomto tématu určuje parametr CharSet knihovny DllImport způsob zařazování spravovaných řetězců; v tomto případě by měly být zařazovány do řetězců ANSI pro nativní stranu.

Informace o zařazování můžete zadat pro jednotlivé argumenty nativní funkce pomocí atributu MarshalAs. Existuje několik možností pro zařazování argumentu String *: BStr, ANSIBStr, TBStr, LPStr, LPWStr a LPTStr. Výchozí hodnota je LPStr.

V tomto příkladu je řetězec zařazován jako dvoubabajtů znakový řetězec Unicode, LPWStr. Výstup je první písmeno Hello World! protože druhý bajt zařazovaného řetězce má hodnotu null a interpretuje ji jako značku konce řetězce.

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

Atribut MarshalAs je v oboru názvů System::Runtime::InteropServices. Atribut lze použít s jinými datovými typy, jako jsou pole.

Jak už bylo zmíněno dříve v tématu, knihovna zařazování poskytuje novou optimalizovanou metodu zařazování dat mezi nativními a spravovanými prostředími. Další informace naleznete v tématu Přehled zařazování v jazyce C++.

Faktory ovlivňující výkon

PInvoke má režii mezi 10 a 30 x86 instrukcemi na volání. Kromě těchto pevných nákladů zařazování vytváří další režii. Neexistují žádné náklady na zařazování mezi typy blittable, které mají stejnou reprezentaci ve spravovaném a nespravovaném kódu. Například neexistují žádné náklady na překlad mezi int a Int32.

Pokud chcete dosáhnout lepšího výkonu, potřebujete méně volání PInvoke, která zařadí co nejvíce dat, místo více volání, která zařadí méně dat na volání.

Viz také

Nativní funkce a vzájemná funkční spolupráce rozhraní .NET