martes, 26 de agosto de 2014

Embeber una librería dentro de un ejecutable en .NET

Buenas a to@s

Hoy voy a contaros un recurso que he utilizado alguna vez para evitarme problemas derivados de redistribuir librerías. En momentos es necesario que un programa sea manejable y a poder ser indivisible, para que no se produzcan problemas derivados de la falta de algunas librerías que este utilice. Algunos leguajes de programación como Delphi solucionaban estos problemas muy bien con su característica RunOnce, por eso los programas de Delphi pesaban 2 megas cuando el mismo código en .Net ocupa poco más de 20 Kb. Dejando a Delphi de un lado, el tener la fiabilidad de que tu programa va a funcionar siempre de manera correcta y que nunca le va a faltar una librería (y ademas estas siempre van a ser la versión correcta), es algo que nos da mucha tranquilidad a la hora de redistribuirlo. 

En .Net existe una caracteriza por la que podemos integrar cualquier fichero en nuestro software. Simplemente seleccionamos el fichero, ensamblado, documento… de nuestro proyecto que queramos, vamos a propiedades y en la propiedad “Acción de compilación” seleccionamos “Recurso Incrustado”. Una vez hecho esto ya podemos llamar al cualquier fichero que hayamos incrustado en nuestra aplicación desde dentro sin miedo a que nos dé un error. Para acceder a estos podremos encontrarlos dentro del espacio de nombres de nuestro programa por ejemplo MiPrograma1.imagen.png.

Hasta aquí todo perfecto, podemos incrustar cualquier cosa, pero si probamos a hacerlo con una librería obtendremos un error en tiempo de ejecución de que no encuentra la librería. Para solucionar este problema debemos insertar una línea en el Main de nuestra aplicación y crear un nuevo método en el fichero program de nuestra aplicación para resolver las librerías gracias a los métodos de reflexión.

Dentro de nuestro main debemos escribir una línea similar a esta.
AppDomain.CurrentDomain.AssemblyResolve += new ResolveEventHandler(ResolverComponentes);

Nos suscribimos al método que se encarga de resolver las referencias y creamos un nuevo manejado que invoque la función que vamos a crear para resolver nuestras referencias. De esta forma lo que el programa va hacer es lo siguiente, primero intenta resolver las librerías por sí mismo y sino la encuentra ira a la función ResolverComponentes que hemos creado.

La función ResolverComponentes devolverá un parámetro tipo System.Reflection.Assembly debe tener dos parámetros de entrada, un object y un ResolveEventArgs que será del que obtendremos el nombre de la librería que debemos devolver.

Esta sería una posible definición:

static System.Reflection.Assembly ResolverComponentes(object sender, ResolveEventArgs args)

Para saber cuál es la librería que necesitamos, podemos hacerlo mediante la propiedad name del segundo parámetro, en este caso args.name. Esto es muy útil si tenemos que cargar varias librerías.

Por ultimo debemos devolver la librería cargada, eso lo hacemos mediante el método load del constructor de System.Reflection.Assembly es decir System.Reflection.Assembly.Load(). Posiblemente este método no os aparezca, dado que solo existe en el constructor.

Al revisarlo os daréis cuenta de que no existe ninguna definición en la que podamos pasarle la ruta donde está la librería. Tras darle unas vueltas lo que me pareció más sencillo fue enviar un array de bytes con la librería contenida en él. Para eso primero nos definimos un System.IO.Stream que contendrá el Stream de nuestra librería. Podemos hacerlo de la siguiente forma:

System.IO.Stream _streamDeLibreria;
_streamDeLibreria = _libreriaResolver.GetManifestResourceStream("MiPrograma.Librerias.LibreriaEmbebida.dll");

Una vez definido el Stream ya solo tenemos que leer todos los bytes y devolverla en el objeto System.IO.Stream:

byte[] _arrayDeLibreria = new byte[_streamDeLibreria.Length];
_streamDeLibreria.Read(_arrayDeLibreria, 0, _arrayDeLibreria.Length);
System.Reflection.Assembly _libreria_resuelta = System.Reflection.Assembly.Load(_arrayDeLibreria);

Por último solo quedaría devolver la biblioteca:

return _libreria_resuelta;


Como podéis ver el método no es muy complicado, si un poco enrevesado pero muy útil.

Si por ejemplo necesitáis hacer esto con varias bibliotecas podéis hacer un if, swich o un select case en el que condicionáis con el nombre, por ejemplo algo así:

if (args.Name == "MiLibreria.Libreria, Version=1.0.0.0, Culture=neutral, PublicKeyToken=1b06a8ecf2258a87")
            
Si vais a realizar esto con muchas librerías, podéis definiros una variable System.Reflection.Assembly por cada librería, cargarlas en las diferentes variables con un método como el anterior y el al función ResolverComponentes solamente debemos hacer un return de la variable previamente cargada con la librería.


Aquí os dejo el método completo para que podáis modificarlo y probarlo:

static System.Reflection.Assembly ResolverComponentes(object sender, ResolveEventArgs args)
{

System.Reflection.Assembly _libreriaResolver = System.Reflection.Assembly.GetExecutingAssembly();
System.IO.Stream _streamDeLibreria;

if (args.Name == "MiLibreria.Libreria, Version=1.0.0.0, Culture=neutral, PublicKeyToken=1b06a8ecf2258a87")
{
_streamDeLibreria = _libreriaResolver.GetManifestResourceStream("MiPrograma.Librerias.LibreriaEmbebida.dll");
}
else
{
_streamDeLibreria = __libreriaResolver.GetManifestResourceStream("MiPrograma.Librerias.LibreriaEmbebida2.dll");           
}

           
byte[] _arrayDeLibreria = new byte[_streamDeLibreria.Length];
_streamDeLibreria.Read(_arrayDeLibreria, 0, _arrayDeLibreria.Length);
System.Reflection.Assembly _libreria_resuelta = System.Reflection.Assembly.Load(_arrayDeLibreria);

return _libreria_resuelta;

}


Si alguien lo necesita y lo pide lo puedo “traducir” a VB si fuera necesario, aunque creo que se entiende bastante bien. 

De la misma manera si lo "traducís" y queréis que lo publique, no hay ningún problema, poneros en contacto conmigo y lo vemos. Si alguien realiza el mismo código, lo mejora o lo modifica y lo publica en su blog, puedo poneros un enlace desde el articulo a vuestra versión.


Espero la lectura haya sido amena e interesante y sobre todo que sirva para algo.
Muy importante, si decides comentar o republicar parte de este articulo porque te ha sido útil, por favor cita la fuente y el autor del mismo (vamos cítame) y pon un enlace al artículo de mi blog

Muchas gracias por leerme.
Saludetes a todos

P.D. Podéis seguirme en @Jberron, Google+ y LinkedIn





No hay comentarios: