Desenvolvimento - Visual Basic .NET

Geração de Assemblies em Run-Time

Todo o código de qualquer linguagem .NET é convertido para a forma de bytecodes chamada IL (ou MSIL ou ainda CIL). IL significa Intermediate Language - linguagem intermediária...

por Washington Coutinho Corrêa Junior



Todo o código de qualquer linguagem .NET é convertido para a forma de bytecodes chamada IL (ou MSIL ou ainda CIL).

IL significa Intermediate Language - linguagem intermediária. Note que existe o IL e o IL Assembly, sendo coisas distintas. O primeiro é formado por códigos binários, enquanto que o segundo é sob a forma de texto puro (como qualquer outra linguagem).

Pode-se programar diretamente em IL Assembly e depois compilar o código através do compilador ilasm.exe. O contrário também pode ser feito, ou seja, possuindo um código compilado ver o seu respectivo código IL Assembly, através do aplicativo ildasm.exe. Todavia, a .NET Framework fornece classes (em System.Reflection.Emit) que permitem a geração do código IL (não o fonte, mas sim o já compilado) de forma bastante confortável.

O que isso significa? Que apesar de ser estaticamente compilada, a .NET pode ser tão poderosa quanto uma linguagem de natureza dinâmica. Os assemblies gerados podem ser carregados durante a execução (mesmo imediatamente após terem sido gerados), lembrando que um assembly é um bloco de código compilado.

Com a possibilidade de gerar novos tipos (Types) dentro desses assemblies, tem-se então a possibilidade de carregar dinamicamente esses tipos (classes) em tempo de execução. Com a relativa simplicidade de criação desses assemblies oferecida pela framework e os opcodes disponíveis na IL tem-se a capacidade de criar novas linguagens para a .NET Framework com uma das já existentes (por exemplo, VB.NET ou C#).

A outra possibilidade é compilar o código de uma das linguagens existentes, sem ter que usar o compilador propriamente dito. Ou seja, nada impede a criação de "scripts" em C# ou VB.NET, que seriam executados e compilados na hora, pelo seu programa, permitindo ainda a modificação livre deles, além da recompilação e reexecução.

Outro detalhe muito interessante é que a geração e o carregamento dos assemblies não precisa ficar restrito apenas a execução no momento, podendo ser gravado em disco. Sim, sob a forma de um executável (.exe) ou de uma biblioteca (.dll), e creio eu que esta seja uma característica desejável para quem quer criar sua própria linguagem de programação, ainda mais com a possibilidade de utilização das bibliotecas da CLR. Aliás, é justamente esse ponto que explorarei no código presente neste artigo. Futuramente farei outro explicando a criação e execução dos assemblies em tempo de execução, como se fossem scripts.

O código abaixo, em VB.NET, gera um executável chamado "teste.exe" na pasta "c:\programa" (tenha certeza de que esta pasta existe, pois o código não verificará isso). Este executável simplesmente escreve "teste" no console. Ele tem um módulo, uma classe e uma rotina chamada Main(), que é o ponto de entrada para a execução. Sua versão em C# segue logo abaixo.

Código em VB.NET:
Imports System
Imports System.Reflection
Imports System.Reflection.Emit

Public Module Teste
Public Sub Main()
Dim nome As New AssemblyName()
Dim domínio As AppDomain
Dim construtor As AssemblyBuilder
Dim módulo As ModuleBuilder
Dim classe As TypeBuilder
Dim método As MethodBuilder
Dim il As ILGenerator

nome.Name =
"teste_assembly"
Dim arquivoEXE As String = "teste3.exe"
Dim pastaDestino As String = "C:\programa"

"Obtendo a thread atual
domínio = System.Threading.Thread.GetDomain()

"Construtor para assemblies dinâmicos
construtor = domínio.DefineDynamicAssembly(nome, AssemblyBuilderAccess.Save, pastaDestino)

"Criando o módulo
módulo = construtor.DefineDynamicModule(
"teste_module", arquivoEXE)

"Definindo a classe (não criando... isso está mais abaixo)
classe = módulo.DefineType(
"Teste")

"Parâmetros do método (no caso, nenhum)
Dim parâmetros() As Type

"Declarando o método Main (com tipo de retorno como void e os parâmetros acima (que, no caso, não tem))
método = classe.DefineMethod(
"Main", MethodAttributes.Public Or MethodAttributes.Static, System.Type.GetType("void"), parâmetros)

"Inicializando o gerador de IL
il = método.GetILGenerator()

"Emitindo os códigos que formarão o método
il.EmitWriteLine(
"teste") "Escrever "teste" na tela: o mesmo que System.Console.Writeline("teste")
il.Emit(OpCodes.Ret)
"Opcode para o return (é necessário!)

"Criando a classe
classe.CreateType()

"Definindo o ponto de entrada do assembly (é necessário para a formação de EXEs; para DLLs não o é)
construtor.SetEntryPoint(método)

"Salvando o assembly
construtor.Save(arquivoEXE)
End Sub
End Module

Código em C#:
using System;
using System.Reflection;
using System.Reflection.Emit;

public class Teste {
public static void Main() {
AssemblyName nome =
new AssemblyName();
AppDomain domínio;
AssemblyBuilder construtor;
ModuleBuilder módulo;
TypeBuilder classe;
MethodBuilder método;
ILGenerator il;

nome.Name =
"teste_assembly";
string arquivoEXE = "teste2.exe";
string pastaDestino = "C:/programa";

//Obtendo a thread atual
domínio = System.Threading.Thread.GetDomain();

//Construtor para assemblies dinâmicos
construtor = domínio.DefineDynamicAssembly(nome, AssemblyBuilderAccess.Save, pastaDestino);

//Criando o módulo
módulo = construtor.DefineDynamicModule(
"teste_module", arquivoEXE);

//Definindo a classe (não criando... isso está mais abaixo)
classe = módulo.DefineType(
"Teste");

//Parâmetros do método
Type[] parâmetros = null;

//Declarando o método Main (com tipo de retorno como void e os parâmetros acima (que, no caso, não tem))
método = classe.DefineMethod(
"Main", MethodAttributes.Public | MethodAttributes.Static, System.Type.GetType("void"), parâmetros);

//Inicializando o gerador de IL
il = método.GetILGenerator();

//Emitindo os códigos que formarão o método
il.EmitWriteLine(
"teste"); //Escrever "teste" na tela: o mesmo que System.Console.Writeline("teste")
il.Emit(OpCodes.Ret);
//Opcode para o return (é necessário!)

//Criando a classe
classe.CreateType();

//Definindo o ponto de entrada do assembly (é necessário para a formação de EXEs; para DLLs não o é)
construtor.SetEntryPoint(método);

//Salvando o assembly
construtor.Save(arquivoEXE);
}
}

O principal atrativo desse código é o objeto "il", que é o responsável por gerar as instruções a serem executadas. Através do método Emit(), podemos especificar os opcodes da IL a serem executados. No caso acima, o EmitWriteline() já faz automaticamente o trabalho de dois Emit()s diferentes, como um atalho para (em VB.NET):

Código em VB.NET:
il.Emit(Emit.OpCodes.Ldstr, "teste") "Carregando a string "teste" na pilha
Dim métodoWriteline As MethodInfo = GetType(Console).GetMethod("WriteLine", New Type() {GetType(String)})
il.Emit(Emit.OpCodes.Call, métodoWriteline)
"Fazendo a chamada do método WriteLine

Note que foi necessário obter a assinatura completa do método WriteLine(). Através do EmitWriteline() isso já é obtido automaticamente.

Assim como usei o AssemblyBuilder, ModuleBuilder, TypeBuilder e o MethodBuilder para criar, respectivamente, o assembly, o módulo, a classe (tipo) e um método, existem outros builders (como para os atributos, por exemplo). Daí em diante, basta explorar as possibilidades do namespace System.Reflection.Emit. Um ponto de partida é dar uma olhada no código-fonte do MyC, um exemplo de um compilador C que acompanha o SDK da Framework. Foi a partir dele e da criação de um simples executável em VB.NET (cujo código IL Assembly pude investigar através do ildasm) que cheguei ao código acima. O ideal seria estudar a IL Assembly separadamente, como uma linguagem a parte, mas sempre que necessário, pode-se fazer o desejado em C# ou VB.NET (ou qualquer outra linguagem .NET familiar), e dar uma olhadela no IL Assembly (através do ildasm) do mesmo a fim de saber quais os opcodes usados para tal tarefa.

Quaisquer erros, sugestões ou dúvidas são mui bem apreciadas por parte do autor, contactável através do e-mail: washingtonj@openlink.com.br. Happy dotNettin"!

Washington Coutinho Corrêa Junior

Washington Coutinho Corrêa Junior