FluentIL – Parte 4 – Suporte para criação de tipos

Publicado em 02/05/2011

2


Olá pessoal, como estamos?

Já tem algum tempo que não escrevia nada nessa série. Então, hoje voltamos a ativa.

No post de hoje apresento as alterações que realizei em nosso projeto para suportar a criação de tipos dinamicamente.

Por favor, busque o código fonte atualizado em https://github.com/elemarjr/FluentIL.

Suportando a criação de tipos (sem qualquer implementação)

A primeira modificação que fiz no projeto foi incluir uma nova classe (chamada DynamicTypeInfo). Essa classe deverá prover a lógica para criação de tipos por Emitting.  Observe os testes que escrevi inicialmente:

[Test]
public void AsType_ReturnsTypeWithSpecifiedName()
{
    // arrange
    var dt = new DynamicTypeInfo("Foo");
    // act
    var t = dt.AsType;
    // assert
    t.Name.Should().Be("Foo");
}

[Test]
public void AsType_ReturnedTypeShouldBeInstantiableViaActivatorCreateInstance()
{
    // arrange
    var dt = new DynamicTypeInfo("Foo");
    // act
    var t = dt.AsType;
    var obj = Activator.CreateInstance(t);
    // assert
    obj.GetType().Should().Be(t);
}

Como os testes revelam, crio uma instância de DynamicTypeInfo passando o nome da classe que desejo instanciar e utilizo uma propriedade chamada AsType que deverá retornar um tipo correspondente. Além disso, escrevi um outro teste para a infraestrutura que utilizo para criação de tipos. Observe:

[Test]
public void TypeBuilder_InvokedTwoTimes_ReturnsSameValue()
{
    // arrange
    var dt = new DynamicTypeInfo("Foo");
    // act
    var t1 = dt.TypeBuilder;
    var t2 = dt.TypeBuilder;
    // assert
    t1.Should().Be(t2);
}

Como pode ver, a criação de tipos utiliza um TypeBuilder para emitir tipos. Estou garantindo que cada DynamicTypeInfo possua apenas um TypeBuilder associado. Eis o código gerado para DynamicTypeInfo:

public class DynamicTypeInfo
{
    public string TypeName { get; private set; }

    public DynamicTypeInfo(string typeName)
    {
        this.TypeName = typeName;
    }

    TypeBuilder TypeBuilderField = null;
    public TypeBuilder TypeBuilder
    {
        get
        {
            if (this.TypeBuilderField == null)
            {
                var assemblyName = new AssemblyName(
                    string.Format("__assembly__{0}", DateTime.Now.Millisecond)
                    );

                var assemblyBuilder = Thread.GetDomain().DefineDynamicAssembly(
                    assemblyName,
                    AssemblyBuilderAccess.RunAndSave
                    );

                var moduleBuilder = assemblyBuilder.DefineDynamicModule(
                    assemblyBuilder.GetName().Name,
                    false
                    );

                this.TypeBuilderField = moduleBuilder.DefineType(this.TypeName,
                    TypeAttributes.Public |
                    TypeAttributes.Class |
                    TypeAttributes.AutoClass |
                    TypeAttributes.AnsiClass |
                    TypeAttributes.BeforeFieldInit |
                    TypeAttributes.AutoLayout,
                    typeof(object),
                    _interfaces.ToArray()
                    );
            }

            return this.TypeBuilderField;
        }
    }

    List<Type> _interfaces = new List<Type>();
    public DynamicTypeInfo Implements<TInterface>()
    {
        _interfaces.Add(typeof(TInterface));
        return this;
    }

    public DynamicMethodInfo WithMethod(string methodName)
    {
        return new DynamicMethodInfo(this, methodName);
    }

    public Type AsType
    {
        get
        {
            return this.TypeBuilder.CreateType();
        }
    }
}

Testes passaram. Obviamente, você está percebendo a presença de outros métodos nessa classe. O método Implements, provê uma interface fluente para “informar” que uma interface deve ser implementada. O método WithMethod cria uma instância (modificada) da classe DynamicMethodInfo que apresentei nos posts anteriores.

Tornando DynamicMethodInfo/DynamicMethodBody compatível com DynamicTypeInfo

Até aqui, criávamos métodos dinâmicos, independentes de um tipo. Temos que modificar isso. A primeira ação é permitir que um DynamicTypeInfo seja informado. Além disso, todo método deverá possuir um nome. Observe o novo construtor:

public DynamicMethodInfo(
    DynamicTypeInfo dynamicTypeInfo,
    string methodName
    )
    : this()
{
    this.DynamicTypeInfo = dynamicTypeInfo;
    this.MethodName = methodName;
}

public DynamicMethodInfo()
{
    this.Body = new DynamicMethodBody(this);
    this.MethodName = "DynMethod";
}

Bonito! Criei uma propriedade para armazenar o nome do método. Outra coisa importante é alterar o acesso ao ILGenerator para considerar as duas vias possívels: DynamicMethod e MethodInfo. Observe:

DynamicMethod _result;
public DynamicMethod AsDynamicMethod
{
    get
    {
        if (this.DynamicTypeInfo != null)
            throw new InvalidOperationException();

        if (_result == null)
        {
            var parameterTypes = _Parameters.Select(p => p.Type)
            .ToArray();
            _result = new DynamicMethod(
                this.MethodName,
                ReturnType,
                parameterTypes
                );

            var ilgen = _result.GetILGenerator();
            foreach (var variable in this.Variables)
                ilgen.DeclareLocal(variable.Type);
        }
        return _result;
    }
}

internal DynamicTypeInfo DynamicTypeInfo { get; private set; }

MethodBuilder methodBuilder = null;

public ILGenerator GetILGenerator()
{
    if (this.DynamicTypeInfo == null)
        return this.AsDynamicMethod.GetILGenerator();
    else
    {
        if (this.methodBuilder == null)
        {
            var parameterTypes = _Parameters.Select(p => p.Type)
                .ToArray();

            methodBuilder = this.DynamicTypeInfo.TypeBuilder.DefineMethod(
                this.MethodName,
                MethodAttributes.Public | MethodAttributes.Virtual,
                CallingConventions.HasThis,
                ReturnType,
                parameterTypes);
        }
        return this.methodBuilder.GetILGenerator();
    }
}

Primeiro, perceba que nego o retorno para AsDynamicMethod quando há um DynamicTypeInfo. Além disso, implemento GetILGenerator para prover o ILGenerator adequado conforme o contexto. Esse método passa a ser usado em todo os Emittings.

Outra modificação relevante é no helper LdArg. Como métodos de instância possuem “this” no índice 0, modifiquei o helper LdArg que recebe um “nome”. Observe;

public DynamicMethodBody Ldarg(params string[] args)
{
    var parameters = _Info.Parameters.ToArray();
    uint offset = (uint) (_Info.DynamicTypeInfo != null ? 1 : 0);

    foreach (var arg in args)
        for (uint i = 0; i < parameters.Length; i++)
            if (parameters[i].Name == arg)
                Ldarg(i + offset);

    return this;
}

Basicamente, verifico se há um “DynamicTypeInfo” especificado. Em caso positivo, adiciono um offset de 1.

Também criei uma propriedade em DynamicMethodBody chamada AsType para melhorar a fluência. Essa propriedade “acessa” a propriedade AsType do DynamicTypeInfo associado. Nesse mesmo propósito, adicionei um método NewMethod. Observe:

public Type AsType
{
    get
    {
        return _Info.DynamicTypeInfo.AsType;
    }
}

public DynamicMethodInfo WithMethod(string methodName)
{
    return this._Info.DynamicTypeInfo.WithMethod(methodName);
}

Onde chegamos?!

FluentIL avançou um bocado. Agora temos condições de definir facilmente novos tipos, bem como métodos associados. Observe:

[Test]
public void CreateTypeWithOneMethod()
{
    // arrange
    var t = IL.NewType("One")
        .WithMethod("Method").Returns(typeof(void))
            .Ret()
        .AsType;

    var instance = Activator.CreateInstance(t);

    instance.GetType().Name.Should().Be("One");
    instance.GetType().GetMethod("Method").Should().Not.Be(null);
}

[Test]
public void TwoPlusTwoWithNamedParameters()
{
    var t = IL.NewType()
        .WithMethod("Add")
            .WithParameter(typeof(int), "a")
            .WithParameter(typeof(int), "b")
            .Returns(typeof(int))
            .Ldarg("a")
            .Ldarg("b")
            .Add()
            .Ret()
        .AsType;

    var instance = Activator.CreateInstance(t);

    var mi = instance.GetType().GetMethod("Add");
    var result = mi.Invoke(instance, new object[] { 2, 2 });
    result.Should().Be(4);
}

[Test]
public void ClassWithMultAndDivMethods()
{

    var t = IL.NewType()
        .WithMethod("Add")
            .WithParameter(typeof(int), "a")
            .WithParameter(typeof(int), "b")
            .Returns(typeof(int))
            .Ldarg("a")
            .Ldarg("b")
            .Add()
            .Ret()
        .WithMethod("Mul")
            .WithParameter(typeof(int), "a")
            .WithParameter(typeof(int), "b")
            .Returns(typeof(int))
            .Ldarg("a")
            .Ldarg("b")
            .Mul()
            .Ret()
        .AsType;

    var instance = Activator.CreateInstance(t);

    var miAdd = instance.GetType().GetMethod("Add");
    var result = miAdd.Invoke(instance, new object[] { 2, 3 });

    var miMult = instance.GetType().GetMethod("Mul");
    result = miMult.Invoke(instance, new object[] { 2, 3 });
    result.Should().Be(6);
}

Bonito, nossa interface já permite a geração de tipos, on-the-fly, com alguma elegância e expressividade (considerando que estamos usando IL).

Consideremos agora uma interface simples:

public interface IFoo
{
    int Add(int a, int b);
    int Mul(int a, int b);
}

Perfeito. Escrevendo um teste para mostrar nosso suporte a interfaces …

[Test]
public void ClassWithMultAndDivMethodsImplementingInterface()
{

    var t = IL.NewType()
        .Implements<IFoo>()
        .WithMethod("Add")
            .WithParameter(typeof(int), "a")
            .WithParameter(typeof(int), "b")
            .Returns(typeof(int))
            .Ldarg("a")
            .Ldarg("b")
            .Add()
            .Ret()
        .WithMethod("Mul")
            .WithParameter(typeof(int), "a")
            .WithParameter(typeof(int), "b")
            .Returns(typeof(int))
            .Ldarg("a")
            .Ldarg("b")
            .Mul()
            .Ret()
        .AsType;

    var instance = (IFoo)Activator.CreateInstance(t);

    instance.Add(2, 3).Should().Be(5);
    instance.Mul(2, 3).Should().Be(6);
}

Lindo e tudo funciona.

Agora sim… por hoje, era isso

Smiley piscando

Etiquetado:,
Publicado em: Post