Chamando funções nativas a partir do código gerenciado

O common language runtime fornece o Platform Invocation Services, ou PInvoke, que permite que o código gerenciado chame funções de estilo C em DLLs (bibliotecas vinculadas dinâmicas) nativas. O mesmo marshaling de dados é usado como para interoperabilidade COM com o runtime e para o mecanismo "It Just Works", ou IJW.

Para saber mais, veja:

Os exemplos nesta seção apenas ilustram como PInvoke pode ser usado. PInvoke pode simplificar o marshaling de dados personalizados porque você fornece informações de marshaling declarativamente em atributos em vez de escrever código de marshaling de procedimento.

Observação

A biblioteca de marshaling fornece uma maneira alternativa e otimizada de realizar marshaling de dados entre ambientes gerenciados e nativos. Para obter mais informações sobre a biblioteca de marshaling, confira Visão geral de marshaling no C++. A biblioteca de marshaling é utilizável somente para dados e não para funções.

PInvoke e o atributo DllImport

O exemplo a seguir mostra o uso de PInvoke em um programa do Visual C++. A função nativa puts é definida em msvcrt.dll. O atributo DllImport é usado para a declaração de 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);
}

O exemplo a seguir é equivalente ao exemplo anterior, mas usa 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);
}

Vantagens do IJW

  • Não é necessário gravar declarações de atributo DLLImport para as APIs não gerenciadas que o programa usa. Basta incluir o arquivo de cabeçalho e o link com a biblioteca de importação.

  • O mecanismo IJW é um pouco mais rápido (por exemplo, os stubs IJW não precisam verificar a necessidade de fixar ou copiar itens de dados porque isso é feito explicitamente pelo desenvolvedor).

  • Ele ilustra claramente problemas de desempenho. Nesse caso, o fato de que você está traduzindo de uma cadeia de caracteres Unicode para uma cadeia de caracteres ANSI e que você tem uma alocação e desalocação de memória concomitantes. Nesse caso, um desenvolvedor que escreve o código usando IJW perceberia que chamar _putws e usar PtrToStringChars seria melhor para o desempenho.

  • Se você chamar muitas APIs não gerenciadas usando os mesmos dados, fazer marshaling uma vez e passar a cópia submetida a marshaling será muito mais eficiente do que fazer o marshaling novamente todas as vezes.

Desvantagens do IJW

  • O marshaling precisa ser especificado explicitamente no código em vez de por atributos (que geralmente têm padrões apropriados).

  • O código de marshaling é embutido, o que o torna mais invasivo no fluxo da lógica do aplicativo.

  • Como as APIs de marshaling explícitas retornam tipos IntPtr para portabilidade de 32 bits a 64 bits, você precisa usar chamadas ToPointer extras.

O método específico exposto pelo C++ é o método mais eficiente e explícito, ao custo de alguma complexidade adicional.

Se o aplicativo usa principalmente tipos de dados não gerenciados ou se ele chama mais APIs não gerenciadas do que APIs do .NET Framework, recomendamos que você use o recurso IJW. Para chamar uma API não gerenciada ocasionalmente em um aplicativo que em sua maior parte é gerenciado, a escolha é mais sutil.

PInvoke com APIs do Windows

PInvoke é prático para chamar funções no Windows.

Neste exemplo, um programa do Visual C++ interopera com a função MessageBox que faz parte da 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);
}

A saída é uma caixa de mensagem que tem o título PInvoke Test e contém o texto Olá, Mundo!.

As informações de marshaling também são usadas pelo PInvoke para pesquisar funções na DLL. Em user32.dll, não há nenhuma função MessageBox, mas CharSet=CharSet::Ansi permite que PInvoke use MessageBoxA, a versão ANSI, em vez de MessageBoxW, que é a versão Unicode. Em geral, recomendamos que você use versões Unicode de APIs não gerenciadas porque isso elimina a sobrecarga de tradução do formato Unicode nativo de objetos de cadeia de caracteres do .NET Framework para ANSI.

Quando não usar PInvoke

O uso de PInvoke não é apropriado para todas as funções de estilo C em DLLs. Por exemplo, suponha que haja uma função MakeSpecial em mylib.dll declarada da seguinte maneira:

char * MakeSpecial(char * pszString);

Se usarmos PInvoke em um aplicativo Visual C++, poderemos escrever algo semelhante ao seguinte:

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

A dificuldade aqui é que não podemos excluir a memória da cadeia de caracteres não gerenciada retornada por MakeSpecial. Outras funções chamadas por meio do PInvoke retornam um ponteiro para um buffer interno que não precisa ser desalocado pelo usuário. Nesse caso, usar o recurso IJW é a escolha óbvia.

Limitações de PInvoke

Você não pode retornar o mesmo ponteiro exato de uma função nativa que você usou como um parâmetro. Se uma função nativa retornar o ponteiro que foi empacotado por PInvoke, isso poderá resultar em corrupção de memória e exceções.

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

O exemplo a seguir exibe esse problema e, embora possa parecer que o programa dá a saída correta, a saída vem da memória que havia sido liberada.

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

Argumentos de marshaling

Com PInvoke, nenhum marshaling é necessário entre tipos primitivos gerenciados e C++ nativos com o mesmo formato. Por exemplo, nenhum marshaling é necessário entre Int32 e int ou entre Double e double.

No entanto, você precisa realizar marshaling de tipos que não têm o mesmo formulário. Isso inclui os tipos char, string e struct. A seguinte tabela mostra os mapeamentos usados pelo marshaler para vários tipos:

wtypes.h Visual C++ Visual C++ with /clr Common Language Runtime
HANDLE void* void* IntPtr, UIntPtr
BYTE unsigned char unsigned char Byte
SHORT short short Int16
WORD unsigned short unsigned short UInt16
INT INT INT Int32
UINT unsigned int unsigned int UInt32
LONG long long Int32
BOOL longo bool Boolean
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 ^ String
LPWSTR wchar_t * String ^ [in], StringBuilder ^ [in, out] String ^ [in], StringBuilder ^ [in, out]
LPCWSTR const wchar_t* String ^ String
FLOAT float float Único
DOUBLE double double Double

O marshaler fixará automaticamente a memória alocada no heap de runtime se o endereço for passado para uma função não gerenciada. A fixação impede que o coletor de lixo mova o bloco de memória alocado durante a compactação.

No exemplo mostrado anteriormente neste tópico, o parâmetro CharSet de DllImport especifica como cadeias de caracteres gerenciadas devem ser submetidas a marshaling; nesse caso, elas devem ser empacotadas em cadeias de caracteres ANSI para o lado nativo.

Você pode especificar informações de marshaling para argumentos individuais de uma função nativa usando o atributo MarshalAs. Há várias opções para marshaling de um argumento String *: BStr, ANSIBStr, TBStr, LPStr, LPWStr e LPTStr. O padrão é LPStr.

Neste exemplo, a cadeia de caracteres é submetida a marshaling como uma cadeia de caracteres Unicode de byte duplo, LPWStr. A saída é a primeira letra de Olá, Mundo! porque o segundo byte da cadeia de caracteres submetida a marshaling é nulo e puts interpreta isso como o marcador de término de cadeia de caracteres.

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

O atributo MarshalAs está no namespace System::Runtime::InteropServices. O atributo pode ser usado com outros tipos de dados, como matrizes.

Conforme mencionado anteriormente no tópico, a biblioteca de marshaling fornece um novo método otimizado de marshaling de dados entre ambientes nativos e gerenciados. Para obter mais informações, confira Visão geral de marshaling no C++.

Considerações sobre desempenho

O PInvoke tem uma sobrecarga entre 10 e 30 instruções x86 por chamada. Além desse custo fixo, o marshaling cria uma sobrecarga adicional. Não há nenhum custo de marshaling entre tipos blittable que têm a mesma representação no código gerenciado e não gerenciado. Por exemplo, não há custo para traduzir entre int e Int32.

Para obter um melhor desempenho, tenha menos chamadas PInvoke e faça com que elas realizem marshaling do máximo de dados possível, em vez de mais chamadas que realizar marshaling de menos dados por chamada.

Confira também

Interoperabilidade entre .NET e nativo