C# - Usando código não gerenciado (PInvoke)


Embora a  Microsoft .NET Framework seja uma plataforma bem abrangente e de já estar na sua versão 4.0, ela não duplicou todos os recursos que estão disponíveis em código não gerenciado. Atualmente o .NET Framework não inclui todas as funções disponíveis na Win32 API e muitas empresas usam soluções proprietárias construídas em código nativo, componentes COM ou ActiveX.

Felizmente a .NET Framework esta equipada com recursos de interoperabilidade que permitem que você use código nativa a partir de aplicações .NET bem como permite acessar assemblies .NET como se fossem componentes COM.

Diante deste cenário fica a questão :

Como chamar uma função exportada por uma DLL nativa se essa função pode ser parte da API Win32 ou de uma biblioteca particular de código nativo ?

A resposta para esta pergunta é simples:

Declare um método no seu código C#, que será usado para acessar a função não gerenciada, como extern e static e aplique o atributo : System.Runtime.InteropServices.DllImportAttribute para especificar o arquivo DLL e o nome da função não gerenciada.

Para usar uma função nativa contida em uma biblioteca externa tudo o que você precisa fazer é declarar um método com a assinatura adequada.

O CLR - Common Language Runtime controla o restante de forma automática, incluindo carregar a DLL na memória quando a função é chamada e regulando os parâmetros a partir dos tipos de dados .NET para os tipos de dados nativos. O serviço .NET que suporta esta execução multi-plataforma é chamado PInvoke (Plataform Invoke) e o processo é transparente.

Ocasionalmente você terá que fazer um pouco de trabalho quando precisar dar suporte a estruturas em memória, fazer callbacks e trabalhar com seqüências mutáveis.

O PInvoke é geralmente usado para acessar as funcionalidades da API Win32, e particularmente características que não estão presentes no conjunto de classes gerenciadas ue compõe o .NET Framework. Três bibliotecas centrais compõe a API WIn32:

Como exemplo, considere as funções Win32 API usadas para escrever e ler arquivos INI (lembra dos benditos arquivos .INI ?), tais como GetPrivateProfileString e WritePrivateProfileString, presentes no arquivo Kernel32.dll. O. NET Framework não não incluem todas as classes que envolvem essasfuncionalidades. No entanto, você pode importar essas funções usando o atributo DllImportAttribute, da seguinte forma:

[DllImport("kernel32.DLL", EntryPoint="WritePrivateProfileString")]
private static extern bool WritePrivateProfileString(string lpAppName,string lpKeyName, string lpString, string lpFileName);

Os argumentos especificados na assinatura do método WritePrivateProfileString deve corresponder ao método da DLL ou um erro de execução ocorrerá quando você tentar chamá-lo.  Lembre-se que você não define qualquer corpo do método, porque a declaração se refere a um método na DLL.

A porção EntryPoint do atributo DllImportAttribute é opcional neste exemplo. Você não precisa especificar o EntryPoint quando o nome da função declarada corresponde ao nome da função na biblioteca externa.

A seguir temos um exemplo de como usar algumas funções Win32 API para obter informações sobre o arquivo INI usando a linguagem C#.

No exemplo declaramos as funções não gerenciados utilizadas que expõe métodos públicos para chamá-las. (Outras funções Win32 API usadas para obter informações sobre um arquivo INI não são mostradas neste exemplo inclusive aquelas que recuperam todas as seções em um arquivo INI.)

No código do exemplo código primeiro exibimos o valor atual de uma chave no arquivo INI, então modificamos esse valor, e então recuperamos o novo valor e, em seguida, gravamos o valor padrão.

O arquivo INI que vamos acessar chama-se Macoratti.ini e deve estar contido na pasta do projeto. O conteúdo do arquivo INI é mostrado a seguir:

[Geral]
NomeUsuario=Macoratti
EnderecoURL=http://www.macoratti.net
UltimoAcesso=25/12/2010

O arquivo possui a sessão Geral e as entradas : NomeUsuario, EnderecoURL e UltimoAcesso do qual vamos obter e manipular os valores.

Um arquivo INI é um arquivo texto usado para armazenar/fornecer configurações pessoais para sistemas/usuários ; Um arquivo INI é um arquivo com dados externo ao programa principal e esta formatado em : Secções(FileName) , Entradas e Valores :

[Seçao1]
entrada=valor
entrada=valor
entrada=valor
[Seção2]
entrada=valor
entrada=valor

  • A Seção identifica um conjunto de entradas e valores e esta relacionado a um determinado programa. Como muitos programas podem usar o mesmo arquivo INI ( Ex: o arquivo WIN.INI ) geralmente uma seção trazia o nome do programa que iria usá-la.

  • Uma Entrada funciona como um identificador para variáveis.

  • Os Valores são usados , como o próprio nome diz , para atribuir valores as entradas , e , sempre estão no formato Strings , mesmo sendo números.

Um exemplo de arquivo INI é o WIN.INI cuja estrutura mostramos em parte a seguir:

[windows]
load=
run=
NullPort=None
device=HP DeskJet 690C,HPDSKJTB,LPT1:
SingleClickSpeed=067614

[Desktop]
Wallpaper=(None)
TileWallpaper=0
WallpaperStyle=2
Pattern=120 49 19 135 225 200 140 30
 

Vamos abrir o Visual C# 2010 Express Edition e criar um novo projeto to tipo Console Application chamado UsandoPInvoke;

No menu File->New Project selecione o template Visual C# e a seguir Console Application definindo o nome UsandoPInvoke e clicando em OK;

Defina os namespaces usados no projeto no arquivo Program.cs:

using System;
using
System.Runtime.InteropServices;
using
System.Text;
using
System.IO;

A seguir vamos declarar as funções não gerenciadas usando DLLImportAttribute conforme o código abaixo:

        // Declaração das funções não gerenciadas: GetPrivateProfileString e 
        // WritePrivateProfileString
        [DllImport("kernel32.dll", EntryPoint = "GetPrivateProfileString")]
        private static extern int GetPrivateProfileString(string lpAppName,string lpKeyName, string lpDefault, StringBuilder lpReturnedString,
        int nSize, string lpFileName);

        [DllImport("kernel32.dll", EntryPoint = "WritePrivateProfileString")]
        private static extern bool WritePrivateProfileString(string lpAppName,string lpKeyName, string lpString, string lpFileName);

Em seguida defina o código a seguir em Main();

        static void Main(string[] args)
        {
            // Precisa usar o caminho completo ou Sistema Operacional vai tentar escrever 
            // o arquivo INI na pasta Windows causando problemas no Windows Vista /WIndows 7
            string nomeArquivoINI = Path.Combine(Directory.GetCurrentDirectory(),"Macoratti.ini");

            nomeArquivoINI = getCaminhoArquivoINI(nomeArquivoINI);
            
            string message = "Valor das entradas em [Geral] é : {0}";

            // Escreve um novo valor no arquivo INI.
            WriteIniValue("Geral", "UltimoAcesso",DateTime.Now.ToString(), nomeArquivoINI);
            
            // Obtêm o valor contido no arquivo INI
            string val1 = GetIniValue("Geral", "NomeUsuario", nomeArquivoINI);
            string val2 = GetIniValue("Geral", "EnderecoURL", nomeArquivoINI);
            string val3 = GetIniValue("Geral", "UltimoAcesso", nomeArquivoINI);

            Console.WriteLine(message, val1 ?? "???");
            Console.WriteLine(message, val2 ?? "???");
            Console.WriteLine(message, val3 ?? "???");
            
            // Aguarda para continuar
            Console.WriteLine(Environment.NewLine);
            Console.WriteLine("Pressione ENTER para continuar.");
            Console.ReadLine();
            
            // Atualiza o arquivo INI
            WriteIniValue("Geral", "NomeUsuario","Macoratti - MVP", nomeArquivoINI);
            WriteIniValue("Geral", "EnderecoURL","http://www.macoratti.net - quase tudo para VB", nomeArquivoINI);
            WriteIniValue("Geral", "UltimoAcesso", DateTime.Now.ToString(), nomeArquivoINI);
            
            // Obtêm o novo valor
            val1 = GetIniValue("Geral", "NomeUsuario", nomeArquivoINI);
            val2 = GetIniValue("Geral", "EnderecoRL", nomeArquivoINI);
            val3 = GetIniValue("Geral", "UltimoAcesso", nomeArquivoINI);

            Console.WriteLine(message, val1 ?? "???");
            Console.WriteLine(message, val2 ?? "???");
            Console.WriteLine(message, val3 ?? "???");

            // Aguarda para continuar
            Console.WriteLine(Environment.NewLine);
            Console.WriteLine("Método Main encerrado. Pressione ENTER.");
            Console.ReadLine();
         }
        public static string getCaminhoArquivoINI(string caminhoArquivo)
        {
            if (caminhoArquivo.IndexOf("\\bin\\Debug") != -1)
            {
                caminhoArquivo = caminhoArquivo.Replace("\\bin\\Debug", "");
            }
            else if (caminhoArquivo.IndexOf("\\bin\\Release") != -1)
            {
                caminhoArquivo = caminhoArquivo.Replace("\\bin\\Release", "");
            }
            return caminhoArquivo; 
        }//fim 

O código acima acessa o arquivo Macoratti.ini na pasta do projeto usando a função getCaminhoArquivoINI para alterar o caminho da pasta Debug.

A leitura do arquivo INI é feita pela função GetIniValue que obtém o valor de cada entrada na seção [Geral] e exibe os valores no console;

O código da função GetIniValue é dado a seguir:

   public static string GetIniValue(string section, string key,string nomeArquivo)
   {
            int chars = 256;
            StringBuilder buffer = new StringBuilder(chars);
            string sDefault = "";
            if (GetPrivateProfileString(section, key, sDefault,buffer, chars, nomeArquivo) != 0)
            {
                return buffer.ToString();
            }
            else
            {
                // Verifica o último erro Win32.
                int err = Marshal.GetLastWin32Error();
                return null;
            }
   }

Para alterar o valor das entradas usamos a função WriteIniValue() e a seguir exibimos os novos valores; O código desta rotina é exibido abaixo:

public static bool WriteIniValue(string section, string key,
        string value, string nomeArquivo)
        {
            return WritePrivateProfileString(section, key, value, nomeArquivo);
        }

O método GetPrivateProfileString é declarado com um parâmetro StringBuilder(lpReturnedString), pois esta seqüência deve ser mutável; quando a chamada for concluída, ele irá conter a informação retornada do arquivo INI .

Sempre que precisar de uma string mutável, você deve usar a classe StringBuilder em vez de da classe String. Muitas vezes, você vai precisar para criar o objeto StringBuilder com um buffer de caracteres de um tamanho definido, e depois passar o tamanho do buffer para a função como um parâmetro. Você pode especificar o número de caracteres no construtor StringBuilder.

Os valores iniciais para as entradas do arquivo Macoratti.ini são:

[Geral]
NomeUsuario=Macoratti
EnderecoURL=http://www.macoratti.net
UltimoAcesso=27/12/2010 07:18:03


Vamos executar o projeto e observe o resultado conforme mostrado a seguir:

Como podemos ver estamos acessando, obtendo e alterando os valores do arquivo INI usando código não gerenciado.

Existem mais funções que podemos usar em nosso código .NET usando PInvoke aguarde em breve mais artigos sobre esse assunto...

Pegue o projeto completo aqui: UsandoPInvoke.zip

Eu sei é apenas C#, mas eu gosto...

Referências:


José Carlos Macoratti