Introdução a Assemblies Dinâmicos

José Antonio Leal de Farias

 

Introdução

Eu tenho muita sorte de poder trabalhar em projetos “não muito comuns” e um deles me levou a construção de um compilador .NET há algum tempo. No início no projeto eu tinha receio da complexidade, mas descobri que gerar Assemblies é uma tarefa bastante simples e trás muitos benefícios, não só para os que produzem ferramentas de desenvolvimento.

Neste artigo mostrarei um pouco como criar Assemblies dinamicamente em .NET.

Um pouco de teoria

O Assembly é a unidade básica de deployment na plataforma .NET. Ele contém tipos, recursos, código, etc. Os Assemblies podem ser classificados em dois tipos: estáticos e dinâmicos. Os Assemblies estáticos são os Assemblies normais construídos pelos compiladores .NET e outras ferramentas como o ‘al’ (Assembly Linker). Uma vez construídos os Assemblies estáticos eles não podem ser alterados, embora possamos carregá-los dinamicamente ou criar tipos a partir deles, mas sua natureza é ainda estática.

Os Assemblies dinâmicos são construídos em tempo de execução e podemos acrescentar os conteúdos dinamicamente ou alterar os já existentes. Podemos também emitir o código IL diretamente no Assembly, normalmente isto acontece na memória, mas no fim nós podemos também persistir no Assembly dinâmico da mesma forma.

O sistema .NET fornece todos esses serviços no namespace System.Reflection.Emit. O System.Reflection.Emit contém várias classes que fornecem acesso ao código de emissão. Poe exemplo, a classe TypeBuilder é usada para definir dinamicamente o tipo; AssemblyBuilder é usado para construir e persistir no Assembly dinâmico, e assim por diante.

Assemblies dinâmicos em ação

Vamos ver um exemplo no qual o iremos gerar o seguinte tipo através das classes System.Reflection.Emit.

 

class MyDynamicType {       public MyDynamicType()       {             Console.WriteLine("Constructor Called");       }       public void MyMethod()       {             Console.WriteLine("Method Called");       } }

 

O primeiro passo é criar a instância dinâmica do Assembly, isto pode ser feito através do AppDomain.Define.Dynamic Assembly ( ) :

 

AssemblyName asmname = new AssemblyName(); asmname.Name = "DynAsm.dll"; // Assemblies dinâmicos suportam execução e persistência dinâmicos AssemblyBuilder asmBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly(asmname, AssemblyBuilderAccess.RunAndSave);

 

A classe AssemblyName fornece o acesso para várias características do Assembly como KeyPair e Culture.

O segundo passo é criar o módulo dinâmico que é responsável por criar tipos dinâmicos.

 

// Nome do módulo e do assembly ModuleBuilder modBuilder = asmBuilder.DefineDynamicModule("MyModule", "MyAsm.dll"); // Definindo o tipo público TypeBuilder typeBuilder = modBuilder.DefineType("MyDynamicType", TypeAttributes.Public);

 

Após conseguirmos a referência do TypeBuilder nós podemos adicionar membros a ele:

 

// Cria o construtor sem argumentos ConstructorBuilder defctor = typeBuilder.DefineConstructor(MethodAttributes.Public, CallingConventions.Standard, new Type[0]);

 

Vem então a importante tarefa de geração do real código IL, isto pode ser feito através da interface ILGenerator (que emite o código IL real), como abaixo:

 

ILGenerator ctorIlGen = defctor.GetILGenerator(); // Console.WriteLine(...) ctorIlGen.EmitWriteLine("Constructor Called"); // Gera o comando ‘Return’ ctorIlGen.Emit(OpCodes.Ret);

 

A enumeração OpCodes representa os opcodes do IL, tais como a chamada de um método, a carga de uma string, retorno de um método, etc. O conhecimento detalhado de todos os opcodes pode ser encontrado no site da MSDN e na especificação do .NET Framework.

O processo acima permanecerá o mesmo para todos os métodos. Após emitir o IL, é hora de emitir o código IL e dinamicamente chamá-lo. O typeBuilder CreateType ( ) é responsável pela emissão do código IL  e retorna o tipo Object. Veja o exemplo a seguir:

 

Type t = typeBuilder.CreateType(); // Cria a instância do objeto a partir do tipo object obj = Activator.CreateInstance(t); // Salva o assembly no disco asmBuilder.Save("MyAsm.dll");

 

Resumindo, o código completo do processo é:

 

AssemblyName name = new AssemblyName(); name.Name = "MyAsm";   AssemblyBuilder asmBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly(name, AssemblyBuilderAccess.RunAndSave);   // Obtém o módulo construtor ModuleBuilder modBuilder = asmBuilder.DefineDynamicModule("MyModule", "MyAsm.dll");       // Obtem o construtor do tipo       TypeBuilder typeBuilder = modBuilder.DefineType("MyDynamicType", TypeAttributes.Public);             // Cria o construtor sem parâmetros             ConstructorBuilder defctor = typeBuilder.DefineConstructor(MethodAttributes.Public, CallingConventions.Standard, new Type[0]);                   ILGenerator ctorIlGen = defctor.GetILGenerator();                         ctorIlGen.EmitWriteLine("Constructor Called");                         ctorIlGen.Emit(OpCodes.Ret);                         MethodBuilder mBuilder = typeBuilder.DefineMethod("MyMethod", MethodAttributes.Public, typeof(void), new Type[0]);                   ILGenerator ilgen = mBuilder.GetILGenerator();                         ilgen.EmitWriteLine("Method Called");                         ilgen.Emit(OpCodes.Ret);       Type t = typeBuilder.CreateType();   object obj = Activator.CreateInstance(t); // Chama obj.mymethod() t.InvokeMember("MyMethod", BindingFlags.InvokeMethod, null, obj, null); asmBuilder.Save("MyAsm.dll");

 

Uma vez que o Assembly é gerado você pode usá-lo em outras aplicações como um assembly comum do .NET.

Vantagens dos Assemblies dinâmicos

São muitas, mas posso destacar:

 

  • Os Assemblies dinâmicos fornecem grande flexibilidade para quem os desenvolve.
  • Basicamente usado pelas ferramentas de geração de Assembly, script de motores e fornecedores de compiladores.
  • Podemos criar novas linguagens .NET com especificações e sintaxe diferentes.
  • Diretamente emitir o IL significa que estamos evitando as especificações e limitações das linguagems atuais e gerando código IL da forma mais eficiente possível.
  • Gerar tipos e códigos quando necessário, os quais podem resultar um download mais eficiente dos assemblies localizados em uma rede.

Conclusão

Essa forma de geração de código abre muitas possibilidades. Em muitas situações onde seja necessário “interpretar” código .NET para customizar uma aplicação dinamicamente, por exemplo, uma solução de geração de código final apresenta inumeráveis vantagens. E se é tão simples, porque não usa-las?