Partilhar via


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

O Common Language Runtime fornece Platform Invocation Services, ou PInvoke, que permite que o código gerenciado chame funções de estilo C em bibliotecas vinculadas dinâmicas (DLLs) nativas. O mesmo mecanismo de organização de dados é usado para a interoperabilidade COM com o ambiente de execução e para o mecanismo "It Just Works", ou IJW.

Para obter mais informações, consulte:

Os exemplos nesta seção apenas ilustram como PInvoke podem ser usados. PInvoke pode simplificar o empacotamento de dados adaptado porque as informações de empacotamento são fornecidas declarativamente em atributos, em vez de se escrever código de empacotamento procedural.

Observação

A biblioteca de marshalling fornece uma maneira alternativa de processar dados entre ambientes nativos e geridos de forma otimizada. Consulte Visão geral do marshaling em C++ para obter mais informações sobre a biblioteca de marshaling. A biblioteca de marshalling é usável apenas 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 Visual C++. A função nativa puts é definida em msvcrt.dll. O atributo DllImport é utilizado na declaração da função 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 há necessidade de escrever DLLImport declarações de atributo para as APIs não gerenciadas que o programa usa. Basta incluir o arquivo de cabeçalho e vincular 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).

  • Ilustra claramente os problemas de desempenho. Nesse caso, o facto de que estás a traduzir de uma string Unicode para uma string ANSI e que tens uma alocação de memória relacionada e desalocação. Nesse caso, um desenvolvedor escrevendo 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 o empacotamento uma vez e passar a cópia empacotada é muito mais eficiente do que refazer o empacotamento todas as vezes.

Desvantagens do IJW

  • O marshaling deve ser especificado explicitamente no código em vez de por meio de atributos (que frequentemente usam padrões adequados).

  • O código de marshaling está embutido, tornando-se mais invasivo no fluxo da lógica da aplicação.

  • Como as APIs de serialização explícita retornam IntPtr tipos, para a portabilidade de 32 bits a 64 bits, deve usar chamadas extras ToPointer.

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

Se o aplicativo usa principalmente tipos de dados não gerenciados ou se 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 gerida de forma esporádica num aplicativo maioritariamente gerido, a escolha torna-se mais subtil.

Utilização de PInvoke com as APIs do Windows

PInvoke é conveniente para chamar funções no Windows.

Neste exemplo, um programa Visual C++ interopera com a função MessageBox que faz parte da API do 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 Hello World!.

As informações de marshaling também são usadas pelo PInvoke para procurar funções na DLL. No user32.dll não há de fato 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 conversão do formato Unicode nativo de objetos de cadeia de caracteres do .NET Framework para ANSI.

Quando não usar PInvoke

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

char * MakeSpecial(char * pszString);

Se usarmos PInvoke em um aplicativo Visual C++, podemos 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 para a cadeia de caracteres não gerenciada retornada por MakeSpecial. Outras funções chamadas através do PInvoke retornam um ponteiro para um buffer interno que não precisa ser desalocado pelo usuário. Neste caso, usar o recurso IJW é a escolha óbvia.

Limitações do PInvoke

Não é possível retornar o mesmo ponteiro exato de uma função nativa que você tomou como parâmetro. Se uma função nativa retornar o ponteiro que foi empacotado para ela pelo PInvoke, corrupção de memória e exceções podem ocorrer.

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

O exemplo a seguir exibe esse problema e, embora o programa possa parecer dar a saída correta, a saída está vindo da memória que foi 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);
}

Organização de Argumentos

Com PInvoke, não é necessário marshaling entre tipos primitivos geridos e nativos C++ com a mesma forma. Por exemplo, nenhum marshaling é necessário entre Int32 e int, ou entre Double e double.

No entanto, deve-se organizar tipos que não têm a mesma forma. Isso inclui os tipos char, string e struct. A tabela a seguir mostra os mapeamentos usados pelo empacotador para vários tipos:

wtypes.h Visual C++ Visual C++ com /clr Tempo de execução de linguagem comum
PEGA Vazio * Vazio * IntPtr, UIntPtr
byte char não assinado char não assinado byte
CURTAS curto curto Int16
PALAVRA curta não assinada curta não assinada UInt16
INT Int Int Int32
UINT int não assinado int não assinado UInt32
LONGO longo longo Int32
Boleano longo Bool Booleano
Tipo de dados DWORD longo não assinado longo não assinado UInt32
ULONG longo não assinado longo não assinado UInt32
CHAR char char Char
LPSTR char * String ^ [in], StringBuilder ^ [in, out] String ^ [in], StringBuilder ^ [in, out]
LPCSTR const char * Cadeia ^ Corda
LPWSTR wchar_t * String ^ [in], StringBuilder ^ [in, out] String ^ [in], StringBuilder ^ [in, out]
LPCWSTR const wchar_t * Cadeia ^ Corda
FLUTUAR flutuante flutuante Solteiro
DUPLO duplo duplo Duplo

O marshaler fixa automaticamente a memória alocada na heap do runtime caso o seu endereço seja passado para uma função não gerida. 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 Strings geridas devem ser convertedas; neste caso, devem ser convertidas em strings ANSI para o lado nativo.

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

Neste exemplo, a cadeia de caracteres é empacotada como uma cadeia de caracteres Unicode de byte duplo, LPWStr. A saída é a primeira letra de Hello World! porque o segundo byte da cadeia de caracteres empacotada é nulo, e puts interpreta essa condição como o marcador de fim 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.

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

Considerações sobre desempenho

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

Para um melhor desempenho, faça menos chamadas PInvoke que transfiram o máximo de dados possível, em vez de mais chamadas que transferem menos dados por chamada.

Ver também

Interoperabilidade nativa e .NET