Mês: julho 2010

The Joel Test

Um amigo postou internamente aqui o link para o “The Joel Test”, do Joel Spolsky. Eu realmente não conheço a fundo o trabalho dele, mas achei muito boa a métrica para saber a maturidade da equipe de desenvolvimento em 12 passos.

São eles:

  • Você usa controle de versão?
  • Você consegue fazer um build em uma etapa?
  • Você faz builds diários?
  • Você tem um banco de dados de bugs?
  • Você corrige os bugs antes de escrever código novo?
  • Você tem uma agenda atualizada?
  • Você tem uma especificação?
  • Os programadores possuem condições silenciosas de trabalho?
  • Você tem as melhores ferramentas que o dinheiro pode comprar?
  • Você tem testers?
  • Seus candidatos escrevem código em suas entrevistas?
  • Você faz teste de usabilidade de corredor?

Achei fantástico pela simplicidade. Doze etapas e resume tudo.

O original está em http://www.joelonsoftware.com/articles/fog0000000043.html

Acessando DLL’s não gerenciadas em .Net – Parte 2

Objetivo

Na primeira parte (Acessando DLL’s Não Gerenciadas No .Net – Parte 1), falamos um pouquinho sobre como acessar DLL’s não gerenciadas em .Net.

Vamos falar um pouco mais agora de como carregar DLL’s dinamicamente e para concluir essa idéia, vamos precisar entender também como compatibilizar delegates e function pointers.

Carregando a DLL Dinamicamente

O carregamento dinâmico de DLL’s pelo Windows pode ser realizado através das funções LoadLibrary (LoadLibrary Function) e FreeLibrary (FreeLibrary Function).

Quando se executa LoadLibrary, deve ser passado o caminho da DLL a ser carregada. A função retorna um handle da DLL. O handle pode ser entendido como um identificador. Sempre que tivermos que fazer algo com essa DLL, temos que passar de volta o handle.

A FreeLibrary descarrega a DLL. Passamos o handle e ela é liberada da memória.

Vimos na primeira parte, que mesmo quando anotamos o método no .Net com [DllImport], o carregamento da DLL acontece de forma dinâmica, ou seja, ela só é carregada quando o método é executado. Então que vantagem temos de usar esse método?

A única vantagem que eu vejo é escolher o caminho da DLL. Usando o [DLLImport] temos que inevitavelmente seguir a regra de carregamento da DLL (diretório da aplicação, system32, path). Nesse caso, podemos carregar de qualquer lugar.

De qualquer forma, o trabalho vale a pena, já que vai nos ajudar a compatibilizar function pointers em C++.

Declarando as funções da API no .Net

As funções que vamos precisar para carregar a DLL dinamicamente são: LoadLibrary, FreeLibrary e GetProcAddress. A definição das chamadas dela na API do Windows são:


BOOL WINAPI FreeLibrary(
  __in  HMODULE hModule
);

HMODULE WINAPI LoadLibrary(
  __in  LPCTSTR lpFileName
);

FARPROC WINAPI GetProcAddress(
  __in  HMODULE hModule,
  __in  LPCSTR lpProcName
);

Vamos discutir um pouco a tipagem, para entendermos a forma que essas funções serão representadas no .Net.

O tipo BOOL (tudo maiúsculo) é definido pela API do Windows. A idéia é armazenar um true ou false, mas o tipo foi redefinido pois dependendo da versão do windows, pode ter uma representação interna diferente.

WINAPI é outra macro que define o calling convention da função (stdcall ou cdecl).

HMODULE é um handle para uma DLL. Internamente o handle é representado por um inteiro, o que facilita bastante nossa vida no C#.

LPCSTR é um ponteiro para uma string terminada em zero. Pode ser de um ou dois bytes dependendo da versão do Windows (na parte um discutimos um pouco isso).

Agora que já sabemos um pouco do tipo, vamos alterar o nosso CppInterop.cs (criado na parte um do tutorial), excluir nossas funções sum, otherSum e concat e adicionar as seguintes funções:

    [DllImport("kernel32.dll", EntryPoint = "LoadLibrary")]
    public static extern int LoadLibrary([MarshalAs(UnmanagedType.LPStr)] string lpLibFileName);

    [DllImport("kernel32.dll", EntryPoint = "GetProcAddress")]
    public static extern IntPtr GetProcAddress(int hModule, [MarshalAs(UnmanagedType.LPStr)] string lpProcName);

    [DllImport("kernel32.dll", EntryPoint = "FreeLibrary")]
    public static extern bool FreeLibrary(int hModule);

Aqui percebemos que o LPCSTR foi compatibilizado para um string (o Platform Invoke resolve isso), os handles para ints, e o BOOL para bool. O Platform Invoke resolve a maioria dos nossos problemas.

Alterando o código C#

Vamos arrastar para o nosso form um OpenDialog (que vai ganhar o nome openDialog1).

Vamos alterar agora o nosso CSharpApp e mudar o código do nosso button1 para:

      if (openFileDialog1.ShowDialog() != DialogResult.OK)
        return;

      int hModule = CppInterop.LoadLibrary(openFileDialog1.FileName);

      if (hModule <= 0)
        throw new Exception("DLL not loaded.");

      MessageBox.Show("DLL loaded");

      CppInterop.FreeLibrary(hModule);

Como percebemos, o código executa o diálogo para procurar um arquivo e você pode escolher qualquer arquivo.

Se escolher um arquivo que não é uma DLL, vai perceber que a função LoadLibrary retorna 0, e uma exceção é disparada.

Se escolher um arquivo DLL válido, ele vai ser carregado, a mensagem exibida e posteriormente descarregado. Se usarmos o Process Explorer para examinar a aplicação, conseguimos ver o carregamento e o descarregamento da DLL.

Chamando métodos através de DLL’s carregadas dinamicamente

Sempre que carregamos a DLL dinamicamente não “conhecemos” as funções da DLL. A única forma de conseguirmos interagir com ela é através da função GetProcAddress, que tem a seguinte chamada na API do Windows:


FARPROC WINAPI GetProcAddress(
  __in  HMODULE hModule,
  __in  LPCSTR lpProcName
);

Essa função recebe como parâmetro o handle da DLL e o nome da função, conforme exportada pela DLL (arquivo .def). O retorno “FARPROC” é um ponteiro para uma função (mais uma macro da API do Windows).

Esse conceito existe nas linguagens não-gerenciadas. Uma vez que você sabe o endereço na memória onde está a função, e redefine ela com o mesmo calling convention e parâmetros, é possível chamá-la.

Para isso, é necessário compatibilizar este function pointer, retornado pela GetProcAddress, com um delegate. Como dissemos anteriormente, o tipo que compatibiliza diretamente com qualquer ponteiro no C# é o IntPtr.

Para isso, vamos definir as mesmas funções da nossa SampleDLL como delegates. São elas:


  public delegate int SumDelegate(int a, int b);

  public delegate void OtherSumDelegate(int a, int b, ref int result);

  public delegate void ConcatDelegate([MarshalAsAttribute(UnmanagedType.LPWStr)]string src1,
      [MarshalAsAttribute(UnmanagedType.LPWStr)]string src2, IntPtr dest, int destSize);

Agora, no nosso código do button1, vamos fazer o seguinte:

      if (openFileDialog1.ShowDialog() != DialogResult.OK)
        return;

      int hModule = CppInterop.LoadLibrary(openFileDialog1.FileName);

      if (hModule <= 0)
        throw new Exception("DLL not loaded.");

      MessageBox.Show("DLL loaded");

      IntPtr sumPtr = CppInterop.GetProcAddress(hModule, "sum");
      SumDelegate sum = (SumDelegate)Marshal.GetDelegateForFunctionPointer(sumPtr, typeof(SumDelegate));
      int res = sum(10, 20);
      MessageBox.Show(res.ToString());

      IntPtr otherSumPtr = CppInterop.GetProcAddress(hModule, "otherSum");
      OtherSumDelegate otherSum = (OtherSumDelegate)Marshal.GetDelegateForFunctionPointer(otherSumPtr, typeof(OtherSumDelegate));
      otherSum(5, 2, ref res);
      MessageBox.Show(res.ToString());

      IntPtr concatPtr = CppInterop.GetProcAddress(hModule, "concat");
      ConcatDelegate concat = (ConcatDelegate)Marshal.GetDelegateForFunctionPointer(concatPtr, typeof(ConcatDelegate));
      IntPtr p = Marshal.AllocHGlobal(100);
      concat("String ", "Concatenation", p, 100);
      MessageBox.Show(Marshal.PtrToStringUni(p));
      Marshal.FreeHGlobal(p);

      CppInterop.FreeLibrary(hModule);

A parte interessante aqui, começa no GetProcAddress. Quando chamamos, retornamos o ponteiro da função numa variável do tipo “IntPtr”.

Depois, quem faz o trabalho sujo é o Marshal.GetDelegateForFunctionPointer. Esse método faz o “cast” do function pointer não gerenciado para um delegate. Uma vez feita a conversão, é só chamar o delegate e estaremos executando o código não gerenciado.

Fizemos isso para nossos 3 delegates.

Callbacks em C++

Ora, se podemos facilmente intercambiar uma função entre ambientes gerenciados e não gerenciados, quer dizer que eu posso passar uma função em C# como parâmetro para uma função em C++?

Sim. É claro que pode. Vamos fazer isso no nosso exemplo.

Primeiro, vamos para o nosso SampleDLL em C++, e adicionamos o seguinte método:

void  keepSayingSomething(void (*callback)(LPTSTR thingToSay)){
	for(int i = 1; i <= 100; i++){
		_TCHAR str3[100];
		_stprintf_s(str3, 100, _TEXT("Saying %i"), i);
		callback(str3);
	}
}

A sintaxe abaixo, equivale à declaração de um function pointer em C++. Leia-se “ponteiro para um método quer retorna void, chama callback e possui um argumento chamado thingToSay do tipo LPTSTR (ponteiro para um array de caracteres, de um ou dois bytes, dependendo do define “UNICODE”. No nosso caso, estamos compilando com UNICODE, logo, 2 bytes).

void (*callback)(LPTSTR thingToSay)

A idéia do método é fazer um loop e chamar 100 vezes o método passado como parâmetro com uma string.

Vamos alterar também o nosso arquivo .def para exportar corretamente a função keepSayingSomething:

LIBRARY	"SampleDLL"
EXPORTS
  sum @1
  otherSum @2
  concat @3
  keepSayingSomething @4

Agora vamos para o nosso CSharpApp. vamos adicionar os seguintes delegates no nosso arquivo CppInterop.cs:

  public delegate void CallbackDelegate([MarshalAsAttribute(UnmanagedType.LPTStr)]string thingToSay);

  public delegate void KeepSayingSomethingDelegate(CallbackDelegate callback);

O primeiro delegate, é a representação do function pointer em C#. Estamos dizendo que o argumento thingToSay será convertido para string. O segundo delegate compatibiliza o método KeepSayingSomething. Veja que o function pointer foi inicialmente convertido para um IntPtr. No caso aqui, o delegate é automaticamente convertido para o function pointer.

Arraste um list box para o seu form (listBox1) e um novo button (button 2).

Agora, vamos declarar um novo método no Form1.cs e atribuir o código para o button 2:

    private void OnSaySomething(string thingToSay)
    {
      listBox1.Items.Insert(0, thingToSay);
    }

    private void button2_Click(object sender, EventArgs e)
    {
      if (openFileDialog1.ShowDialog() != DialogResult.OK)
        return;

      int hModule = CppInterop.LoadLibrary(openFileDialog1.FileName);

      if (hModule <= 0)
        throw new Exception("DLL not loaded.");

      IntPtr keepSayingSomethingPtr = CppInterop.GetProcAddress(hModule, "keepSayingSomething");
      KeepSayingSomethingDelegate keepSayingSomething = (KeepSayingSomethingDelegate)Marshal.GetDelegateForFunctionPointer(keepSayingSomethingPtr, typeof(KeepSayingSomethingDelegate));
      keepSayingSomething(OnSaySomething);

      CppInterop.FreeLibrary(hModule);
    }

No botão 2, repetimos o mesmo código para carregar diretamente a DLL, e depois liberar a mesma.

A parte nova do código está em executar a GetProcAddress e compatibilizar a chamada do método keepSayingSomething com o novo delegate. Até aqui nada de novo.

Já na outra linha, quando chamamos o método keepSayingSomething e passamos o método OnSaySomething como parâmetro, estamos convertendo automaticamente o delegate num function pointer. Podemos fazer isso indiretamente, através do método Marshal.GetFunctionPointerForDelegate. Este método pega um delegate do .Net e converte num function pointer C++, retornando o mesmo para um IntPtr. Nesse caso, poderíamos fazer a chamada do delegate da seguinte forma:

  public delegate void KeepSayingSomethingDelegate(IntPtr callback);

Agora executamos o programa, clicamos no button2 e… recebemos um belo erro: Run-time check failure #0 – The value of ESP was not properly saved across a function call. This is usually a result of calling a function declared with one calling convention with a function pointer declared with a different calling convention.

Vamos entender isso melhor no item abaixo:

Calling Conventions

Calling conventions (convenções de chamadas, não sei se a tradução é correta), são formas de controlar a passagem de argumentos entre funções. Elas existem pois cada compilador gera código em linguagem de máquina de forma diferente para empilhar e desempilhar os argumentos de funções.

Alguns fazem da direita para esquerda, outro da esquerda para a direita. Independente do que cada um significa, quando chamamos funções entre ambientes diferentes, temos que usar o mesmo método.

No Visual C++ quando não especificamos uma calling convention, o padrão é __cdecl. Podemos especificar outra. No .Net, quando compatibilizamos o delegate, por default ele entende __stdcall. Por isso tomamos o erro acima.

Podemos resolver o problema de duas formas. A primeira é especificar o calling convention __stdcall no C++, conforme o exemplo:

void  keepSayingSomething(void (__stdcall *callback)(LPTSTR thingToSay)){
	for(int i = 1; i <= 100; i++){
		_TCHAR str3[100];
		_stprintf_s(str3, 100, _TEXT("Saying %i"), i);
		callback(str3);
	}
}

Se você recompilar a DLL e executar novamente o programa, percebe que ele funciona normalmente.

A outra forma de resolver o problema é remover o __stdcall (o padrão do C++ é __cdecl) ou especificar o __cdecl e no C# especificar que o calling convention é cdecl, conforme os exemplos:

void  keepSayingSomething(void (__cdecl *callback)(LPTSTR thingToSay)){
	for(int i = 1; i <= 100; i++){
		_TCHAR str3[100];
		_stprintf_s(str3, 100, _TEXT("Saying %i"), i);
		callback(str3);
	}
}

No C#, precisamos anotar o delegate para especificar o calling convention

  [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
  public delegate void CallbackDelegate([MarshalAsAttribute(UnmanagedType.LPTStr)]string thingToSay);

Mais diversão com Delegates

Vamos fazer mais um exemplo divertido. Como percebemos no exemplo acima, a aplicação fala 100 vezes, de forma síncrona e o método termina.

Vamos fazer uma thread dentro da DLL que fica chamando o callback a cada 100 milissegundos para observarmos mais alguns comportamentos bacanas.

Para isso, vamos alterar nosso SampleDLL.cpp (C++):

DWORD WINAPI keepSayingSomethingThreadProc(__in  LPVOID lpParameter){
	void (__cdecl *callback)(LPTSTR) = NULL;
	callback = (void (__cdecl *)(LPTSTR)) lpParameter;
	int i = 1;
	while (true){
		_TCHAR str3[100];
		_stprintf_s(str3, 100, _TEXT("Saying %i"), i);
		callback(str3);
		i++;
		Sleep(100);
	}
	return 0;
}

void  keepSayingSomething(void (__cdecl *callback)(LPTSTR thingToSay)){
	DWORD threadID;
	HANDLE th = CreateThread(NULL, 0, &keepSayingSomethingThreadProc, callback, 0, &threadID);
}

Vamos discutir um pouco do código acima.

No momento que o método keepSayingSomething é chamado, ele chama o método CreateThread da API do Windows. Este método vai criar a thread, passando como parâmetro o método keepSayingSomethingThreadProc que será executado dentro dessa thread. O callback recebido como parâmetro, é passado para o parâmetro lpParameter do método keepSayingSomethingThreadProc e o método termina sua execução.

Já o keepSayingSomethingThreadProc fica eternamente em loop, dormindo a cada 100ms. Em cada rodada do loop, ele gera uma string concatenada, chama o callback e incrementa i.

Agora vamos alterar nosso código do button2, na aplicação C# para:

    private void updateListBox(string thingToSay)
    {
      listBox1.Items.Insert(0, thingToSay);
    }

    private void OnSaySomething(string thingToSay)
    {
      listBox1.Invoke(new CallbackDelegate(updateListBox), new object[] {thingToSay});
    }

    private void button2_Click(object sender, EventArgs e)
    {
      if (openFileDialog1.ShowDialog() != DialogResult.OK)
        return;

      int hModule = CppInterop.LoadLibrary(openFileDialog1.FileName);

      if (hModule <= 0)
        throw new Exception("DLL not loaded.");

      IntPtr keepSayingSomethingPtr = CppInterop.GetProcAddress(hModule, "keepSayingSomething");
      KeepSayingSomethingDelegate keepSayingSomething = (KeepSayingSomethingDelegate)Marshal.GetDelegateForFunctionPointer(keepSayingSomethingPtr, typeof(KeepSayingSomethingDelegate));
      keepSayingSomething(OnSaySomething);
    }

Vamos remover o FreeLibrary no final senão obviamente vai dar problemas com a thread. Deveríamos aqui controlar de forma mais elegante o carregamento e descarregamento da DLL, mas vamos ao foco da idéia dos callbacks.

O método OnSaySomething vai ser alterado para dar um listbox1.Invoke, chamando o método updateListBox. Isso é necessário pois como o callback vai ser chamado de outra thread, se não fizermos isso, vamos receber um InvalidOperationException: Operação entre threads inválida: controle ‘listBox1’ acessado de um thread que não é aquele no qual foi criado.

Vamos arrastar também um button3 e colocar o seguinte código no OnClick dele:

    private void button3_Click(object sender, EventArgs e)
    {
      GC.Collect();
    }

Agora executamos a aplicação e clicamos no botão 2. Vamos escolher selecionar a DLL e vemos o listBox enchendo de “Saying…”.

Se deixarmos a aplicação rodando um tempo, vamos receber um erro muito estranho no debugger do C#: “CallbackOnCollectedDelegate was detected”. Isso é intermitente e depende de quando o GC passa. Por isso criamos o button3. Quando rodamos o GC, o erro acontece na hora.

Esse também é divertido e para resolvermos, vamos discutir um pouco sobre delegates.

Estudando um pouco mais os delegates

O C# tem uma série de mecanismos para ajudarmos a programar melhor e mais rápido. Mas é sempre bom entender como as coisas funcionam nos bastidores para não ficarmos batendo cabeça com problemas desse tipo.

Quando declaramos um delegate, como abaixo:

  public delegate void KeepSayingSomethingDelegate(CallbackDelegate callback);

Internamente, estamos declarando uma classe que herda de System.MulticastDelegate. Essa classe tem um comportamento interessante, que é o de manter uma lista interna de métodos e quando você chama o delegate, na verdade chama vários métodos.

O exemplo abaixo deixa isso claro:

    private void button4_Click(object sender, EventArgs e)
    {
      CallbackDelegate d = new CallbackDelegate(Method1);
      d += Method2;
      d("Something");
    }

    private void Method1(string parm1)
    {
      MessageBox.Show("Method1 Invoked: " + parm1);
    }

    private void Method2(string parm1)
    {
      MessageBox.Show("Method2 Invoked: " + parm1);
    }

Declaramos um método 1 e um método 2, compatíveis com o delegate. Quando instanciamos o delegate passando como parâmetro o Method1, na verdade criamos uma classe internamente que contém uma lista e o Method1 adicionado na lista.

Quando fazemos um d += Method2; estamos na verdade adicionando nessa lista interna o Method2. O C# é legal e contém uma série de operadores pra facilitar nossa vida e fazermos escrever menos código.

Quando chamamos d(“Something”); percebemos que os dois métodos são executados.

Esse comportamento quem controla é a classe “System.MulticastDelegate”, da qual criamos uma instância sem saber.

Agora vamos fazer mais um exemplo, para entendermos outro comportamento implícito:

    private void Method1(string parm1)
    {
      MessageBox.Show("Method1 Invoked: " + parm1);
    }

    private void button5_Click(object sender, EventArgs e)
    {
      ExecuteMethod(Method1, "Something");
    }

    private void ExecuteMethod(CallbackDelegate Method, string thing){
      Method(thing);
    }

Ao clicar no button5, chamamos o método ExecuteMethod, que passa o Method1 por parâmetro. Quando fazemos essa passagem, o C# entende que o Method1 é compatível com o delegate CallbackDelegate por que sua assinatura bate com o delegate. Na verdade ele não passa o método como parâmetro. Ele cria uma instância do delegate, com um único método na sua lista interna e passa como parâmetro.

Por que isso é importante pra mim? Vamos para a próxima sessão.

CallbackOnCollectedDelegate was detected

Quando chamamos nossa DLL não gerenciada, passando um método do .Net como callback para a DLL C++, fizemos o seguinte:

      IntPtr keepSayingSomethingPtr = CppInterop.GetProcAddress(hModule, "keepSayingSomething");
      KeepSayingSomethingDelegate keepSayingSomething = (KeepSayingSomethingDelegate)Marshal.GetDelegateForFunctionPointer(keepSayingSomethingPtr, typeof(KeepSayingSomethingDelegate));
      keepSayingSomething(OnSaySomething);

No momento que fizemos keepSayingSomething(OnSaySomething);, conforme aprendemos no tópico anterior, criamos uma instância de uma classe (do delegate), adicionamos o método dentro da lista de métodos do delegate, convertemos isso num function pointer e entregamos para nossa DLL C++.

Acontece que em lugar nenhum “guardamos” essa referência do delegate (da classe que criamos internamente sem saber). Como ela ficou sem nenhuma variável apontando para essa referência, quando o garbage collector passa, ele entende como memória “livre”, vai lá e desaloca essa memória.

Nossa DLL C++ tem esse endereço guardado lá, porque foi entregue para ela como um function pointer. Quando ela chama esse método, ele já não existe mais na memória.

Divertido, né?

Para resolver esse problema de uma forma simples devemos primeiro declarar uma variável privada para o delegate:

  private CallbackDelegate callbackDelegate;

E no nosso código do button2 vamos fazer:

  private void button2_Click(object sender, EventArgs e)
    {
      if (openFileDialog1.ShowDialog() != DialogResult.OK)
        return;

      int hModule = CppInterop.LoadLibrary(openFileDialog1.FileName);

      if (hModule <= 0)
        throw new Exception("DLL not loaded.");

      IntPtr keepSayingSomethingPtr = CppInterop.GetProcAddress(hModule, "keepSayingSomething");
      KeepSayingSomethingDelegate keepSayingSomething = (KeepSayingSomethingDelegate)Marshal.GetDelegateForFunctionPointer(keepSayingSomethingPtr, typeof(KeepSayingSomethingDelegate));
      callbackDelegate = new CallbackDelegate(OnSaySomething);
      keepSayingSomething(callbackDelegate);
    }

Quando guardamos a referência na variável callbackDelegate, garantimos que o garbage collector não vai marcar o delegate como memória livre, porque guardamos a referência.

Código fonte

O código fonte dos exemplos pode ser encontrado aqui

Conclusão

Os recursos do Platform Invoke são bastante interessantes e permitem que reaproveitemos todo o nosso código gerenciado dentro de aplicações .Net.

Porém, é importante conhecermos mais a fundo um pouco mais do mundo gerenciado e do não gerenciado para não cairmos em problemas gerados pelas características de cada um deles.

Percebemos também que mesmo que a linguagem tente ser amigável e facilitar nossa vida para que consigamos escrever código melhor e mais rápido, é muito importante entender como as coisas funcionam num nível mais baixo para não gerarmos inocentemente problemas de difícil localização e correção.