Herencia múltiple estática en C# con Mono.Cecil

Introducción

Este fin de semana empecé una prueba de concepto, que luego se transformo en un proyecto. Mi idea en este artículo es mostrar todo mi razonamiento, mi metodología para enfrentar este desarrollo, como fui aprendiendo las herramientas  y como nacieron los distintos artefactos que componen esta solución.

Todo el código que voy a mostrar puede ser descargado o visto online en este sitio (HeredarPoc bitbucket). Si bien en ese repositorio llegamos a un lugar bastante avanzado, actualmente esta congelado y el código actualizado puede encontrarse aquí. No obstante, para seguir esta guía recomiendo el primer repositorio ya que parte prácticamente desde cero hasta llegar a algo bastante avanzado.

Un poco de teoría

El objetivo del proyecto es crear una herramienta que modifique nuestro ensamblado después de haber sido compilado, de manera tal que un código como este:

public class Auditable : IAuditable
{
  public DateTime Creado        { get; set; }
  public string   CreadoPor     { get; set; }
  public DateTime Modificado    { get; set; }
  public string   ModificadoPor { get; set; }
}

public class Validable : IValidable
{
  public IEnumerable<string> Validate()
  {
     ValidationService.Validate(this);
  }
}


[ExtendWith(typeof(Auditable), typeof(Validable))]
public class Persona
{
  public string Nombre   { get; set; }
  public string Apellido { get; set; }
}

Se transforme en esto, luego de compilarse:

public class Persona : IAuditable, IValidable
{
  public string   Nombre        { get; set; }
  public string   Apellido      { get; set; }

  public IEnumerable<string> Validate()
  {
     ValidationService.Validate(this);
  }

  public DateTime Creado        { get; set; }
  public string   CreadoPor     { get; set; }
  public DateTime Modificado    { get; set; }
  public string   ModificadoPor { get; set; }
}

Básicamente este es uno de los casos de uso más sencillos y utilizados de herencia múltiple; conocidos como MixIns. La idea es que cada Template (me gusta llamar a los templates “Condimento”) es un pedazo de funcionalidad mínima que podemos utilizar al programar una clase.

En este tipos de escenarios la herencia convencional falla; ya que cuando heredamos, heredamos un todo. Es probable que nuestra clase base tenga demasiadas cosas inconexas, lo cual sea difícil de mantener con el tiempo. Las cadenas de herencia largas también son difíciles de seguir, de mantener y de entender. Es por ello que una buena práctica de programación es favorecer la composición a la herencia.

Más información:

Definiendo lo que queremos

Mi primer idea fue empezar a jugar con un test mas o menos así:

using System;
using System.IO;
using System.Linq;
using System.Reflection;
using NUnit.Framework;
using SharpTestsEx;

namespace HeredarPoc.Tests
{
    [TestFixture]
    public class MixinTests
    {
        private string weavedAssemblyPath;

        [TestFixtureSetUp]
        public void Setup()
        {
            var weaver = new Weaver();
            const string assemblytoprocessBinDebugAssemblytoprocessDll =
                @"......AssemblyToProcessBinDebugAssemblyToProcess.dll";
            var assemblyPath = Path.GetFullPath(assemblytoprocessBinDebugAssemblytoprocessDll);
            weavedAssemblyPath = assemblyPath.Replace(".dll", "2.dll");
            weaver.Weave(assemblyPath, weavedAssemblyPath);
        }

        [Test]
        public void CanMixASimpleProperty()
        {
            var type = Assembly.LoadFile(weavedAssemblyPath).GetType("AssemblyToProcess.SampleClass1", true);
            type.Satisfy(t => t.GetProperties()
                .Any(p => p.Name == "MixedProperty" && p.PropertyType == typeof (string)));
        }

        [Test]
        public void CanUseAMixedProperty()
        {
            var assembly = Assembly.LoadFile(weavedAssemblyPath);
            dynamic instance = assembly.CreateInstance("AssemblyToProcess.SampleClass1");
            instance.MixedProperty = "hello";
            Assert.AreEqual("hello", instance.MixedProperty);
        }

    }
}

El test hace lo siguiente:

  • El setup, llama a nuestro objeto bajo estudio Weaver del cual voy a hablar en un momento, pasandole una ruta a un ensamblado original, y una ruta a un ensamblado de destino, es en esa ruta donde va a quedar nuestra dll “retocada”.
  • El método CanMixASimpleProperty carga un tipo desde el ensamblado “retocado” mediante reflection, y comprueba que ese tipo tenga la propiedad que estaba en el template.
  • El método CanUseAMixedProperty crea una instancia de este tipo, también desde el ensamblado retocado, e intentar utilizar una propiedad definida en el template. Para ello hice uso de la palabra clave “dynamic”.

El template es el siguiente:

Y la clase que use como objetivo para las pruebas es esta:

Nota sobre TDD y estilo de desarrollo

Es evidente que este tipo de tests no son “exactamente” unit tests. En este punto del desarrollo no puedo darme el lujo de escribir tests unitarios, por que apenas conozco el modelo interno de las herramientas que voy a utilizar.

Por otro lado una de las cosas mas menospreciadas de la metodología Red-Green-Refactor es la primer parte: Red. Considero que tiene mucho valor, no por el hecho de ver que este en rojo, si no que lo que mas valor me aporta es ver que la falla del test es realmente la que yo quiero.  Llegar a ese punto en este tipo de experimentos suele ser un desafío también.

Introducción a Mono.Cecil

En este punto debería quedar claro, que la intención es crear una implementación de la clase Weaver. Para ello, voy a utilizar una herramienta muy específica llamada Mono.Cecil:

Cecil is a library written by Jb Evain to generate and inspect programs and libraries in the ECMA CIL format. It has full support for generics, and support some debugging symbol format.

In simple English, with Cecil, you can load existing managed assemblies, browse all the contained types, modify them on the fly and save back to the disk the modified assembly.

Para los que están familiarizados con Reflection en .Net, es bastante sencillo de entender lo que hace. Al igual que la API de reflection, puedo obtener un tipo, ver que propiedades, métodos, fields,  sus atributos, etc. Los dos atractivos más grandes para mi caso son:

  • Permite llegar hasta niveles muchos más bajos que el api de reflection de .Net. Por ejemplo, puedo preguntar que variables están definidas dentro de un método, o cuales son las instrucciones que un método tiene.
  • Todo es modificable y puedo guardar los cambios que haga. Puedo agregar variables a un método, quitar un método de una clase, etc.

Este diagrama de clases resulta muy importante para lo que voy a explicar a continuación:

diagram

El diagrama es bastante trivial, los ensamblados en .Net contienen módulos, dichos módulos contienen tipos. TypeDefinition es de particular interés por que ahí esta todo lo que necesitamos importar, en otro TypeDefinition. Como se puede ver la mayoría son colecciones de cosas, y a todas estas colecciones se pueden agregar/quitar elementos. No son simple enumeradores.

La técnica que fui implementando es la de clonar.

Recordar que en el caso de nuestro test el Template tenía solamente un auto-property. Pero en realidad… una auto-property es un concepto de alto nivel, una forma abreviada o un truco del compilador. Al compilarse esto genera una propiedad común y corriente que internamente utiliza un backing field.

Lo que hice fue primero clonar los fields, luego clonar los métodos y luego clonar las propiedades.

De más esta decir que en cada paso hay que hacer alguna magia, para redireccionar algo que estaba apuntando a otra cosa del template. Por ejemplo, al clonar una propiedad, hay que apuntar su SetMethod y su GetMethod a los métodos ya importados.

La mayor complejidad la encontré al intentar clonar una instrucción, esto merece un título aparte.

Instrucciones en CIL

MSIL, CIL o Common Intermediate Language según wikipedia es:

…es el lenguaje de programación legible por humanos de más bajo nivel en el Common Language Infrastructure y en el .NET Framework. Los lenguajes del .NET Framework compilan a CIL. CIL es un lenguaje ensamblador orientado a objetos, y está basado en pilas. Es ejecutado por una máquina virtual. Los lenguajes .NET principales son C#, Visual Basic .NET, C++/CLI, y J#.

Me quedo con la parte, que más me gusto “lenguaje ensamblador orientado a objetos”. Me recuerda mucho a mis prácticas en la secundaria con lenguaje ensamblador, solo que ese lenguaje ensamblador no era orientado a objetos. Dentro de CIL existe todo lo que explique en el título anterior Clase, Propiedad, Método, Attributos, Variables, etc. sus modificadores de acceso y demás cosas. Pero al llegar a nivel de instrucción es cuando más se parece a assembler.

Las instrucciones en CIL, son propiamente como las de cualquier lenguaje ensamblador, están compuestas básicamente de dos partes:

  • Operador: también conocido como OpCode
  • Operando

Como el Operador (por el momento) lo puedo clonar tal y cual esta no me preocupé mucho. El operando es la parte más difícil, ¿Qué valores puede tener un operando?

  • Un operando puede apuntar a otra instrucción, un ejemplo sería en un bloque condicional, es decir un if.
  • Un operando puede apuntar a un field definido en la misma clase.
  • Un operando puede apuntar a un field definido en otra clase.
  • Un operando puede ser igual a una constante string, int, lo que sea.
  • Un operando puede apuntar a un type.

Aunque en nuestro ejemplo anterior (test) solo se presentan el caso de field y de instrucción.

Mínima implementación

Me costo bastante llegar a la mínima implementación. En el camino descubrí otras cosas:

  • Para agilizar mi trabajo utilice Reflector Pro. Esto me ayudó mucho a ver como iba quedando mi ensamblado, inclusive reflector permite alternar entre c# y código CIL. Lo cual es muy útil.
  • Mono.Cecil permite hacer cualquier cosa, por más mal que parezca. Prácticamente no realiza ninguna validación de lo que estamos haciendo y no garantiza que el runtime de .net pueda si quiera cargar nuestros ensamblados.
  • Dado lo que dije en el punto anterior, se hace imprescindible una herramienta de línea de comandos que viene con .Net llamada PeVerify.
  • El runtime de .Net es muchas veces más permisivo que PeVerify.

La primera implementación, que hace que el escenario antes mencionado funcione, es la siguiente:

using System;
using System.Collections.Generic;
using System.Linq;
using Mono.Cecil;
using Mono.Cecil.Cil;
using Mono.Collections.Generic;

namespace HeredarPoc
{
    public class Weaver
    {
        public void Weave(string source, string target)
        {
            var assembly = AssemblyDefinition.ReadAssembly(source);
            var module = assembly.Modules.First();

            var toBeWeaved = from t in module.Types
                             let attributes =
                                 t.CustomAttributes.Where(ca => ca.AttributeType.FullName == typeof (MixInAttribute).FullName)
                             where attributes.Any()
                             select new
                                        {
                                            Type = t,
                                            MixedClasses = attributes.SelectMany(a => a.ConstructorArguments.Select(ca => ca.Value))
                                                                     .OfType<CustomAttributeArgument[]>()
                                                                     .SelectMany(caas => caas.Select(caa => caa.Value))
                                                                    .OfType<TypeReference>()
                                                                    .Select(tr => module.Types.First(td => td.FullName == tr.FullName))
                                   };
            
            foreach (var pair in toBeWeaved)
            {
                
                foreach (var fieldDefinition in pair.MixedClasses.SelectMany(mc => mc.Fields))
                {
                    var newfield = new FieldDefinition(fieldDefinition.Name, fieldDefinition.Attributes, fieldDefinition.FieldType);
                    //newfield.Attributes = fieldDefinition.Attributes;
                    foreach (var ca in fieldDefinition.CustomAttributes)
                    {
                        newfield.CustomAttributes.Add(ca);
                    }
                    pair.Type.Fields.Add(newfield);
                }

                foreach (var propertyToBeMixed in pair.MixedClasses.SelectMany(tr => tr.Properties))
                {
                    var propertyDefinition = new PropertyDefinition(propertyToBeMixed.Name, propertyToBeMixed.Attributes, propertyToBeMixed.PropertyType)
                                                 {
                                                     GetMethod = CloneMethod(propertyToBeMixed.GetMethod, pair.Type),
                                                     SetMethod = CloneMethod(propertyToBeMixed.SetMethod, pair.Type)
                                                 };
                    foreach (var customAttribute in propertyToBeMixed.CustomAttributes)
                    {
                        propertyDefinition.CustomAttributes.Add(customAttribute);
                    }
                    pair.Type.Properties.Add(propertyDefinition);
                    pair.Type.Methods.Add(propertyDefinition.GetMethod);
                    pair.Type.Methods.Add(propertyDefinition.SetMethod);
                }

                //foreach (var methodToMixIn in pair.MixedClasses.SelectMany(tr => tr.Methods))
                //{
                //    pair.Type.Methods.Add(new MethodDefinition(methodToMixIn.Name, methodToMixIn.Attributes, methodToMixIn.ReturnType)
                //                            {
                //                                Body = methodToMixIn.Body
                //                            });
                //}
            }

            module.Write(target);
        }

        private static MethodDefinition CloneMethod(MethodDefinition sourceMethod, TypeDefinition into)
        {
            var methodDefinition = new MethodDefinition(sourceMethod.Name, sourceMethod.Attributes, sourceMethod.ReturnType)
                                       {
                                           MetadataToken = MetadataToken.Zero,
                                           DeclaringType = into,
                                           Body = {InitLocals = sourceMethod.Body.InitLocals}
                                       };

            foreach (var customAttribute in sourceMethod.CustomAttributes)
            {
                methodDefinition.CustomAttributes.Add(customAttribute);
            }

            foreach (var variableDefinition in sourceMethod.Body.Variables)
            {
                methodDefinition.Body.Variables.Add(new VariableDefinition(variableDefinition.Name, variableDefinition.VariableType));
            }

            foreach (var parameterDefinition in sourceMethod.Parameters)
            {
                var definition = new ParameterDefinition(parameterDefinition.Name, parameterDefinition.Attributes, parameterDefinition.ParameterType);
                methodDefinition.Parameters.Add(definition);
            }

            var pendingInstructions = new Dictionary<int, Instruction>();

            foreach (var instruction in sourceMethod.Body.Instructions)
            {
                Instruction instructionToAdd;
                if(!pendingInstructions.TryGetValue(sourceMethod.Body.Instructions.IndexOf(instruction), out instructionToAdd))
                {
                    instructionToAdd = CloneInstruction(methodDefinition, into, sourceMethod.Body.Instructions, instruction, pendingInstructions);
                }
                methodDefinition.Body.Instructions.Add(instructionToAdd);
            }
            for (int i = 0; i < methodDefinition.Body.Instructions.Count - 1; i++)
            {
                if(methodDefinition.Body.Instructions[i] == null)
                {
                    methodDefinition.Body.Instructions.RemoveAt(i);
                }
            }
            return methodDefinition;
        }

        private static Instruction CloneInstruction(
            MethodDefinition newMethod, 
            TypeDefinition newType, 
            Collection<Instruction> sourceInstructions, 
            Instruction instructionToClone, 
            IDictionary<int, Instruction> pendingInstructions)
        {

            if (instructionToClone.Operand == null) return Instruction.Create(instructionToClone.OpCode);

            var fieldDefinition = instructionToClone.Operand as FieldDefinition;
            if(fieldDefinition != null)
            {
                return Instruction.Create(instructionToClone.OpCode, newType.Fields.First(f => f.Name == fieldDefinition.Name));
            }
            
            var instructionDefinition = instructionToClone.Operand as Instruction;
            if(instructionDefinition != null)
            {
                var offset = sourceInstructions.IndexOf(instructionDefinition);

                var targetInstruction = newMethod.Body.Instructions.ElementAtOrDefault(offset)
                                        ?? pendingInstructions.GetValueOrDefault(offset);

                if (targetInstruction == null)
                {
                    targetInstruction = CloneInstruction(newMethod, newType, sourceInstructions, instructionDefinition, pendingInstructions);
                    pendingInstructions[offset] = targetInstruction;
                }

                return Instruction.Create(instructionToClone.OpCode, targetInstruction);
            }

            throw new NotImplementedException(string.Format("can't clone instructions with operand equals to {0}", instructionToClone.Operand.GetType()));
        }
    }

    public static class DictionaryExtensions
    {
        public static TValue GetValueOrDefault<TKey, TValue>(this IDictionary<TKey, TValue> dic, TKey key)
        {
            TValue value;
            return dic.TryGetValue(key, out value) ? value : default(TValue);
        }
    }
}

Lo que esta clase hace en principio es buscar que tipos tiene que modificar y que templates debe importar en ellos. Luego para cada template va clonando pedacitos de su estructura (por ahora fields, métodos y propiedades) en la clase de destino.

El código que clona la instrucción puede parecer complejo. El problema ahí es que mi forma de clonar es secuencial, y es muy factible que el operando de una instrucción, sea otra instrucción que aún no fue clonada. Por lo tanto lo que hago es clonar inmediatamente la otra instrucción recursivamente, y almacenar en una lista para luego agregarla cuando sea su turno.

Avance y refactoring posteriores

Fui progresivamente agregando escenarios fáciles como “un método en el template que usa una propiedad del template” hasta casos mas complejos como un “template que implementa una interfaz”.

Sucesivos refactorings hicieron que el weaver me quedara de esta forma:

using System.Collections.Generic;
using System.Linq;
using HeredarPoc.Cloners;
using HeredarPoc.Inspectors;
using Mono.Cecil;

namespace HeredarPoc
{
    public class Weaver
    {
        private static readonly IEnumerable<IInspector> Inspectors
            = new IInspector[] { new AttributeBaseInspector() };

        private static readonly IEnumerable<ICloneVisitor> Visitors
            = new ICloneVisitor[]
                  {
                    new FieldCloneVisitor(), 
                    new MethodCloneVisitor(), 
                    new PropertyCloneVisitor(),
                    new InterfaceCloneVisitor()
                  };

        public void Weave(string source, string target)
        {
            var assembly = AssemblyDefinition.ReadAssembly(source);
            var pairs = Inspectors.SelectMany(i => i.GetPairs(assembly));

            foreach (var mixPair in pairs)
            {
                foreach (var mixedClass in mixPair.Templates)
                {
                    foreach (var cloneVisitor in Visitors)
                    {
                        cloneVisitor.Visit(mixedClass, mixPair.Target);
                    }
                }
            }

            assembly.MainModule.Write(target);
        }
    }
}

Como se puede ver acá, pude extraer artefactos que realizan una parte más especifica de la clonación. Como así también extraje otro artefacto que es el encargado de inspeccionar el ensamblado y encontrar que tipos debe modificar y que templates le tiene que agregar.

Inclusive el MethodCloneVisitor utiliza otros clases mas pequeñas para poder llevar a cabo el clonado del cuerpo del método. Lo cual hace que el código sea más entendible.

Plan

Simon Cropp (Australia) es quien esta trabajando conmigo en este proyecto opensource. Como dije antes, el nombre del proyecto es Heredar y se encuentra en google code. No se encuentra liberada ninguna versión todavía.

Hasta ahora soporta casos simples, nuestra idea es ir agregando progresivamente. Un buen lugar para ver los casos que están soportados son los tests, pero esta vista en el browser puede ser muy útil también.

Hemos ido importando código desde otro proyecto de Simon, del cual ya hablé antes NotifyPropertyWeaver. Esto nos permitió de manera rápida brindar soporte para muchas plataformas:

  • .Net 3.5
  • .Net 3.5 Client Profile
  • .Net 4
  • .Net 4 Client Profile
  • Silverlight 3
  • Silverlight 4
  • Silverlight on Windows Phone 7

    Absolutamente todo lo que esta soportado hasta el momento funciona en las plataformas antes mencionadas y disponemos de una suite de tests que lo verifica cada vez. También estamos utilizando integración continua con TeamCity, en el sitio de codebetters para proyectos opensource.

    Al igual que NotifyPropertyWeaver;

    • para utilizarlo solo hará falta insertar una línea que llama a una tarea de msbuild en el archivo de proyecto. (implementado)
    • no se requiere dependencias para el deployment. Dado que el weaver automáticamente eliminará el atributo y la referencia. (parcialmente implementado)

    Estamos abiertos a cualquier tipo de sugerencia o aporte, ya sea por mail, twitter o lo que sea.

    Gracias y espero no haber Sonrisa


  • blog comments powered by Disqus
    • Categories

    • Archives