Chamando funções de DLLs externas

Escrito em 29 de julho de 2007 em Delphi por Leonel Togniolli

Muitas vezes é necessário chamar código externo a uma aplicação, seja do sistema operacional, código de terceiros em DLLs, ou mesmo seu próprio código modularizado por diversos motivos. Fazer essas chamadas de uma aplicação Delphi é bem simples, mas mesmo assim é interessante conhecer alguns fundamentos:

Uma DLL não possui informação de metadados sobre seu conteúdo. Tudo que ela possui é uma tabela de símbolos exportados que lista os procedimentos dentro dela. Não existe informação sobre número de parâmetros, seus tipos ou propósitos,  valores de retorno, convenção de chamada ou qualquer coisa parecida. A única forma de acessar métodos de uma DLL qualquer é possuir documentação sobre ela, contendo explicitamente a assinatura de funções. Para APIs do Windows, isso é documentado pela Microsoft, enquanto DLLs de terceiros precisam de documentação que as acompanhem.

Mesmo que a DLL tenha sido escrita em outra linguagem, é perfeitamente possível usá-la em Delphi, convertendo a declaração para os tipos equivalentes. Tomando como exemplo uma função da API do Windows, GetLastInputInfo, que retorna o tempo decorrido que o computador está sem uso, podemos ver a declaração dela na MSDN:

typedef struct tagLASTINPUTINFO {
    UINT cbSize;
    DWORD dwTime;
} LASTINPUTINFO, *PLASTINPUTINFO;

BOOL GetLastInputInfo(
    PLASTINPUTINFO plii
);

E a conversão em windows.pas:

type
  PLastInputInfo = ^TLastInputInfo;
  {$EXTERNALSYM tagLASTINPUTINFO}
  tagLASTINPUTINFO = packed record
    cbSize: UINT;
    dwTime: DWORD;
  end;
  TLastInputInfo = tagLASTINPUTINFO;

{$EXTERNALSYM GetLastInputInfo}
function GetLastInputInfo(var plii: TLastInputInfo): BOOL; stdcall;

function GetLastInputInfo; external user32 name 'GetLastInputInfo';

Analizando esse código, podemos ver que a estrutura foi convertida em um packed record. Packed indica que o compilador não deve efetuar otimizações colocando os campos do record em posições alinhadas de memória. Essa não é uma otimização que o compilador C faz, e precisamos ter uma estrutura do mesmo tamanho. Em seguida vemos que o ponteiro sendo passado como parâmetro foi convertido para var, e foi adicionada a convenção de chamada stdcall, o padrão da API do Windows. Finalmente, em vez da implementação da função, uma declaração que ela é external, na user32.dll (user32 é uma constante contendo esse valor). Não se importe com as diretivas EXTERNALSYM – elas são para compatibilidade com C++Builder, indicando que eles já tem essa declaração em outro lugar.

Assim como a tabela de símbolos exportados que mencionei anteriormente, um executável ou DLL no windows possui uma tabela de símbolos importados de outros módulos. Ao utilizarmos GetLastInputInfo no nosso código, ou qualquer outra função declarada como external, o compilador adiciona uma entrada nessa tabela indicando que precisamos dessa função. Durante o carregamento do seu programa, o sistema operacional efetuará a ligação de todos os métodos na tabela de simbolos importados. Isso é chamado de ligação estática, e se qualquer um desses módulos não for encontrado, a sua aplicação não será carregada.

Como em alguns casos movemos código para uma DLL externa justamente para poder distribuí-la em apenas alguns casos, servindo como plugins, isso não é o comportamento esperado. Outros casos onde não queremos essa dependência é quando estamos chamando APIs do Windows que não se encontram em todas as versões. GetLastInputInfo é exatamente uma delas – a documentação indica que ela só existe a partir do Windows 2000. Se utilizarmos a ligação estática, nossa aplicação nem inicializaria no Windows 98, por exemplo.

A solução é utilizar ligação dinâmica – carregar a DLL desejada, verificar se ela possui o procedimento que desejamos e chamá-lo caso exista. Obviamente não é possível chamar um método que não existe no Windows 98 mesmo dessa forma, então é necessário prever esta condição no código.

Para isso, declaramos um tipo procedural, e uma variável desse tipo:

type
  TGetLastInputInfo = function(var plii: TLastInputInfo): BOOL; stdcall;
var
  GetLastInputInfo: TGetLastInputInfo;

A partir daí, basta carregar a user32.dll, verificar se a função desejada está presente, e chamá-la. O código que implementa esta lógica fica assim:

type
  TGetLastInputInfo = function(var plii: TLastInputInfo): BOOL; stdcall;
var
  GetLastInputInfo: TGetLastInputInfo;
  LastInputInfo: TLastInputInfo;
  User32dll: Cardinal;
begin
  User32dll := LoadLibrary(user32);
  GetLastInputInfo := GetProcAddress(User32dll, 'GetLastInputInfo');
  if Assigned(GetLastInputInfo) then
  begin
    GetLastInputInfo(LastInputInfo);
    // processar LastInputInfo
  end;
end;

Desta forma não temos uma ligação estática, podendo seguir um outro caminho em sistemas não suportados.


3 comentários em 'Chamando funções de DLLs externas' »

Assine os comentários usando RSS ou faça um TrackBack para 'Chamando funções de DLLs externas'.

[-]
[+]
José Otavio disse,

Em 07 de janeiro de 2008 às 10:19

Parabéns pelo artigo… me ajudou muito!

 
[-]
[+]
CJ disse,

Em 08 de janeiro de 2009 às 09:43

Muito bom,
só um detalhe: para analisarmos o retorno é necessário estabelecer o retorno, tipo: LastInputInfo.dwTime.
Porém quando em inatividade ele retorna o mesmo valor constantemente, é assim mesmo?

[-]
[+]

Em 08 de janeiro de 2009 às 13:25

Isso. Segundo a documentação, esse campo é o momento da ultima atividade:

http://msdn.microsoft.com/en-us/library/ms646272(VS.85).aspx