Generación dinámica de código

Nociones básicas para la generación dinámica de código en múltiples lenguajes.

 

Fecha: 17/Ago/2005 (16/Ago/2005)
Autor: Gerardo Contijoch (mazarin9@yahoo.com.ar)

 


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 String

Pero 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.String

Para 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:

PropiedadContenido
NameNombre de la clase, estructura, etc.
AttributesColección de atributos que se aplican a la clase, estructura, etc.
CommentsComentarios de la clase, estructura, etc.
BaseTypesColección de los tipos de los cuales hereda esta clase, estructura, etc.
MembersColecció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:

PropiedadContenido
NameNombre del método
ParametersColección de parametros pasados al método
ReturnTypeTipo de retorno del método
StatementsColecció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:

ClaseContenido
CodeAssignStatementCódigo de asignación (por ejemplo, en C#: <UnaVariable> = <UnValor>;)
CodeMethodReturnStatementCódigo de retorno de valor (por ejemplo, en VB: Return <UnValor>)
CodeTryCatchFinallyStatementCódigo de un Try/Catch/Finally
CodeVariableDeclarationStatementCó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


ir al índice