Creando una DLL en VISUAL C++ 2010 EXPRESS y Usándola en una Aplicación en VISUAL C# 2010 EXPRESS

Un día necesité proteger parte de mi código en C# contra los decompiladores para .Net. Ofuscar el código no me parecía lo suficientemente seguro, así que decidí meter una parte del código dentro de una librería DLL escrita en C++. Para este tutorial, la función dentro de la librería recibirá un puntero a un array de datos tipo byte, y un dato tipo entero (int); hará un pequeño bucle y al final devolverá otro puntero a un array de bytes.

Primero abrimos el Visual C++ (yo tengo la versión 2010 Express), elegimos Nuevo Proyecto->Win32->Proyecto de Win32. A mi proyecto le he puesto el nombre de "miDLL".


Como tipo de aplicación elegimos "Biblioteca de vínculos dinámicos". Por ser la versión Express, no nos permite usar las librerías MFC ó ATL, pero no importa porque yo sigo sin saberlas usar :P

 

Al presionar el botón de "Finalizar", el Visual C++ nos crea los archivos necesarios para empezar a programar la librería. En el archivo dllmain.cpp ya ha creado el código para el punto de entrada de la DLL:


Es en este archivo donde añadiremos el código de nuestra función. Para este tutorial, es un pequeño bucle que realiza una división sencilla:

extern "C"

__declspec (dllexport) unsigned char *punt (unsigned char *puntero, int lenght)

{

  unsigned char* p2 = new unsigned char[lenght];

 

    for (int i=0; i < lenght; i++)

    { 

       if (puntero[i] > 100)

          p2[i] = puntero[i] / 2;

       else

          p2[i]=puntero[i];

    }
 

  return p2;

}

 

Nos falta añadir un archivo de cabecera. Para ello vamos a Proyectos->Agregar nuevo elemento:


Escojo la opción "Archivo de Cabecera". A mi nuevo archivo lo llamaré "miDLL":


Esto nos creará un archivo .h en blanco llamado "miDLL.h". Allí escribimos el siguiente código, el cual contiene la firma de nuestra función y le indica a la DLL que es la función que usarán otras aplicaciones:

#ifndef _INICIO_H

#define _INICIO_H
 

#ifdef BUILD_DLL // en la construcción de la librería

#define EXPORT __declspec(dllexport)

#else // en la construcción del ejecutable

#define EXPORT __declspec(dllimport)

#endif
 

#ifdef __cplusplus /* if in a C++ program */

extern "C"

#endif
 

__declspec (dllexport) unsigned char *punt (unsigned char *puntero, int lenght);

#endif

 

Como última comprobación, vamos a Proyecto->Propiedades de miDLL->Propiedades de configuración->C/C++->Avanzadas->Convención de llamada:

Las "Convenciones de Llamada" le indican a la DLL los siguientes detalles (Fuente: Documentación MSDN):

Es importante saber esto, pues la Convención de Llamada debe definirse en la aplicación que usará la DLL. Si no se hace así, provocará una PImvokeStackImbalance Exception (el compilador detecta que la convención de llamada declarada en la DLL no coincide con la convención de llamada declarada en la aplicación. En C# para Windows la convención de llamada por defecto es StdCall).


Usando la DLL Desde C#

Para hacer este tutorial más sencillo, la DLL será usada por una aplicación de consola escrita en C# (llamada ConsoleApplication1), el código es el mismo también para una aplicación de Windows Forms.

Primero abrimos el Visual C# (yo uso la versión 2010 Express) y elegimos como proyecto "Aplicación de Consola". Luego copiamos la DLL ya compilada (ubicada en la carpeta Debug dentro de la carpeta del proyecto miDLL y llamada "miDLL.dll") a la carpeta Debug del proyecto en C# (si se compila como "Debug") ó Release (al generar el proyecto ó si se compila como "Release"). Yo prefiero copiar miDLL.dll a ambas carpetas.

Hay dos maneras de llamar a la DLL: Usando código inseguro con punteros y usando código seguro sin punteros.

Para ambos casos, se imprimen en la consola los valores originales del array de bytes, y luego se imprimen los valores del array de bytes sobre los que se ha aplicado la llamada a la DLL. Nótese cómo en la declaración de la función de la DLL se establece la convención de llamada.

Usando código no seguro:

Para poder usar código inseguro en C# se debe ir a Proyecto->Propiedades de ConsoleApplication1->Generar y marcar la casilla que dice "Permitir código no seguro".

Este es el código de la aplicación:

using System.Runtime.InteropServices;

 

namespace ConsoleApplication1

{

  class Program

  {

    [DllImport("miDLL.dll", CallingConvention = CallingConvention.Cdecl)]

    unsafe static extern byte *punt (byte *puntero, int lenght);
 

    static void Main(string[] args)

    {

        byte[] p = new byte[20];
 

        for (int i = 0; i < p.Length; i++)

        {

            p[i] = (byte)(i * 2 + 101);

            Console.WriteLine(p[i]);

        }
 

        Console.WriteLine();

 

        unsafe

        {

            fixed (byte* p0 = p)

            {

               byte* p2 = punt(p0, p.Length);
 

               for (int i = 0; i < p.Length; i++)

                  Console.WriteLine(p2[i]);

           }

        }

 

        Console.Read();

    }

  }

}

 

Usar código no seguro me permite copiar la firma exacta de la DLL y usarla tal y como ha sido declarada. En C# no existe el tipo de dato "unsigned char" así que lo reemplazo por "byte". Ambos tipos de datos ocupan la misma cantidad de bits.

Usando código seguro:

En este caso la aplicación en C# sólo trabaja con código administrado (cuya memoria es administrada por el Colector de Basura ó Garbage Collector), pero la DLL emplea código no administrado (la memoria no se libera automáticamente pues no hay un Colector de Basura), por ello debemos hacer una conversión entre los valores devueltos por la DLL (no administrados) y los que recibe la aplicación (administrada). El código es:

using System;

using System.Runtime.InteropServices;

 

namespace ConsoleApplication1

{

   class Program

   {

        [DllImport("miDLL.dll", CallingConvention = CallingConvention.Cdecl)]

        static extern IntPtr punt(byte[] puntero, int lenght);
 

        static void Main(string[] args)

        {

             byte[] p = new byte[20];
 

             for (int i = 0; i < p.Length; i++)

             {

                 p[i] = (byte)(i * 2 + 101);

                 Console.WriteLine(p[i]);

             }
 

             Console.WriteLine();

 

             IntPtr pTr = punt(p, p.Length);

             byte[] results = new byte[p.Length];
 

             Marshal.Copy(pTr, results, 0, p.Length);
 

             for (int i = 0; i < p.Length; i++)

                  Console.WriteLine(results[i]);

 

             Console.Read();

       }

   }

}

 

"IntPtr" representa a un puntero y se usa para operar con código no administrado. Para obtener los valores que devuelve a DLL (no administrada) y usarla desde la aplicación en C# (administrada) usamos el método Copy de la clase Marshal, la cual, según Microsoft "Proporciona una colección de métodos para asignar memoria no administrada, copiar bloques de memoria no administrados y convertir los tipos administrados en no administrados, así como otros métodos diversos que se utilizan al interactuar con código no administrado".

El proyecto con los tres ejemplos se puede descargar de aquí. La DLL se compila con Visual C++ Express 2010, y la aplicación de consola con Visual C# Express 2010, y corre sobre .Net 3.5.

Un detalle más: Si la DLL contiene más de una función, el compilador cambiará sus nombres, y estos nuevos nombres serán los que se declararán en las aplicaciones que utilicen la DLL. Hay formas de evitar que el compilador haga esto, pero yo prefiero dejar la DLL como está debido a que mi propósito no era compartir o reutilizar código si no ocultarlo. Para poder saber los nuevos nombres de las funciones utilizo esta aplicación: DLLFunctionViewer.

 

www.000webhost.com