Objetivo
O objetivo desta primeira parte é descrever como utilizar DLL’s escritas em C/C++ (ou qualquer outra linguagem não-gerenciada) em C#. É um procedimento relativamente simples, mas existem algumas técnicas diferentes para carregamento estático e dinâmico, e ainda como compatibilizar os tipos entre as duas linguagens.
Criando uma DLL exemplo no C++
Para criar uma DLL no C++, devemos seguir os seguintes passos:
- File | New | Project
- Visual C++ (Muitas vezes dentro de “other languages”, principalmente para quem configurou o C# como ambiente principal no Visual Studio).
- Win32 Project
- Escolha o diretório
- O wizard do Win32 Project vai ser iniciado.
- Clique em Next
- Escolha DLL
- Empty Project
Crie um novo “header file”, chame ele de stdafx.h.
Crie um novo source file de nome SampleDLL.cpp, com o seguinte conteúdo:
#include "stdafx.h" #include "windows.h" #include "tchar.h" int sum(int a, int b){ return a + b; } void otherSum(int a, int b, int &result){ result = a + b; }
No caso temos dois métodos, um “sum” que retorna a soma de dois números, e outro “otherSum” que retorna a soma de dois números, por referência.
Adicione um novo “Module Definition File” (.def). Esse arquivo serve para colocar algumas definições na DLL. Nesse caso, para exportar os nomes das funções com nomes mais “amigáveis”. Vamos chamá-lo de SampleDLL.def colocar o seguinte conteúdo neste arquivo:
LIBRARY "SampleDLL" EXPORTS sum @1 otherSum @2
Vamos compilar o projeto e partir para o lado C#.
Criando uma aplicação no C#
File | New | Project | Windows Application.
Vamos criar uma nova classe chamada CppInterop.
Dentro dessa classe, vamos colocar o seguinte código:
[DllImport("SampleDLL.dll")] public static extern int sum(int a, int b); [DllImport("SampleDLL.dll")] public static extern void otherSum(int a, int b, ref int result);
O atributo “DllImport” é quem identifica qual a DLL que vai ser carregada para executar o método. A seguir a assinatura do método da DLL vai ser praticamente copiada para o C#. Nesse caso, a tradução é manual, ou seja, é necessário entender como funciona a chamada da função e usar tipos que são intercambiáveis. Falaremos mais sobre a tipagem adiante.
- Arraste um botão para dentro do form.
- Clique duas vezes neles.
Dentro do método button1_click, adicione o código:
int res = CppInterop.sum(10, 20); MessageBox.Show(res.ToString()); CppInterop.otherSum(5, 2, ref res); MessageBox.Show(res.ToString());
Agora execute e clique no botão. Vai receber um DllNotFoundException com o erro: “Não é possível carregar a DLL ‘SampleDLL.dll’: Não foi possível encontrar o módulo especificado. (Exceção de HRESULT: 0x8007007E)”
Para explicar esse erro, vamos ao próximo tópico.
Static Load x Dynamic Load
A diferença básica entre o carregamento estático (static load) e o carregamento dinâmico (dynamic load) está na dependência direta da DLL. Se você tem uma aplicação escrita em C++ e faz referências diretas a funções “externas”, está fazendo um static load.
Quando a aplicação é executada, automaticamente a DLL é procurada e caso não encontre a aplicação nem sobe.
A seqüência de busca de uma DLL é praticamente padrão no Windows:
- Diretório System32
- Diretório da aplicação
- Qualquer diretório no PATH (variável de ambiente).
Já nas aplicações com carregamento dinâmico, a aplicação chega a subir, e ocorre a tentativa de subir a DLL. Nesse caso é possível a aplicação continuar executando ou mesmo mandar erros mais amigáveis. Geralmente usa-se essa abordagem em plugins.
No caso de aplicações em C# Windows Forms, percebemos um comportamento um pouco diferente. O erro só ocorre quando tentamos executar o método “extern” com o atributo DllImport, o que nos faz entender que no momento da execução do método, a DLL é carregada e o método é executado.
Vamos agora então copiar a SampleDLL.dll para dentro do diretório bin/debug e atender o segundo critério listado acima para carregamento da DLL.
Executamos a aplicação e percebemos que ela funciona. Executamos o código C++ dentro do C# através da DLL.
Outro comportamento interessante que observamos aqui é que mesmo após a execução dos métodos, se tentarmos apagar ou renomear a DLL (que teoricamente está em uso), conseguimos realizar a operação. Isso nos faz pensar que o carregamento e descarregamento da DLL a cada execução, porém examinando o resultado do executável com o ProcessExplorer, percebemos que a DLL continua carregada.
Com isso, percebemos que quando fazemos o DllImport diretamente, internamente o Platform Invoke (como a Microsoft chama essa feature de intercambiar código gerenciado com não-gerenciado) sempre utiliza carregamento dinâmico.
O atributo DllImport também nos dá outras opções para especificar diferentes entry points ou calling conventions. Não vou explorar todos os detalhes aqui.
Diferenças de Tipagens
Existem formas completamente diferentes de tratar informações no mundo gerenciado e não gerenciado. Muitos tipos são exatamente iguais, como o usado no exemplo acima, o Int32. Ele acaba tendo exatamente a mesma representação nas duas linguagens. O mesmo não corre com alguns tipos.
Esse artigo do msdn nos demonstra a compatibilidade, de forma bem simples: Platform Invoke Data Types .
A parte divertida das tipagens, está no tipo “IntPtr”. Ele foi uma forma de conseguir colocar um ponteiro dentro do C#. Tudo que é um ponteiro em C++ ou outras linguagens não gerenciadas são diretamente intercambiáveis com um IntPtr.
Devolvendo Strings do C++ para o C#
O C++ tem uma forma muito particular de tratar strings. São as famosas strings terminadas em zero ou “Null terminated strings” (ver Caractere Nulo).
Geralmente quem vem do mundo C#, Java ou mesmo Delphi, tem tipos strings que fazem todo o controle de tamanho, memória, etc. Em C++ existe também esse tipo, mas geralmente não é usado, por ser “caro” demais. Programadores C++ geralmente tem performance correndo pelas veias.
Geralmente passa-se um ponteiro para o primeiro caractere da string e a vai-se incrementando o caractere até achar um caractere #0 (null). Isso indica que acabou a string e a memória dali pra frente passa a ser “desconhecida”. Outros níveis de complexidade começam a aparecer quando a string deixa de ser uma string ASCII/ANSI (um byte por caractere) e passa a ser Unicode ou Multibyte. A idéia deste artigo não é tentar explicar isso.
Tendo em mente essa idéia, geralmente quando é necessário devolver uma string terminada em zero em linguagens não gerenciadas, ocorre um problema quanto à alocação da memória necessária para a string.
- Se dentro do C++ você declarar um array de caracteres como uma variável local e devolver o ponteiro, vai gerar um belíssimo bug, pois a memória da pilha é liberada e o ponteiro passa a ser inválido.
- Se dentro do C++ você alocar a memória no heap com um new ou um malloc e devolver o ponteiro, quem chamou a função precisa “lembrar” de desalocar a memória. Essa é uma bela forma de gerar um vazamento de memória (memory leak).
- A solução comumente usada para esse problema é quem está chamando a função alocar um buffer e passar o ponteiro para o início do buffer e o seu tamanho máximo. É praticamente uma convenção no mundo C/C++ essa abordagem.
Com isso, vamos fazer um novo método na nossa DLL C++ para concatenar duas strings. É um método bastante inútil, visto que já existe o strcat. Mas para fins didáticos, é interessante. Vamos ao seu conteúdo no C++:
void concat(LPTSTR src1, LPTSTR src2, LPTSTR dest, int destSize){ _tcscpy_s(dest, destSize, src1); _tcscat_s(dest, destSize, src2); }
O tipo LPTSTR no C++ indica um “ponteiro para uma string terminada em zero”, porém o LPTSTR tem uma série de defines dentro do windows.h e tchar.h, para fazer com que, caso a aplicação seja compilado com _UNICODE, os tipos sejam convertidos internamente para 2 bytes por caractere (O Windows trata todos os caracteres com 2 bytes em Unicode). Caso seja compilado sem _UNICODE, vai um byte por caractere. Aqui vamos assumir que vai ser compilado como _UNICODE.
A idéia desse método é devolver em “dest” as strings src1 e src2 concatenadas. “destSize” é o tamanho do buffer pré-alocado em dest.
_tscpy_s é uma macro que traduz para uma função que copia uma string para outro buffer de forma “segura” (evitando o estouro do buffer). Por isso o parâmetro destSize é passado. A macro é para garantir a questão da compilação com ou sem _UNICODE.
_tsccat_s é uma macro que traduz para uma função que concatena duas strings de uma forma segura.
Como fica isso no C#?
Vamos alterar o nosso CppInterop.cs e adicionar o seguinte método:
[DllImport("SampleDLL.dll")] public static extern void concat([MarshalAsAttribute(UnmanagedType.LPWStr)]string src1, [MarshalAsAttribute(UnmanagedType.LPWStr)]string src2, IntPtr dest, int destSize);
O MarshalAsAttribute (UnmanagedType.LPWStr) é um atributo usado no argumento da função, indicando que no momento da tradução, no lado “Unmanaged” temos um LPWStr (ponteiro para caractere de 2 bytes), ou seja, no momento do “Invoke” da função, o Platform Invoke vai fazer o trabalho sujo de tradução para nós. Aqui se usássemos um LPTStr, o resultado seria o mesmo, já que no lado “Unmanaged” sabemos que temos uma string de caracteres de 2 bytes. Eu coloquei diferente justamente para ilustrar que o Platform invoke não faz “checagens” dos tipos do lado de lá. Ele assume que o tipo do lado de lá, executa o método e vamos ver o que acontece (as vezes cai em memória indevida e dá Access Violation).
Os dois primeiros parâmetros serão passados como strings e traduzidas. Se o terceiro parâmetro é um LPTSTR no C++, por que foi traduzido como um IntPtr no C#?
A resposta é que a idéia desse parâmetro é passar o ponteiro para um “buffer” onde a função vai responder a string concatenada. Ou seja, eu vou dar um espaço de memória para o C++ escrever e depois vou ler o que tem dentro. O destSize é justamente o tamanho desse buffer, para evitar que o C++ escreva em memória indevida.
A chamada dessa função no C# fica da seguinte forma:
IntPtr p = Marshal.AllocHGlobal(100); CppInterop.concat("String", " concatenation", p, 100); MessageBox.Show(Marshal.PtrToStringUni(p)); Marshal.FreeHGlobal(p);
Vamos tentar entender o código acima.
A primeira linha, vai alocar um buffer de 100 bytes e retornar o ponteiro para um IntPtr.
A segunda, vai executar o método concat, convertendo os dois primeiros parâmetros e dando o buffer pré-alocado no terceiro parâmetro e indicando pro C++ q ele pode escrever em até 100 bytes.
A terceira linha, usa métodos auxiliares da classe Marshal, o PtrToStringUni, que vai converter o conteúdo do buffer para uma string em C#, considerando que o que tem apontado é uma string terminada em zero Unicode (existem as versões multi-byte e ANSI).
A quarta linha é para liberar a memória alocada na primeira linha. Ou seja, o buffer que eu pré-aloquei. Quem disse que o C# não vaza memória? :-P. É interessante colocar blocos como esse dentro de um try/finally.
Outro exercício interessante que podemos fazer com esse exemplo é mudar o tamanho do buffer em AllocHGlobal e na chamada do concat para “10”. Com isso, vamos perceber que vai estourar uma EAccessViolationException no C#. Por incrível que pareça isso é bom.
A função _tcscat_s, verifica o tamanho do buffer e estoura uma exceção que é retornada para o C#.
Se quisermos ver uma coisa mais divertida, mantemos o tamanho do buffer com 10 e na DLL C++, alteramos as chamadas de _tcscpy_s para _tsccpy e _tcscat_s para _tcscat.
O efeito que obtemos é mais divertido, por que a string é concatenada mas escreve numa área de memória que sabe-se lá o que tem dentro. O C# chega a mostrar a string, porém dá uma exceção EAccessViolationException. O C# consegue identificar que está lendo memória “inválida”, porém, já houve a gravação nessa memória.
Conclusão
Observamos que o C# tem vários recursos interessantes para acessar código escrito em linguagens não gerenciadas. Mesmo funções da API do Windows podem ser chamadas diretamente desta forma.
Percebemos também que é necessário conhecermos muito bem o funcionamento das chamadas em código não gerenciado, já que a tradução dos tipos não é simples e totalmente intuitiva.
Nas próximas partes desse artigo pretendo abordar alguns tópicos um pouco mais avançados sobre o assunto.
Gostaria de agradecer os amigos Marcos Capelini e Rodrigo Strauss por me ajudarem bastante com as práticas e culturas mundo C/C++.
Código fonte
Baixe o código fonte aqui.
Muito bom este artigo, está sendo muito útil.
Obrigado
Abinálio
Que bom! Esse é o objetivo!
Divirta-se. Se tiver algum outro assunto que acha que merece ser abordado, me avise. Na medida do possível eu vou atrás.
Ainda pretendo escrever sobre carregar e descarregar dinamicamente, compatibilização de function pointers com delegates e callbacks entre código gerenciado e não gerenciado.
Abraço,
Eric
Eric, este artigo ajudou-me muto. Ja havia lido um em inglês mas que é largamente superado por este. Deus te abençoe e derrame sobre voce saúde em abundância e infinitas bencãos.
Abraço
Eric,
Muito bom o post. Só que comecei a seguir e no final deu erro. Então decidi baixar o código fonte e executar. Retornou o seguinte erro: “An attempt was made to load a program with an incorrect format. (Exception from HRESULT: 0x8007000B)”. Estou usando o visual studio 2012 então quando fui executar tive que converter o código.
Eu sei que esse post é mais antigo mas estou precisando MUITO.
Tentei de várias formas e todas dão erro. tentei usar extern “C” com __declspec(dllexport) e, quando eu uso parâmetros no método, também da erro.
Ex: Se crio um método
extern “C”
{
__declspec(dllexport) int sum()
{
return 12345;
}
}
Funciona.
Se crio…
extern “C”
{
__declspec(dllexport) int sum(int a, int b)
{
return a + b;
}
}
ERRO: “A call to PInvoke function ‘TesteDLL!TesteDLL.CppInterop::sum’ has unbalanced the stack. This is likely because the managed PInvoke signature does not match the unmanaged target signature. Check that the calling convention and parameters of the PInvoke signature match the target unmanaged signature.”
Se puder me ajudar…
agradeço desde já,
Renan.
Renan,
Esse erro acontece quando algo não está batendo entre o mundo gerenciado e não-gerenciado. Uma vez tive um problema similar quando a palavra chave “int” significava uma coisa no C# e outra coisa no C++ (num era um Int64, no outro um Int32, ou coisa parecida). Tente especificar os tipos corretamente e ter certeza baseado na versão do compilador (C++) e target framework (.NET) se as duas coisas batem.
Outra coisa que pode não estar batendo é a calling convention. Em C++ o defaul é cdecl. Quando especificado, pode mudar para stdcall. As funções da API do windows usam stdcall como default.
Neste caso, ao traduzir a chamada para o .NET, vc deve lembrar de especificar a anotação dos atributos para definir a calling convention. Ex.:[dllimport(“mylib.dll”,CallingConvention=CallingConvention.Cdecl)]
Mexer com interop é assim mesmo… rs. É divertido, mas tem hora q descobrir esse tipo de problema dá um trabalhão. A regra básica é simples, as duas coisas tem q ser idênticas, bit a bit, senão não funciona.
Abraço,
Eric
Funcionou!
O problema era que ele estava mudando para stdcall. quando especifiquei a calling convention executou.
Muito obrigado Eric!
Porem agora fiz um outro teste básico. Adicionei aos parâmetros uma string out (__declspec(dllexport) int sum(int a, int b, char** c)) adicionei também no C# e quando executo não da exception e sim abre uma janela do windows: “vshot32.exe parou de funcionar”
Ja passou por isso?
Obrigado mais uma vez,
Renan.
Renan,
Leia o artigo inteiro. Está explicado lá. char** pode ser qualquer coisa em C/C++. Você precisa entender que tipo de string vai usar e anotar corretamente o método. Pode ser uma string simples, widechar, terminada em , entre outras.
Abraço,
Eric
Tem toda a razão, me desculpe!
Não tinha lido a parte que você fala de strings. Muito Bom! Entendi e consegui reproduzir!
Obrigado!