Criando e consumindo DLL

Visão geral

Criar bibliotecas é prático, você mantêm um conjunto de funcionalidades presos em arquivos externos ao seu programa e então você distribui junto com o seu executável.

Uso prático

Você pode incluir um conjunto de funções dentro de uma DLL para que vários programas juntos consumam as suas funções, com isso você garante que programas diferentes usem o mesmo método, por exemplo, uma função que testa se o login de uma pessoa, isto é, nome de usuário e senha estão corretos para prosseguir com acesso ao banco de dados. Para impedir que cada programa seu tenha um Ctrl+C/Ctrl+V da mesma função em todos os programas, você criaria uma biblioteca em pascal que fizesse esse teste e colocaria todos os programas para compilar junto com essa biblioteca e isso daria certo, mas passam-se os anos e alguém altera essa biblioteca e agora teremos os programas novos que usam o código novo, porém programas mais antigos deveriam ser recompilados com a nova biblioteca ou pode acontecer problemas. Daí a ideia de usar bibliotecas externas ao seu programa principal que normalmente manterá sua consistência.

Outro exemplo prático para usar DLLs são relatórios. Ao invés de colocar relatórios embutidos dentro do seu executável, é mais saudável deixá-los externamente em forma de DLL. Esse método permite que clientes diferentes solicitem novos relatórios que afetem apenas eles.

Como criar uma função para DLL bem básica usando strings

O pascal tem métodos de lidar com variaveis que diferem muito de outras linguagens como C/C++, então no exemplo abaixo, vamos lidar com um tipo de string chamado de pChar. O tipo pChar é um array ou buffer de caracteres ou bytes enfileirados, ele não é tão prático quanto String, mas escrever DLLs que possam ser usadas por outros programas que foram escritos em outra linguagem requer que usemos ele, então veja um exemplo bem básico de como funciona.

O exemplo abaixo é uma função genérica que não implementa, mas cria um modelo de como eu posso testar um login no banco de dados fornecendo parâmetros como login, senha e role por meio de uma TStringList e retorna novamente uma TStringList.

Escolhi TStringList porque elas são fáceis de manipular, eu posso usar TStringList.Values[‘parametro’] para descobrir um valor ou TStringList.Items.IndexOf[‘parametro’] para saber se um parâmetro foi fornecido e assim por diante.

Algo que pode muito confundi-lo é que ao olhar para o código você notará que o parâmetro de entrada é pChar e não TStringList como eu disse, por que?

Não vou usar exatamente o TStringList, mas TStringList.Text que também é uma string. Ao lidar com DLLs precisamos que ela seja criada de um jeito que outros programas possam entender, outras linguagens não usam uma String do jeito que o pascal o faz, assim precisamos usar um tipo especial de String chamada de pChar.

O tipo pChar é uma cadeia especial de caracteres que pode facilmente ser convertido para string e vice-versa. Então vamos ao código:

library DLL_Teste;
uses
  Classes,
  SysUtils
  { you can add units after this };

function F_TestLogin(pParamList:pChar): pChar; stdcall;
var
  sLoginName, sLoginPassword, sRole, sConfigFile:String;
  ListaIN:TStringList;
  ListaOUT:TStringList;
  function FakeLogin(ALoginName, APassword, ARole:String):String;
  begin
    Result:=emptyStr;
  end;
begin
  ListaIN:=TStringLIST.Create;
  ListaOUT:=TStringLIST.Create;
  ListaOUT.Values['RESULT']:='FAIL';
  // garantindo que qualquer exit antes do tempo retorne FAIL
  Result:=pChar(ListaOUT.Text);
  try
    // A estrutura de minha string é compativel com TStrings então posso
    // importá-la como se fosse uma StringList
    ListaIN.Text:=String(pParamList);   // pChar para string
    sConfigFile:=Trim(ListaIN.Values['CONFIG_FILE']);
    sLoginName:=Trim(ListaIN.Values['User_Name']);
    sLoginPassword:=Trim(ListaIN.Values['Password']);
    sRole:=UPPERCASE(Trim(ListaIN.Values['Role']));
    // apenas um exemplo de teste de autenticação
    if FakeLogin(sLoginName, sLoginPassword, sRole)=emptyStr then
    begin
      // Prosseguindo...
	  ListaOUT.Values['RESULT']:='OK';
	  ListaOUT.Values['MSG_RESULT']:='OK';
    end
    else
    begin
      // Erro ao tentar conectar ao servidor
      ListaOUT.Values['RESULT']:='FAIL';
      ListaOUT.Values['MSG_RESULT']:='Autenticação falhou';
    end;
  except
  on e:exception do
     begin
	   ListaOUT.Values['RESULT']:='FAIL';
       ListaOUT.Values['MSG_RESULT']:=e.message;
	 end;
  end;
  ListaOUT.Values['User_Name']:=sLoginName;
  ListaOUT.Values['Password']:=sLoginPassword;
  ListaOUT.Values['Role']:=sRole;
  Result:=pChar(ListaOUT.Text);
  ListaIN.Free;
  ListaOUT.Free;
end;

exports
  F_TestLogin;

begin
end.

Não usará outra linguagem, apenas pascal?

Se você usará apenas pascal e nada mais, seja delphi ou freepascal então não é necessário usar pChar ou qualquer outro tipo de ponteiros para tipos primitivos. Ao usar os tipos primitivos do pascal ao invés de pChar e ponteiros você economiza muito código dedicado a conversão de tipos. também poderá remover as indicações do tipo stdcall;

Como consumir DLLs

Agora, como posso consumir a DLL acima?

Você pode consumir DLLs de duas formas:

  • Estática: quando seu aplicativo é carregado, todas as DLLs que foram informada sem seu programa serão carregadas conjuntamente e se falhar uma delas, o programa será interrompido. Quando usar? Você precisa que seja estática quando se tratar de um programa ou serviço indispensável ao seu programa, sem essa(s) biblioteca(s) o programa não deve nem funcionar. Por exemplo, uma DLL de acesso a impressora fiscal.
  • Dinamicamente: A DLL é requerida apenas no momento do uso, você a executa e em seguida a mesma é liberada da memória. Quando usar? Quando não está certo se a funcionalidade dentro da biblioteca será usada ou não, é o caso de relatórios, um usuário poderá decidir imprimir ou não o relatório então porque carregar a biblioteca todas as vezes?

Carregar uma DLL estaticamente

Não há muito segredo ao carregar uma DLL estaticamente. Porém, visto que em Delphi ou FreePascal podemos gerar um código binário num único executável então porque deveríamos usar uma DLL dessa forma? Realmente não há muitos motivos para isso, mas vamos ao exemplo mesmo assim:

(todo)

Carregar uma DLL dinamicamente

Essa é a principal razão de usar uma DLL no Delphi ou FreePascal, uma vez que você pode carregá-la e descarregá-la após o uso. Muito versátil, por exemplo, no caso de relatórios, pode manter versões diferentes do mesmo relatório ou atualizá-los mantendo o programa principal intacto. Vamos a um exemplo de como carregar e descarregar uma DLL dinamicamente:

procedure TForm1.BtnConsumirClick(Sender: TObject);
type
  TF_TestLogin = function (pParamList:pChar): pChar; stdcall;
var
  F_TestLogin: TF_TestLogin;  // Uma variável para acessar a sub-rotina na DLL
  LibHandle: THandle; // um handle para a DLL
  pResultado:pChar;   // resultado da função é um pchar
  ListaIN:TStringList;   // lista de parametros de entrada
  ListaRecebe:TStringList;  // valores recebidos em forma de lista
  pListaIN:Pchar;        // lista de parametros de entrada em formato pChar
  ErrorMode: UINT;       // codigo de erro caso aconteça
  ErrorMsg:String;       // mensagem de erro que impeça de prosseguir 
const
  DLL='C:\caminho\para\DLL_Teste.dll';
begin
  // variavel que indicará a ocorrência de algum erro
  ErrorMsg:=emptyStr;
  // variavel que conterá o retorno da função dentro da DLL
  pResultado:=pChar(emptyStr);
  // Minha lista que receberá o retorno
  ListaRecebe:=TStringList.Create;
  // Meu parametro de entrada
  ListaIN:=TStringList.Create;
  ListaIN.Values['User_Name']:='meulogin';
  ListaIN.Values['Password'] :='minhasenha';
  ListaIN.Values['Role']     :='RDB$ADMIN';
  // Caminho para a DLL
  if not FileExists(DLL)
    then ErrorMsg:='Arquivo não existe: '+DLL;
  if ErrorMsg=emptyStr then
  begin
    // Desliga OS error messages
    ErrorMode := SetErrorMode(SEM_FAILCRITICALERRORS);
    // Pegar o handle identificador da biblioteca a ser usada
    try
      LibHandle := LoadLibrary(PChar(DLL));
    finally
      SetErrorMode(ErrorMode);
    end;
    if LibHandle  <= HINSTANCE_ERROR
      then ErrorMsg:='Erro ao carregar: '+DLL;
    if ErrorMsg=emptyStr then
    begin
      // Confere se o carregamento da DLL foi bem-sucedido
      if Win32Check(Bool(LibHandle)) then
      begin
        // Atribui o endereço da chamada da subrotina à variável F_TestLogin, mas
        // nesse ponto o FPC e Delphi tem jeitos diferentes:
        {$IFDEF FPC}
          Pointer(F_TestLogin) := GetProcAddress(LibHandle, 'F_TestLogin');
        {$ELSE}
          F_TestLogin := GetProcAddress(LibHandle, 'F_TestLogin');
        {$ENDIF}

        // Verifica se um endereço válido foi retornado
        if @F_TestLogin <> nil then
        begin
          // pegando o resultado
          pListaIN:=pChar(ListaIN.Text);
          pResultado := F_TestLogin(pListaIN);
          // como o resultado é pChar então tenho que converter 
          // para string e jogar numa lista onde fica mais facil
          // processar depois
          ListaRecebe.Text:=String(pResultado);
        end;
      end;
    end;
  end;
  //
  // tudo pronto para usufruir do retorno
  //
  if ErrorMsg=emptyStr then
  begin
    memoResultado.Text:=ListaRecebe.Text;
    if SameText(ListaRecebe.Values['RESULT'],'OK') then
      memoResultado.Lines.Add('Legal que tudo tenha ocorrido bem.')
    else
      memoResultado.Lines.Add('Que chato, parece que o login falhou.');
  end
  else
  begin
    memoResultado.Lines.Add('Não pude chamar a DLL: '+ErrorMsg);
  end;

  // liberando memoria
  F_TestLogin := nil;
  while not FreeLibrary(LibHandle) do
    Sleep(5); // espera 5s para uma nova tentativa
  ListaIN.Free;
  ListaRecebe.Free;
end;

Fatoração de código

Você pode vir a achar que carregar uma DLL dinamicamente é muito verboso, no entanto, raramente vamos usá-lo do jeito que foi mostrado, normalmente vamos fatorar, isto é, criar uma função com o código acima e apenas variando o nome da DLL, os parâmetros de entrada e retornando o resultado conseguido. Ao fatorar uma única vez como por exemplo ChamarRelatorio(‘relatorio_vendas.dll’) evita-se o esforço de copiar/colar em todas as ocorrências praticamente o mesmo código.

Conclusão

Espero que tenha achado o artigo benéfico. O exemplo fornecido funciona em Delphi e também em FreePascal.