Generación dinámica de códigoNociones básicas para la generación dinámica de código en múltiples lenguajes.
Fecha: 17/Ago/2005 (16/Ago/2005)
|
Con este artículo tengo la intención de mostrar como es posible crear código dinámicamente en cualquier lenguaje compatible con el CLR. No va a ser un artículo muy extenso ya que la mayoría de los objetos con los que hay que trabajar se comportan de igual manera, por lo que sólo va a ser necesario explicar algunas cuestiones básicas.
Con que vamos a trabajar
Todo lo que necesitamos para generar código se encuentra bajo el namespace System.CodeDom. Allí podremos encontrar clases como CodeTypeDeclaration, para hacer referencia al código de una clase, o CodeThisReferenceExpression, para hacer referencia al código que hace referencia al objeto con el que estamos trabajando (en otras palabras, "this" en C#, "Me" en VB). Esta última clase puede parecer un poco extraña. ¿Es necesario usar una clase para escribir "this" o "Me"? Pues sí. Lo único que vamos a tener que escribir como texto (o string) son sólo los nombres de las partes de código a generar (nombre de una clase, nombre de una propiedad, nombre de una variable, etc), el resto del código va a ser generado dinamicamente por clases. Nunca vamos a escribir cosas como "Public" (VB), "return" (C#), "X = Y", "Dim" (VB), etc. De esto se encargan las clases.
¿Porqué hacer tan complicada la generación de código? La respuesta es sencilla. Cada lenguaje tiene su sintaxis propia y por lo tanto no podemos esperar que estas sean compatibles. Por ejemplo, en Visual Basic podemos escribir lo siguiente para declarar una variable de tipo String:
Dim x As StringPero si escribimos eso en C#, nuestro código no llegará siquiera a compilar. En cambio, si usamos un objeto para hacer referencia a "x" (la variable) y otro objeto para hacer referencia a "String" (el tipo de la variable), podremos generar código C# útilizando la siguiente sintaxis (la verdadera sintaxis de C# es más compleja, pero esto es solo un ejemplo):
<TipoDato> <NombreVariable>;obteniendo
string x;que es el equivalente al código VB anterior. De este modo, uno puede generar código útilizando una sintaxis propia (algo muy útil si deseamos convertir código a un lenguaje propio):
Variable: <NombreVariable> Como: <TipoDeDato>pudiendo obtener algo como
Variable: x Como: System.StringPara no complicarse con el tema, lo mejor es ver al código como un árbol. Un árbol tiene un tronco, del cual se pueden desprender más de una rama, cada una de las cuales pueden tener más ramas, la cuales pueden contener aún más ramas, etc. Cada una de estas "ramas" puede ser representada con un objeto perteneciente al namespace System.CodeDom.
Clases básicas
El "tronco" de nuestro árbol está representado por la clase CodeCompileUnit. Esta clase posee, entre otras, la propiedad Namespaces de tipo CodeNamespaceCollection, el cual representa una colección de objetos CodeNamespace. Estos objetos son útilizados para representar un Namespace y su contenido. Las propiedades más importantes de la clase CodeNamespace son Imports (una colección de objetos CodeNamespaceImport, utlizados para "importar" namespaces) y Types (una colección de objetos CodeTypeDeclaration, los cuales pueden representar código de clases, estructuras, interfaces y enumeraciones).
Veamos ahora en que consiste la clase CodeTypeDeclaration. Esta es una lista de algunas propiedades con las que podemos trabajar:
Propiedad Contenido Name Nombre de la clase, estructura, etc. Attributes Colección de atributos que se aplican a la clase, estructura, etc. Comments Comentarios de la clase, estructura, etc. BaseTypes Colección de los tipos de los cuales hereda esta clase, estructura, etc. Members Colección de miembros de la clase, estructura, etc. La última propiedad es la más importante. Esta colección (de tipo CodeTypeMemberCollection) puede contener objetos del tipo CodeTypeMember. CodeTypeMember es la clase base útilizada para representar campos (útilizando la clase CodeMemberField), métodos (CodeMemberMethod) y propiedades (CodeMemberProperty) entre otras cosas. Como ejemplo, veamos algunas de las propiedades de la clase CodeMemberMethod:
Propiedad Contenido Name Nombre del método Parameters Colección de parametros pasados al método ReturnType Tipo de retorno del método Statements Colección de objetos CodeStatement que representan el contenido del método La clase CodeStatement es la clase base de muchas otras clases, las cuales se útilizan para representar el contenido de un miembro, es decir, el código a ejecutarse. Algunas de las clases derivadas son:
Clase Contenido CodeAssignStatement Código de asignación (por ejemplo, en C#: <UnaVariable> = <UnValor>;) CodeMethodReturnStatement Código de retorno de valor (por ejemplo, en VB: Return <UnValor>) CodeTryCatchFinallyStatement Código de un Try/Catch/Finally CodeVariableDeclarationStatement Código para declarar una variable (por ejemplo, en VB: Dim <nombreVariable> As <TipoDato>) Como dije al comienzo del artículo, y podemos comprobarlo ahora, hay clases para representar casi cualquier cosa. No voy a entrar más en detalle sobre las clases ya que en este mismo sitio se puede encontrar una descripción de cada una de ellas.
Armando el árbol
Ya vistas las clases con las que vamos a trabajar, veamos como trabajar con ellas. Me voy a limitar a mostrar el código básico. El archivo que acompaña el artículo contiene el código completo.
Inicialmente vamos a necesitar un objeto de tipo CodeCompileUnit, en el cual se encontrarán los namespaces con las clases a generar. El siguiente fragmento de código muestra como crear una clase:
//Creamos un metodo CodeMemberMethod metodo = new CodeMemberMethod(); metodo.Name = "UnMetodo"; // Seteamos al metodo como privado metodo.Attributes = MemberAttributes.Private; // Seteamos el tipo de retorno metodo.ReturnType = new CodeTypeReference("System.Int32"); // Creamos la clase CodeTypeDeclaration clase = new CodeTypeDeclaration("MiClase"); // Seteamos a la clase como publica clase.Attributes = MemberAttributes.Public; // Agregamos el metodo a la clase clase.Members.Add(metodo); // Creamos un namespace CodeNamespace ns = new CodeNamespace("MiNamespace"); // Importamos algunos namespaces ns.Imports.Add(new CodeNamespaceImport("System.Data.SqlClient")); ns.Imports.Add(new CodeNamespaceImport("System.Xml")); // Agregamos la clase al namespace ns.Types.Add(clase); //Creamos el CodeCompileUnit CodeCompileUnit ccu = new CodeCompileUnit(); // Agregamos el namespace al CodeCompileUnit ccu.Namespaces.Add(ns);Como se puede apreciar no es tan dificil crear el árbol. En éste ejemplo nuestro código consistirá en un namespace llamado "MiNamespace", el cual contiene sólo una clase. Esta clase, llamada "MiClase", posee únicamente un método (que devuelve un entero) llamado "UnMetodo".
Para generar el código vamos a tener que recurrir a un generador de código, es decir, a cualquier clase que implemente la interface ICodeGenerator. La forma más sencilla de obtener uno es útilizando una clase que herede de la clase CodeDomProvider, que se encuentra en el namespace System.CodeDom.Compiler. Para generar el código podemos usar el siguiente fragmento:
//Generador que vamos a usar ICodeGenerator gen; //La clase CSharpCodeProvider hereda de CodeDomProvider Microsoft.CSharp.CSharpCodeProvider cp = new Microsoft.CSharp.CSharpCodeProvider(); //Si quisieramos generar codigo VB tendríamos que usar el siguente codigo //Microsoft.VisualBasic.VBCodeProvider cp = new Microsoft.VisualBasic.VBCodeProvider(); //Asignamos a gen un generador de código gen = cp.CreateGenerator(); //Guardamos la extension que debe tener el archivo string extension = cp.FileExtension; //Creamos un IndentedTextWriter con el cual vamos a guardar el codigo generado IndentedTextWriter itw = new IndentedTextWriter(new StreamWriter("C:\\codigo." + extension, false), " "); //Finalmente generamos el codigo gen.GenerateCodeFromCompileUnit(ccu, itw, new CodeGeneratorOptions());Cabe aclarar que no es obligatorio útilizar un objeto CodeCompileUnit. También es posible generar el código a partir de una clase (gen.GenerateCodeFromType()) o un namespace (gen.GenerateCodeFromNamespace()) por ejemplo.
El generador de código
El código que acompaña al artículo es un generador de código básico. Su finalidad es la de mostrar el uso de las clases aca expuestas y nada más. El diseño general de la aplicación deja bastante que desear (y por lo tanto no debe tomarse como ejemplo) y el código a generar es limitado (permite trabajar solo con un namespace, no podremos generar miembros estáticos, no podremos generar campos, trabaja con un número limitado de tipos, etc). Hay 3 clases importantes que son las que hacen el trabajo pesado. Estas son Generador, GeneradorDeClases y GeneradorDeMiembros. Estan bastente incompletas, pero pueden servir como base para generar clases generadoras más complejas que permitan especificar más opciones y amplien su útilidad.
Lo unico que queda por aclara es que el código es creado en un archivo llamado "codigo" en el directorio de la aplicación. Espero que les haya parecido interesante, y sobre todo, útil.
Espacios de nombres usados en el código de este artículo:
System.CodeDom
System.CodeDom.Compiler
Fichero con el código de ejemplo: qrox_gencodigo.zip - 23 KB