¡Acceso ilimitado 24/7 a todos nuestros libros y vídeos! Descubra la Biblioteca Online ENI. Pulse aquí
¡Acceso ilimitado 24/7 a todos nuestros libros y vídeos! Descubra la Biblioteca Online ENI. Pulse aquí
  1. Libros
  2. Aprender la programación orientada a objetos con el lenguaje C#
  3. P
Extrait - Aprender la programación orientada a objetos con el lenguaje C#
Extractos del libro
Aprender la programación orientada a objetos con el lenguaje C# Volver a la página de compra del libro

P-Invoke

Introducción

Este capítulo se dirige a los desarrolladores C/C++.

Pasar a la programación orientada a objetos no es sencillo para los desarrolladores, y tampoco para los directores de las empresas, que imaginan sus inversiones en software en código nativo reducidas a la nada.

El framework .NET permite evitar eliminar completamente el pasado, permitiendo intercambios entre su mundo, conocido como "gestionado" y el exterior: el mundo no gestionado en código nativo. En consecuencia, pasar a la programación orientada a objetos se podrá hacer de manera progresiva y, por tanto, será menos traumática para todos. Las DLL realizadas en C/C++ con su correspondiente esfuerzo siempre se podrán utilizar en un esquema de programación orientada a objetos.

Sin embargo, preste atención al hecho de que durante la ejecución del código nativo desde el framework .NET se podrá corto-circuitar la seguridad. Las funciones no gestionadas podrán acceder directamente a los recursos del sistema operativo.

1. Recordatorio sobre las DLL no gestionadas

Una DLL no gestionada es un archivo que contiene código repartido en funciones. En la mayoría de casos, estas funciones son accesibles desde el exterior. Por tanto, hablamos de funciones "exportadas". A diferencia de lo que sucede con  un archivo .EXE, es imposible ejecutar directamente el código...

El caso sencillo

El primer ejemplo (proyecto PInvoke01 para descargar), muestra el caso más sencillo. El objeto C# llama a una función C/C++ que no espera ningún argumento y que no devuelve nada; por tanto, no habrá problemas de conversión de tipos entre los dos mundos. Sin embargo, este caso "sencillo" va a mostrar cómo cargar una DLL no gestionada, cómo encontrar la entrada de la función expuesta y después cómo llamarla.

1. Declaración y llamada

Por parte de la DLL, la función C llamada FuncionNoGestionada se debe declarar como "expuesta". En el entorno de desarrollo de Microsoft y para la mayor parte de los compiladores GCC debe estar precedida por __declspec(dllexport) y declararse en el archivo .DEF del proyecto.


#include "stdafx.h"  
#include "Dll_C.h"  
  
  
__declspec(dllexport) void FuncionNoGestionada(void)  
{  
    MessageBeep(MB_ICONASTERISK);  
}
 


LIBRARY  
EXPORTS  
  
FuncionNoGestionada
 

Durante una llamada a una función, se utiliza la pila para preguntar los argumentos y guardar la dirección de retorno. La manera en que se gestiona la pila se llama convención de llamada. Hay varias técnicas y una mala adecuación entre el código que invoca y el código invocado provoca funcionamientos...

Llamada con argumentos y retorno de función

Este segundo ejemplo (proyecto PInvoke02 para descargar) muestra:

  • una transmisión de argumentos de tipos clásicos entre un objeto C# y una función C/C++,

  • la recuperación del resultado de la función C/C++ en el objeto C#.

La función C/C++ devuelve la suma de dos enteros que se pasan como argumentos.

P-Invoke tiene automáticamente en cuenta el tipo int, y también lo hace su brazo derecho, el Marshal; por lo tanto, por el momento, no hay nada demasiado complicado.

Por parte de la DLL, a continuación se muestra la función C/C++:


#include "stdafx.h"  
#include "Dll_C.h"  
  
  
__declspec(dllexport) int Sumar(int x, int y )  
{  
    return x + y ;  
}
 

Piense siempre en declarar la función en el archivo de definición (.DEF):


LIBRARY  
EXPORTS  
  
Sumar
 

Por parte de C#, a continuación se muestra la declaración y la llamada a la función C/C++:


using System;  
using System.Runtime.InteropServices;  
  
namespace ConsoleApp  
{  
    class Program  
    {  
        [DllImport("Dll_C.dll", EntryPoint = "Sumar",  
CallingConvention = CallingConvention.Cdecl)]  ...

Tratamiento con las cadenas de caracteres

1. Codificación de los caracteres

Los problemas empiezan con la operación con caracteres. Como se ha recordado con anterioridad, la codificación de los caracteres puede utilizar formatos muy diferentes. El tipo char del C/C++ codifica el carácter con un byte (8 bits, es decir 256 combinaciones). Esta codificación minimalista que deja, entre otros, los alfabetos asiáticos, griegos y rusos en el olvido, cada vez más se sustituye por el carácter extendido wchar_t, que codifica cada carácter con dos bytes (16 bits, es decir 65.536 combinaciones). En C/C++ es posible elegir el formato del carácter definitivamente por su tipo (char o wchar_t) o utilizando la macro TCHAR con una opción de compilación. De hecho, la macro TCHAR hace que el tipo carácter sea adaptable. En función de la directiva seleccionada, cada TCHAR se sustituye en la compilación por un char o por un wchat_t. La directiva en cuestión se ajusta en la Página de propiedades del proyecto C/C++, pestaña General, opción Character Set.

images/RI09_12.png

En .NET, el System.Char de manera nativa está en UNICODE, por lo tanto muy cerca del wchar_t del C/C++.

No hay que confundir el carácter C#, que es un alias de System.Char y que contiene el UNICODE, con el carácter del C/C++ que contiene el ANSI.

2. Codificación de las cadenas

En C/C++...

Intercambio de tablas

1. De C# a C/C++

En C/C++, una tabla es un puntero a una zona de memoria que contiene un número definido de "casillas". Estas casillas están tipadas y su número se fija durante la creación de la tabla. Tenemos tablas de bytes, int, char, etc. En C#, la tabla es un objeto que también contiene una sucesión de datos, con un juego de métodos y atributos para explotarlos.

El siguiente ejemplo (proyecto PInvoke04 para descargar) va a mostrar cómo una tabla puede pasar de C# a C/C++.

Por parte de C/C++ hay una función que recupera una tabla y muestra su contenido en la consola. La tabla se declara por un puntero tipado. El tamaño dinámico se pasa como segundo argumento, lo que permite realizar una iteración correcta de la tabla.


#include "stdafx.h"  
#include <stdio.h>  
#include "Dll_C.h"  
  
__declspec(dllexport) void MostrarTabla(const int* pTab, size_t 
iTamanio)  
{  
    int* p = pTab;  
    for (size_t i = 0; i < iTamanio; i++)  
    {  
        printf("%d\r\n", *p++);  
    }  
}
 

Por parte de C#, la función C se declara con un primer argumento de tipo tabla precedido por la directiva [In]. Esta directiva [In] es el acceso directo...

Compartición de estructuras

Recordemos que el tipo structure reúne varios campos que pueden tener tipos diferentes. El tipo structure se utiliza mucho en los intercambios entre C# y C/C++, porque evita que las funciones tengan demasiados argumentos y parezca un conjunto de información de características del objeto a tratar.

El siguiente ejemplo (proyecto PInvoke05 para descargar) muestra la transmisión de una estructura que representa un cliente por su nombre, apellido, edad y número de teléfono.

1. Declaración de las estructuras

Por parte de C/C++:


#pragma pack(push, AppPack)  
#pragma pack(1)  
  
struct Cliente  
{  
    bool activo;  
    char nombre[25];  
    char apellido[25];  
    BYTE edad;  
    char telefono[20];  
    int cuentaCliente;  
};  
  
#pragma pack(pop, AppPack) 
 

Cada campo "texto" de la estructura se almacena en una tabla de caracteres de tamaño predefinido. Una directiva de "packing" permite fijar la alineación de los campos en el byte. En función de las optimizaciones del compilador C/C++, los campos de las estructuras no son necesariamente contiguos en memoria. El compilador puede insertar bytes entre ellos para que el microprocesador acceda más rápidamente, porque se optimizó para realizar "pasos" de ocho bytes, por ejemplo. Estos bytes "intermedios" añadidos por el compilador, no contienen datos, pero en el caso de un intercambio con C# provocarán desplazamientos en memoria.

Por este motivo se utiliza la directiva C/C++ de packing, para fijar la alineación independientemente de las optimizaciones y del tipo de compilación de la DLL. En nuestro ejemplo, la alineación se hace en el byte.

Por parte de C#:


[StructLayout(LayoutKind.Sequential, Pack = 1)]  
public struct Cliente  
{  
    [MarshalAs(UnManagedType.I1)]  
    public bool activo;  
    [MarshalAs(UnManagedType.ByValArray, SizeConst...

Las directivas [In] y [Out]

En estos ejemplos hemos utilizado las directivas [In] y [Out] que simplifican de manera considerable la tarea, evitando codificación:

  • Para [In]:

  • Asignar una zona de intercambio.

  • Copia con conversión "marshal" de la variable C# en la zona de intercambio, accesible por C/C++.

Para [Out]:

  • Copia con conversión "marshal" de la variable C/C++, de la zona de intercambio a una variable C#.

  • Para los dos:

  • Desasignación de la zona de intercambio.

A continuación se muestra lo que sería la parte C# del último ejercicio (proyecto PInvoke07 para descargar), sin las directivas [In] y [Out]:


[DllImport("Dll_C.dll", EntryPoint = "RegistrarCliente",  
CallingConvention = CallingConvention.Cdecl)]   
static extern void RegistrarCliente(IntPtr cliente);  
  
[StructLayout(LayoutKind.Sequential, Pack =1)]  
public struct Cliente  
{  
    [MarshalAs(UnManagedType.I1)]  
    public bool activo;  
    [MarshalAs(UnManagedType.ByValArray, SizeConst =25)]  
    public char[] nombre;  
    [MarshalAs(UnManagedType.ByValArray, SizeConst =25)]  
    public char[] apellido;  
    public byte edad;  
    [MarshalAs(UnManagedType.ByValArray, SizeConst =20)]  
    public...

Realización de un "wrapper"

En las secciones anteriores, la codificación del acceso a las funciones de las DLLs no es óptima. De hecho, la dirección de la función C/C++ se evalúa en cada llamada y no hay noción de instancia de usuario en la DLL. Para remediar esto, el desarrollador C# podrá crear una clase llamada "wrapper" que va a:

  • gestionar las cargas, resolver las entradas y descargar la DLL no gestionada.

  • proporcionar métodos C# que van a llamar a las funciones no gestionadas.

El wrapper puede ser objeto de un ensamblado "separado". Este generalmente es el caso para los periféricos programables. El fabricante pone a disposición de los desarrolladores un wrapper en forma de ensamblado, es decir una DLL, que basta con referenciar en el proyecto para poder utilizarla.

Por motivos de simplificación, el wrapper de nuestro ejemplo aparece en el proyecto C# en forma de una clase (proyecto PInvoke08 para descargar).

La DLL a wrapper es muy sencilla. Contiene dos funciones: una recupera y guarda un valor, mientras que la segunda lo restituye.


#include "stdafx.h"  
#include <stdio.h>  
#include "Dll_C.h"  
  
  
static int DatoAGuardar = 0;  
  
  
__declspec(dllexport) void Copia(int datoAGuardar)  
{  
    DatoAGuardar = datoAGuardar;  
}  
  
__declspec(dllexport) int Restituye()  
{  
    return DatoAGuardar;  
} 
 

1. Una región "NativeMethods"

La carga de la DLL y la resolución de sus puntos de entrada se va a realizar mediante la llamada a algunas funciones de la API Win32. Incluso si no es una obligación, es conveniente declarar estas funciones en una clase separada.


#region WIN32_DLL_MANAGER  
  
public class Win32DllManager  
{  
    [DllImport("kernel32.dll", CharSet = CharSet.Unicode)]  
    public static extern IntPtr LoadLibrary(string lpFileName); 
  
    [DllImport("kernel32.dll")]  
    public static extern bool FreeLibrary(IntPtr module);  
  
    [DllImport("kernel32.dll", CharSet = CharSet.Unicode)]  ...

Ejercicio

1. Enunciado

El ejercicio propuesto consiste en realizar una aplicación gráfica que permite comprobar parcialmente la función MessageBox "nativa" de Windows. La interfaz a desarrollar se podría parecer a la siguiente:

images/RI09_17.png

La documentación de MessageBox está disponible en el sitio Web MSDN de Microsoft en la siguiente dirección: https://msdn.microsoft.com/es-es/library/windows/desktop/ms645505(v=vs.85).aspx

Se puede leer en qué DLL está la función, que esta función existe en los formatos Unicode y MBS y que combina con un determinado número de propiedades para realizar visualizaciones específicas.

2. Corrección


using System;  
using System.Collections.Generic;  
using System.ComponentModel;  
using System.Data;  
using System.Runtime.InteropServices;  
using System.Windows.Forms;  
  
namespace WindowsFormsApplications  
{  
    public partial class Form1: Form  
    {  
        public Form1()  
        {  
            InitializeComponent();  
            radioButtonInformacion.Checked = true;  
            radioButtonOk.Checked = true;  
      ...