image002[1]

Intermediate Language, o .NET por debaixo do capô 1


Todas as linguagens suportadas pelo .NET quando compiladas passam pela fase de transformação em IL(Intermediate Language). É devido a esse motivo que podemos escrever uma biblioteca usando F# ou VB e usar ela em uma biblioteca codificada em C#. É importante que um desenvolvedor da plataforma .NET conheça pelo menos um básico de IL, inclusive para criar código em tempo de execução pode ser necessário conhecer IL.

O código em C#

Para esse exemplo vou mostrar um código simples desenvolvido em C# e veremos como é o mesmo código em IL.

[csharp]class Program
{
static void Main(string[] args)
{
Console.WriteLine(MaiorNumero());
Console.Read();
}
public static int MaiorNumero()
{
Console.WriteLine("Digite o primeiro numero: ");
var a = int.Parse(Console.ReadLine());
Console.WriteLine("Digite o segundo numero: ");
var b = int.Parse(Console.ReadLine());
return a > b ? a : b;
}
}
[/csharp]

Código bem simples, seu objetivo é receber dois números do usuário e retornar qual é o maior, se os dois forem iguais ele retorna o ultimo número, caso os dois sejam iguais, o mesmo número é retornado.

Executando o código a seguir temos:

Figura 1: Execução da aplicação

Gerando o método MaiorNumero em tempo de execução usando IL

class Program
{
    static void Main(string[] args)
    {
        var maiorNumero = CriaMaiorNumero();
        Console.WriteLine(maiorNumero());
        Console.Read();
    }

    private static Func CriaMaiorNumero()
    {
        var tipoString = typeof(string);
        var tipoInt = typeof(int);
        var tipoConsole = typeof(Console);

        var writeLine = tipoConsole.GetMethod("WriteLine", new[] { tipoString });
        var readLine = tipoConsole.GetMethod("ReadLine", new Type[] { });
        var parse = typeof(Int32).GetMethod("Parse", new[] { tipoString });

        var qualEhOMaior = new DynamicMethod("MaiorNumero", tipoInt, new Type[] { });

        var ilGen = qualEhOMaior.GetILGenerator();

        ilGen.DeclareLocal(tipoInt);
        ilGen.DeclareLocal(tipoInt);

        var IL_0034 = ilGen.DefineLabel();
        var IL_0035 = ilGen.DefineLabel();

        ilGen.Emit(OpCodes.Ldstr, "Digite o primeiro número: ");
        ilGen.Emit(OpCodes.Call, writeLine);
        ilGen.Emit(OpCodes.Call, readLine);
        ilGen.Emit(OpCodes.Call, parse);
        ilGen.Emit(OpCodes.Stloc_0);
        ilGen.Emit(OpCodes.Ldstr, "Digite o segundo número: ");
        ilGen.Emit(OpCodes.Call, writeLine);
        ilGen.Emit(OpCodes.Call, readLine);
        ilGen.Emit(OpCodes.Call, parse);
        ilGen.Emit(OpCodes.Stloc_1);
        ilGen.Emit(OpCodes.Ldloc_0); ilGen.Emit(OpCodes.Ldloc_1);
        ilGen.Emit(OpCodes.Bgt_S, IL_0034);
        ilGen.Emit(OpCodes.Ldloc_1);
        ilGen.Emit(OpCodes.Br_S, IL_0035);
        ilGen.MarkLabel(IL_0034);
        ilGen.Emit(OpCodes.Ldloc_0);
        ilGen.MarkLabel(IL_0035);
        ilGen.Emit(OpCodes.Ret);

        var tipoDaFuncao = typeof (Func);
        return qualEhOMaior.CreateDelegate(tipoDaFuncao) as Func;
    }
}

Agora iremos gerar o mesmo código só que em tempo de execução, ou seja, vamos gerar o método MaiorNumero enquanto nossa aplicação estiver rodando.

Sim, é muito mais código. Para gerar um método em tempo de execução usamos a classe DynamicMethod, ao instanciar um DynamicMethod passamos 3 parâmetros(existe outras sobrecargas do construtor), primeiro é o nome do método, segundo o tipo do seu retorno e o terceiro é um array de Type passando qual é o tipo de cada parametro. Como nosso método não recebe nenhum parametro passo um array vazio, então o DynamicMethod entende que não temos nenhum parametro, caso eu não quisesse que meu método tivesse retorno poderia passar null no segundo parametro ou typeof(void).

Para escrever o comportamento do método, usamos o método GetILGenerator para nos retornar o “construtor” do método, com ele podemos ir dizendo sequencialmente o que o método deve fazer.

No nosso método em C# nos temos 2 variáveis, são elas “a” e “b” que foram definidas ao decorrer do método, mas em IL precisamos defini-las logo no inicio, então é isso que eu faço nas linhas ilGen.DeclareLocal(tipoInt), ou seja, declarei duas variáveis locais do tipo inteiro.

A trés linhas seguintes usamos para criar marcos no nosso código, como em assembly, IL não possui condicionais como if,else, switch e nem blocos de repetição como for, while, é possível que vocês já tenham ouvido falar em goto, e é isso que de fato acontece, nos informamos ao nosso código que ele deve avançar até aquele marco ou retroceder até o mesmo.

Apos a definição dos labels(marcos) começo a escrita do comportamento do método, usando o método Emit do ilGen. Esse método(Emit) pode receber 1 ou 2 parâmetros, e isso vais depender do primeiro parâmetro.

O primeiro Emit ele recebe como primeiro parametro o OpCodes.Ldstr, a classe OpCodes contem o tipo de instrução que iremos realizar, a intrução “Ldstr” serve para carregar uma string para minha pilha de memória. É bem visível que temos uma abreviação de Load string, e é claro o segundo parâmetro é qual string iremos carregar, esse abreviação tambem se aplica aos outros comandos como veremos a seguir.

O emit apos o carregamento da string é do tipo Call(chamada de método) e para isso precisamos informar qual método queremos chamar, para isso precisamos passar um objeto do tipo MethodInfo que pode ser obtido via Reflection como fazemos na linha var writeLine = tipoConsole.GetMethod(“WriteLine”, new[] { tipoString }); . O método writeline que carregamos recebe um parâmetro do tipo string, que é exatamente o objeto que temos na nossa mémoria temporária, logo ao chama o writeline ele vai usar como parâmetro a string carregada na instrução acima.

O terceiro emit é a chamada ao método ReadLine do Console, como ele não recebe nenhum parâmetro não precisei carregar nada, mas ele retorna um valor, e esse valor é adicionado na nossa memória temporária, quando chamamos algum método que consome a mémoria temporaria, ele retira o mesmo dela, então quando foi chamado o WriteLine que usou a string que estava na memória temporária ela foi removida, limpado a memória, que agora é composta do retorno do ReadLine.

Proximo passo é fazer com que o dado recebido pelo usuario va para nossa variavel, mas como temos uma variavel do tipo inteiro precisamos converte-la, e é isso que acontece com a quarta instrução de emit, onde mais uma vez fazemos uma chamada de método ao método Parse da classe int, não vou me preocupar com valores que não podem se converter para inteiro, se você entrar com um valor que não é possivel converter o problema vai parar.

Seguindo adiante a quinta instrução emit usamos o tipo Stloc que é uma abreviação de Set local, se lembra que definimos as variaveis locais? E por que a intrução é Stloc_0, o 0 é o indice da lista de variveis que temos, também poderiamos usar ilGen.Emit(OpCodes.Stloc, 0), ou seja, passando o indice no segundo parametro, mas a classe OpCodes disponibiliza alguns atalhos para os primeiros indices.

No segundo trecho de codigo fazemos o mesmo que o primeiro, a diferença é que pedimos o segundo numero, e alocamos o resultado na segunda variavel.

A partir da instrução ilGen.Emit(OpCodes.Ldloc_0); até a ilGen.Emit(OpCodes.Ret); é referente a nossa espressão ternaria “a > b ? a : b“.

O OpCode Ldloc é a abreviação de Load local, ou seja, carregar uma variável local, carregamos os valores das duas primeiras variaveis. Para tratar das condicionais a classe OpCodes nos da uma serie de recursos, são eles:

  1. beq, beq.s, beq.un, beq.un.s
  2. bne, bne.s, bne.un, bne.un.s
  3. bge, bge.s, bge.un, bge.un.s
  4. bgt, bgt.s, bgt.un, bgt.un.s
  5. ble, ble.s, ble.un, ble.un.s
  6. blt, blt.s, blt.un, blt.un.s.

As abreaviaços respectivamente:

  1. Bequal => valor == valor2
  2. Bnequal => valor != valor2
  3. Bgreater equal => valor >= valor2
  4. Bgreater => valor > valor2
  5. Bless equal => valor <= valor2
  6. Bless  => valor < valor2

Explicado os recursos de desvio condicional apos carregas as duas variáveis o que fazemos é testar se a primeira variável é maior que a segundo(Bgt), sendo positiva essa resposta nos informamos vá até o marco IL_0034, esse nome eu posso definir da maneira como quiser, na verdade ele é o nome da variável que você deu ao definir o marco, la nas primeiras instruções, logo apos declarar as variáveis. Caso contrario siga para proxima instrução que é Ldloc_1, que carrega o valor da segunda variável para a memoria temporária, seguindo para a próxima instrução, Br_s indicamos que queremos ir para o marco informado, que nesse caso é o IL_0035.

O método MarkLabel serve para definirmos que a próxima instrução Emit pode ser localizada pelo marco indicado, nesse caso dizemos que o marco IL_0034 vai carregar o valor da variável 2. Novamente definimos o um marco, que é o IL_0035 que é reponsavel por retorna, ou seja, o OpCodes.Ret é o return do método que retorna o valor da minha memoria temporária.

Recapitulando de forma simples, nas linhas onde faço ilGen.Emit(OpCodes.Ldloc_0); ilGen.Emit(OpCodes.Ldloc_1);(carrego os valores das variáveis 1 e 2 para memoria temporária) verificamos a primeira é maior que a segunda? Se for pule até o marco IL_0034 que carrega o valor da variável 1 e e prossegue retornando o valor da variável 1. Mas se o valor da variável 1 não for maior que a variável 2, sigo para próxima instrução que carrega o valor da variável 2 para memoria temporária e pula para o retorno, retornando assim o valor da variável 2.

Conclusão

Vimos que em IL precisamos definir as variaveis no inicio do método, também vimos que para chamar método precisamos informar seu caminho completo, usando o objeto do tipo MethodInfo, e que ao chamar métodos recebem parametros a nossa memoria temporaria é consumida, podendo inclusive ser zerada, que é o que acontece quando usamos os desvios condicionas como Bgt, que usa 2 valores da memoria temporária. Foi mostrado também que para “pularmos” para certos trecho do método devemos definir os marcos. No fim de tudo, esse código tem o mesmo resultado que o primeiro.

Para saber mais:

Lista de OpCodes e uma breve descrição sobre cada um


sobre Alberto Monteiro

Desenvolvedor no Grupo Fortes, cuja principal área de conhecimento são em tecnologias Microsoft, como Windows Forms / Services, WPF, ASP.NET(MVC/WEB API), Windows Phone, EF. Gosta de sopa de letrinhas(SOLID, DDD, TDD, BDD, IoC, SoC, UoW), possui aplicações de Windows Phone publicadas no marketplace, já contribuiu no jQuery UI. Atualmente trabalha com ASP.NET MVC / Web API, Windows Azure, Amazon AWS, jQuery/UI, Knockout, EF, Ninject, AutoMapper, Restfulie, SignalR, KendoUI.