Desenvolvimento - C/C++

Reutilizando Código Nativo no .NET

Nesse artigo, veremos como reutilizar código nativo escrito na linguagem C em aplicações .NET.

por Leandro Alves Santos



Nesse artigo, veremos como reutilizar código nativo escrito na linguagem C em aplicações .NET.

Platform Invoke

Platform Invoke é o mecanismo que permite a chamada de código nativo através da plataforma .NET. Podemos usufruir deste mecanismo chamando funções da API do Windows, funções de DLL’s criadas a partir de código escrito em linguagens como C, etc.

Criando um Exemplo Simples

Vamos criar uma função em C que escreve uma mensagem na tela e iremos chamá-la através do nosso código gerenciado com a linguagem C#.

Abra o Visual Studio 2008, clique em File -> New -> Project..., selecione a linguagem Visual C++ e selecione o template Win32 Project conforme a figura 1. O nome do projeto será CodigoNativo.

Figura 1: Criando um projeto Win32 no Visual Studio.

Após isso, veremos uma janela semelhante à figura 2. Clique em Application Settings ou clique no botão “Next >”.

Figura 2: Application Wizard

Em Application Settings, vamos efetuar as seguintes configurações:

Application Type: DLL

Additional Options: Empty Project

Veja as opções selecionadas na figura 3. Após configurar as opções, clique em Finish.

Figura 3: Application Settings

Agora clique com o botão direito na pasta Source Files, selecione Add e depois “New Item...”. Na janela que abriu, selecione o template C++ File (.cpp). Na figura 4 podemos ver como a janela está nesse momento. Você pode colocar o nome que desejar no arquivo, mas é importante que a extensão do arquivo seja .c.

Figura 4: Adicionando um arquivo ao projeto

Agora vamos escrever nosso código que escreve uma mensagem na tela. O código pode ser visto na listagem 1.

#include <stdio.h>

__declspec(dllexport) void EscreverTexto()

{

      printf("Bem vindo ao \"DLL Hell\"!\n");

}

Listagem 1: Método EscreverTexto() em código C

Na listagem 1, temos uma função que simplesmente escreve um texto na tela.

O atributo dllexport em conjunto com a palavra chave __declspec possibilitam o nosso método de ser visível fora da DLL que será gerada.

Anteriormente foi mencionado que o arquivo deveria ser criado com a extensão .c, nós fizemos isso para que o compilador entenda que estamos trabalhando com código escrito na linguagem C. Caso tivéssemos criado nosso arquivo com a extensão .cpp, ocorreria o “decoration” do nome da função.

Decoration é uma técnica em que o nome do método, o tipo de retorno e a lista de parâmetros são utilizados para criar um nome único da função para o link editor.

Caso o nosso arquivo tivesse a extensão .cpp, poderíamos utilizar a palavra chave extern em conjunto com a string “C”, com isso o “decoration” não ocorreria. Veja na listagem 2 como ficaria o código utilizando a palavra chave extern.

#include <stdio.h>

extern "C"

{

      __declspec(dllexport) void EscreverTexto()

      {

            printf("Bem vindo ao \"DLL Hell\".\n");

      }

}

Listagem 2: Utilizando extern “C” para evitar o processo de “decoration”

Compile a solução clicando em Build -> Build Solution.

Agora vamos criar um projeto em C# que fará a chamada a função EscreverTexto() da DLL em código nativo.

Para criar um projeto em C#, utilizaremos praticamente os mesmos passos que usamos para criar um projeto em Visual C++. Com a exceção de que selecionaremos a linguagem Visual C# e criaremos um projeto do tipo Console Application. Podemos ver isso na figura 5.

Figura 5: Criando um projeto Console Application

Na listagem 3 podemos ver o código do arquivo program.cs.

using System;

using System.Collections.Generic;

using System.Linq;

using System.Text;

using System.Runtime.InteropServices;

namespace ConsoleApplication

{

    class Program

    {

        [DllImport("CodigoNativo.dll")]

        static extern void EscreverTexto();

        static void Main(string[] args)

        {

            Console.WriteLine("Estou no código gerenciado.");

            EscreverTexto();

            Console.WriteLine("Voltei ao código gerenciado.");

            Console.ReadKey();

        }

    }

}

Listagem 3: Programa que contém a chamada da função EscreverTexto()

Vejam o seguinte trecho de código:

[DllImport("CodigoNativo.dll")]

static extern void EscreverTexto();

Nesse trecho de código, declaramos a função EscreverTexto() com o atributo DllImport. Esse atributo indica que estamos trabalhando com uma função não gerenciada e passamos como parâmetro o nome da DLL que contém essa função.

Reparem que dentro do método main, chamamos a função da mesma forma que chamamos um método escrito em C#.

Compile o projeto ConsoleApplication. Copie a DLL do projeto CodigoNativo para a pasta bin\Debug do projeto ConsoleApplication. A DLL pode ser encontrada na pasta Debug. Após copiar a DLL, execute a aplicação. O resultado pode ser visto na figura 6.

Figura 6: Execução do aplicativo ConsoleApplication

Enviando e Recebendo Dados do Código Nativo

Quando chamamos uma função de código nativo que possui parâmetros e/ou retorna algum valor, esses dados podem ser convertidos entre o mundo gerenciado e não gerenciado de acordo com o tipo de dado a ser trabalhado. Esse processo é chamado de Marshaling.

Grande parte dos tipos não precisa passar pelo processo de Marshaling por ter a mesma representação entre o código gerenciado e não gerenciado.

Vejamos agora dois casos: O tipo System.Int32 não precisa de nenhuma conversão entre código gerenciado e código não gerenciado. O tipo System.String pode ser convertido em BSTR ou em um array de caracteres.

Trabalhando com Tipos Por Valor

Criaremos agora um exemplo simples, onde passamos dois tipos inteiros por valor e esses valores são somados e a função retorna a soma entre esses dois valores.

Na listagem 4 temos o código em C que efetua a soma.

#include <stdio.h>

__declspec(dllexport) int Somar(int valor1, int valor2)

{

    int resultado = valor1 + valor2;

    return resultado;

}

Listagem 4: Função que efetua um calculo e retorna o resultado

Na listagem 5 temos o código em C# do arquivo program.cs que declara a função Somar, chama a função e mostra o resultado.

using System;

using System.Collections.Generic;

using System.Linq;

using System.Text;

using System.Runtime.InteropServices;

namespace ConsoleApplication

{

    class Program

    {

        [DllImport("CodigoNativo.dll")]

        static extern Int32 Somar(Int32 valor1, Int32 valor2);

        static void Main(string[] args)

        {

            Int32 resultado;

            resultado = Somar(30, 40);

            Console.WriteLine("A função Somar retornou o valor {0}.", resultado);

            Console.ReadKey();

        }

    }

}

Listagem 5: Código que declara e executa o método Somar.

Nesse exemplo, declaramos a função contida na DLL e no método main, chamamos a função passando os parâmetros requisitados e mostramos o resultado. Como podemos ver, passar parâmetros para código nativo é muito simples. O resultado pode ser visto na figura 7.

Figura 7: Execução do aplicativo que soma dois valores.

Veremos agora um exemplo de uma função que recebe um valor inteiro e informa se esse valor é par ou ímpar. Na listagem 6 temos o código em linguagem C.

     

#include <stdio.h>

__declspec(dllexport) int VerificarNumeroPar(int valor)

{

      int resultado = valor % 2;

      if(resultado == 0)

            return 1;

      else

            return 0;

}

Listagem 6: Função que indica se um valor é par ou ímpar

Agora estamos trabalhando com um tipo inteiro simulando um tipo Boolean. O tipo Boolean pode ser convertido para um formato de um inteiro de 4 bytes onde 0 é igual a falso e qualquer outro valor é verdadeiro, pode ser um inteiro de 1 byte, onde 0 é falso e 1 é verdadeiro ou para um inteiro de 2 bytes onde -1 é igual a verdadeiro e 0 é igual a falso.

Todos os tipos que precisam passar pelo processo de Marshaling, tem um comportamento default nesse processo, mas nós podemos indicar o tipo de conversão através do atributo MarshalAs em conjunto com o enum UnmanagedType. Na listagem 7, utilizamos o atributo MarshalAs para o parâmetro que estamos enviando e para o retorno da função VerificarNumeroPar.

using System;

using System.Collections.Generic;

using System.Linq;

using System.Text;

using System.Runtime.InteropServices;

namespace ConsoleApplication

{

    class Program

    {

        [DllImport("CodigoNativo.dll")]

        [return: MarshalAs(UnmanagedType.Bool)]

        static extern Boolean VerificarNumeroPar(

            [MarshalAs(UnmanagedType.I4)]Int32 valor1);

        static void Main(string[] args)

        {

            Int32 valor;

            Boolean resultado;

            valor = 5;           

            resultado = VerificarNumeroPar(valor);

            Console.WriteLine(

                "O numero {0} é par? {1}", valor, resultado);

            valor = 7;

            resultado = VerificarNumeroPar(valor);

            Console.WriteLine(

                "O numero {0} é par? {1}", valor, resultado);

            valor = 10;

            resultado = VerificarNumeroPar(valor);

            Console.WriteLine(

                "O numero {0} é par? {1}", valor, resultado);

            Console.ReadKey();

        }

    }

}

Listagem 7: Código que declara e executa o método VerificarNumeroPar

Na figura 8 podemos ver a execução do aplicativo.

Figura 8: Execução do aplicativo que verifica se um dado número é par

Trabalhando com Tipos Por Referência

Para trabalhar com tipos por referência, utilizaremos a palavra chave ref em nosso código gerenciado quando estamos trabalhando com um Value Type, como um inteiro por exemplo. Se estivéssemos trabalhando por exemplo, com um objeto string, isso não seria necessário, pois o tipo string no .NET Framework é um Reference Type.

Vamos agora a um exemplo onde passamos para o código nativo uma variável do tipo inteiro com a palavra chave ref e esse valor será incrementado. O método não possui retorno. Veja antes o código escrito em linguagem C na listagem 8. Notem que o método recebe um ponteiro para um tipo inteiro.

#include <stdio.h>

__declspec(dllexport) void Incrementar(int *valor)

{

      ++*valor;

}

Listagem 8: Método que incrementa um valor inteiro.

Na listagem 9 veremos o código escrito em C# que chama esse método e mostra o valor da variável que foi enviada como parâmetro.

using System;

using System.Collections.Generic;

using System.Linq;

using System.Text;

using System.Runtime.InteropServices;

namespace ConsoleApplication

{

    class Program

    {

        [DllImport("CodigoNativo.dll")]

        static extern void Incrementar(ref Int32 valor);

        static void Main(string[] args)

        {

            Int32 valor = 0;

            Console.WriteLine("Valor original: {0}", valor);

            Incrementar(ref valor);

            Console.WriteLine(

                "Valor após primeiro incremento: {0}", valor);

            Incrementar(ref valor);

            Console.WriteLine(

                "Valor após segundo incremento: {0}", valor);

            Incrementar(ref valor);

            Console.WriteLine(

                "Valor após terceiro incremento: {0}", valor);

            

            Console.ReadKey();

        }

    }

}

Listagem 9: Código em C# que chama o método de incremento escrito em linguagem C

Como estamos trabalhando com referência ou um ponteiro, dependendo do ponto de vista, o valor foi alterado na variável que passamos como parâmetro para o código não gerenciado.

Na figura 9, podemos ver a execução do aplicativo.

Figura 9: Execução do aplicativo que incrementa um valor inteiro

Conclusão

Como podemos ver nesse artigo, é muito simples reutilizar códigos que foram escritos em código nativo/não gerenciado, pois a Platform Invoke faz grande parte do trabalho para nós.

Leandro Alves Santos

Leandro Alves Santos - Visite o blog do autor: http://weblogs.pontonetpt.com/las/.