Llamar a funciones nativas desde código administrado

Common Language Runtime proporciona Platform Invocation Services, o PInvoke, que permite que el código administrado llame a funciones de estilo C en bibliotecas nativas de vínculos dinámicos (DLL). La misma serialización de datos se usa tanto para la interoperabilidad COM con el tiempo de ejecución como para el mecanismo "It Just Works", o IJW.

Para más información, consulte:

Los ejemplos de esta sección ilustran cómo usar PInvoke. PInvoke puede simplificar la serialización de datos personalizados, ya que el usuarios proporciona información de serialización mediante declaración en atributos, en lugar de escribir código de serialización de procedimientos.

Nota:

La biblioteca de serialización es una forma alternativa de serializar datos entre entornos nativos y administrados de forma optimizada. Para más información sobre la biblioteca de serialización, consulte la introducción a la serialización en  C++. La biblioteca de serialización solo se puede usar para datos, no para funciones.

PInvoke y el atributo DllImport

En el ejemplo siguiente se muestra el uso de PInvoke en un programa de Visual C++. La operación puts de la función nativa se define en msvcrt.dll. El atributo DllImport se usa para la declaración 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);
}

El siguiente ejemplo es equivalente al ejemplo anterior, pero 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);
}

Ventaja de IJW

  • No es necesario escribir DLLImport declaraciones de atributo para las API no administradas que usa el programa. Solo tiene que incluir el archivo de encabezado y vincularlo con la biblioteca de importación.

  • El mecanismo IJW es algo más rápido (por ejemplo, los códigos auxiliares de IJW no necesitan comprobar si es necesario anclar o copiar elementos de datos, ya que el desarrollador lo realiza explícitamente).

  • Muestra claramente los problemas de rendimiento. En este caso, el hecho de que va a realizar la traducción de una cadena Unicode a una cadena ANSI y que tiene una asignación y desasignación de memoria de operador. En este caso, un desarrollador que escribe el código mediante IJW se daría cuenta de que llamar a _putws y usar PtrToStringChars mejoraría el rendimiento.

  • Si llama a muchas API no administradas que usan los mismos datos, serializarla una vez y pasar la copia serializada es mucho más eficaz que volver a calcular las referencias cada vez.

Desventajas de IJW

  • La serialización debe especificarse explícitamente en el código, en lugar de por atributos (que a menudo tienen los valores predeterminados adecuados).

  • El código de serialización está insertado, donde es más invasivo en el flujo de la lógica de la aplicación.

  • Como las API de serialización explícitas devuelven IntPtr tipos de portabilidad de 32 bits a 64 bits, debe usar llamadas a ToPointer adicionales.

El método específico expuesto por C++ es el más eficaz y explícito, aunque también es un poco más complejo que el resto.

Si la aplicación usa principalmente tipos de datos no administrados o si llama a más API no administradas que las API de .NET Framework, se recomienda usar la característica IJW. Para llamar a una API no administrada ocasional en una aplicación principalmente administrada, la elección es más sutil.

PInvoke con las API de Windows

PInvoke es cómodo para llamar a funciones en Windows.

En este ejemplo, un programa de Visual C++ interopera con la función MessageBox que forma parte de la 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);
}

La salida es un cuadro de mensaje cuyo título es PInvoke Test y que contiene el texto ¡Hola mundo!.

PInvoke también usa la información de serialización para buscar funciones en el archivo DLL. En user32.dll, no hay de hecho ninguna función MessageBox, pero CharSet=CharSet::Ansi permite que PInvoke use MessageBoxA, la versión ANSI, en lugar de MessageBoxW, que es la versión Unicode. En general, se recomienda usar versiones Unicode de las API no administradas, ya que eso elimina la sobrecarga de la traducción del formato Unicode nativo de objetos de cadena de .NET Framework a ANSI.

En qué situaciones no se debe usar PInvoke

El uso de PInvoke no es adecuado para todas las funciones de estilo C en los archivos DLL. Por ejemplo, supongamos que hay una función MakeSpecial en mylib.dll declarada como se indica a continuación:

char * MakeSpecial(char * pszString);

Si usamos PInvoke en una aplicación de Visual C++, podríamos escribir algo similar al siguiente:

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

La dificultad aquí es que no podemos eliminar la memoria de la cadena no administrada que devuelve MakeSpecial. Otras funciones que se llaman a través de PInvoke devuelven un puntero a un búfer interno que el usuario no tiene que desasignar. En este caso, el uso de la característica IJW es la opción obvia.

Limitaciones de PInvoke

No se puede devolver el mismo puntero exacto de una función nativa que se ha tomado como parámetro. Si una función nativa devuelve el puntero que PInvoke ha serializado, se pueden producir daños en la memoria y excepciones.

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

En el ejemplo siguiente se muestra este problema y, aunque el programa parezca dar la salida correcta, la salida procede de la memoria que se había liberado.

// 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 la serialización

Con PInvoke, no se necesita ninguna serialización entre los tipos primitivos nativos administrados y de C++ con el mismo formato. Por ejemplo, no se requiere ninguna serialización entre Int32 e int, ni entre Double y double.

Sin embargo, debe serializar los tipos que no tienen el mismo formato. Esto incluye los tipos char, string y struct. En la tabla siguiente se muestran las asignaciones usadas por el serializador para varios tipos:

wtypes.h Visual C++ Visual C++ con /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 int sin firma unsigned int UInt32
LONG long long Int32
BOOL long bool Booleano
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 Single
DOUBLE double double Doble

El serializador ancla automáticamente la memoria asignada en el montón del runtime si su dirección se pasa a una función no administrada. El anclado evita que el recolector de elementos no utilizados mueva el bloque de memoria asignado durante la compactación.

En el ejemplo que se ha mostrado anteriormente en este tema, el parámetro CharSet de DllImport especifica cómo se deben serializar las cadenas administradas; en este caso, deben serializarse en cadenas ANSI para el lado nativo.

Puede especificar la información de serialización de argumentos individuales de una función nativa mediante el atributo MarshalAs. Hay varias opciones para serializar cualquier argumento String * : BStr, ANSIBStr, TBStr, LPStr, LPWStr y LPTStr. El valor predeterminado es LPStr.

En este ejemplo, la cadena se serializa como una cadena de caracteres Unicode de doble byte, LPWStr. La salida es la primera letra de ¡Hola mundo!, ya que el segundo byte de la cadena serializado es null y puts lo interpreta como marcador de final de cadena.

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

El atributo MarshalAs está en el espacio de nombres System::Runtime::InteropServices. Este atributo se puede usar con otros tipos de datos, como matrices.

Como ya se ha mencionado en el tema, la biblioteca de serialización proporciona un método nuevo y optimizado de serialización de datos entre entornos nativos y administrados. Para más información, consulte Información general de la serialización en C++.

Consideraciones de rendimiento

PInvoke tiene una sobrecarga de entre 10 y 30 instrucciones de x86 por llamada. Además de este costo fijo, la serialización crea una sobrecarga adicional. No hay ningún costo de serialización entre los tipos que pueden transferirse en bloque de bits que tienen la misma representación en el código administrado y no administrado. Por ejemplo, la traducción entre int e Int32 no tiene ningún costo.

Para mejorar el rendimiento, tiene menos llamadas de PInvoke que serializan tantos datos como sea posible, en lugar de más llamadas que serializan menos datos por llamada.

Consulte también

Interoperabilidad nativa y de .NET