DLL, string e PChar

Compartilhar/Favoritos

Artigo sobre algumas dificuldades que os programadores enfrentam ao decidirem não usar a unidade ShareMem para passar strings entre o aplicativo e DLLs. O artigo traz exemplos de dois métodos interessantes para facilitar a transferência de strings nesse caso.

Este é de um grupo de posts muito antigos, de 2002. Vêm do meu antigo site, quando ainda trabalhava com Delphi e tinha um K6-2(!!!!). Bons tempos aqueles…

Provavelmente não serve para muita coisa hoje, mas é legal ver como as coisas eram. Além disso, ainda há gente acessando!

Lá vai…

Você saberia se virar sem a unidade Sharemem ao criar suas DLLs?

Este artigo descreve um pouco do funcionamento da unidade Sharemem e do gerenciador de memória do Delphi e apresenta duas formas criar DLLs sem ela.

Os exemplos foram escritos para Delphi 5.

Download

Para pegar os exemplos mostrados no decorrer deste artigo, clique aqui (14KB compactado).

Funcionamento de Sharemem e BORLANDMM.DLL

Este “capítulo” descreve um pouco do funcionamento do sistema de alocação de strings e de gerenciamento de memória com e sem a unidade Sharemem. Se não estiver interessado nisso, pode ignorá-lo e ir direto ao próximo.

O problema em usar strings e outras variáveis com processamento especial de alocação e desalocação com DLLs é o gerenciamento de memória.

Primeiro, um pouco sobre long strings.

Como você deve saber, uma string longa não é muito mais que um ponteiro para um local na memória. Este local tem algumas informações importantes a respeito da string, além, é claro, dos caracteres armazenados no momento.

Além dos caracteres, há um inteiro de 32 bits indicando o comprimento da string. Ao contrário de PChar, o Delphi pode ler o tamanho da string sem precisar percorrê-la e procurar pelo caractere #0. Além disso, uma string longa pode conter caracteres #0 válidos, ou seja, que não indiquem o final da string, mas algo que você possa interpretar como quiser.

Por fim, existe o campo reference count, outro inteiro de 32 bits indicando o número de variáveis que estão apontando no momento para aquele local na memória. Isso permite ao Delphi fazer algumas otimizações na atribuição de variáveis string.

Quando você atribui um valor a uma variável string, o Delphi normalmente aloca o espaço em memória necessário ao armazenamento dos caracteres, preenche os campos que indicam o comprimento e reference count e copia os caracteres para esse local na memória.

Quando a execução sair do escopo da variável (por exemplo sair da função onde existe uma variável local do tipo string), o Delphi trata de zerar as variáveis string e verificar se precisa desalocar o espaço onde os caracteres se encontram. Se precisar, utiliza a função de desalocação do gerenciador de memória.

O gerenciador de memória é uma biblioteca (localizada na unidade System) que trata de alocação, desalocação e realocação de memória no Delphi. Veja GetMem, FreeMem e ReallocMem para saber mais sobre essas operações.

O problema é que um executável host e as DLLs ligadas à ele têm seus próprios gerenciadores de memória. Uma porção de memória alocada em um deles não pode ser liberado em outro. Veja o diagrama abaixo.

Gerenciamento simples de memória

Apesar dos gerenciadores serem idênticos (ambos implementados na unidade System), a “instância” é diferente. Todas as variáveis e áreas de controle, etc, estão localizadas em locais diferentes.

Quando a unidade Sharemem é incluída em um executável e suas DLLs, ela trata de substituir o gerenciador de memória padrão para um localizado em BORLANDMM.DLL. O Delphi permite essa alteração do gerenciador de memória através da procedure SetMemoryManager.

Assim, qualquer porção de memória (incluindo strings) alocada em um pode ser desalocada em outro.

Gerenciamento de memória com BORLANDMM.DLL

Desta forma, o gerenciador padrão para o executável e suas DLLs são ignorados, dando lugar a um gerenciador compartilhado. Ele é compartilhado também por qualquer outro aplicativo que utilizar Sharemem.

Método 1: Procedure de liberação de PChar

Esse método se baseia na implementação, na DLL, de uma procedure de liberação de PChars retornados por ela. É uma procedure bastante simples, parecida com StrDispose do Delphi:

procedure DllStrDispose(Str: PChar);

Vantagens

  • Simplifica o processo de preparação para as chamadas. Não é necessário pré-alocar um PChar e nem chamar a função antes para retornar o tamanho necessário. Por só precisar chamar a função uma vez, evita processamento repetido.
  • Esse método pode ser usado para a criação de DLLs para uso com qualquer linguagem (Delphi, C/C++, VB, etc).

Desvantagens

  • Sempre que a DLL retornar uma PChar, o programa deve se encarregar de liberá-la ao final do uso. Isso não chega a ser uma desvantagem pois isso é uma consequência do uso de PChar.
  • Não é prático se você utiliza várias DLLs. É interessante para aplicativos com só uma DLL.

Como mencionado, cada DLL deve exportar sua função de desalocação de PChar, o que pode tornar o programa confuso se você precisar usar várias. Cada uma teria que ter um nome diferente e isso poderia criar algumas confusões como tentar desalocar uma PChar criada por uma DLL com a procedure de liberação de outra. Isso, claro, iria gerar uma exceção.

Vamos ao exemplo de uma DLL criada com este método:

library StrDLL2;
 
uses
  SysUtils,
  Classes;
 
{ Aloca um PChar e copia os caracteres da string AString para ele }
function DllStrGet(AString: string): PChar;
var
  Size: Integer;
begin
  Size := Length(AString) + 1;
  Result := StrAlloc(Size); { Aloca usando o gerenciador de memória da DLL }
  Move(Pointer(AString)^, Result^, Size);
end;
 
{ Função de liberação das PChars retornadas pela DLL }
procedure DllStrFree(Str: PChar); stdcall;
begin
  StrDispose(Str); { Libera usando o gerenciador de memória da DLL }
end;
 
{ Função de demonstração }
function GetAutoexec: PChar; stdcall;
var
  SL: TStringList;
begin
  SL := TStringList.Create;
  try
    SL.LoadFromFile('c:\autoexec.bat');
    Result := DllStrGet(SL.Text);
  finally
    SL.Free;
  end;
end;
 
exports
  DllStrFree, GetAutoexec;
 
begin
end.

A função de alocação e cópia da string (DllStrGet) é usada só internamente, sempre que você for retornar uma string. Se quiser, você pode tranquilamente alocar a PChar usando diretamente StrAlloc ou outras funções do Delphi.

Para usar as funções da DLL em seu aplicativo, você deve primeiro declarar as funções:

function GetAutoexec: PChar; stdcall;; stdcall; external 'StrDLL2.dll';
procedure DllStrFree(Str: PChar); stdcall; external 'StrDLL2.dll';

Para chamar as funções, faça como abaixo:

procedure ShowAutoexec;
var
  P: PChar;
begin
  P := GetAutoexec;
  try
    ShowMessage(P);
  finally
    DllStrFree(P);
  end;
end;

O bloco try..finally foi usado para garantir a desalocação da variável. Se ocorresse exceção entre a chamada de GetAutoexec e DllStrFree, esta não seria chamada, fazendo com que o PChar retornado não fosse desalocado.

Método 2: Interface IString

Este segundo método é muito mais interessante que o primeiro. Ele tem todas as vantagens dele, além de:

  • Nenhuma função de desalocação precisa ser chamada. A desalocação é feita automaticamente pelo Delphi, mesmo utilizando PChar.
  • Pode ser usada em várias DLLs sem causar confusão.

A única desvantagem é com relação à performance. O processo de transferência de strings é um pouco mais lento do que com strings longas.

A solução baseia-se nas facilidades que o Delphi apresenta com relação a interfaces COM. Iremos utilizar duas unidades novas. Uma delas será usada tanto no aplicativo host quanto nas DLLs e a outra, só nas DLLs.

A primeira é a declaração de uma interface que chamaremos de IString. Em sua declaração, existe só um método, o Str, que deve retornar um PChar:

unit StrInt;
 
interface
 
type
  IString = interface(IUnknown)
    ['{6ADF6275-82C0-4290-8C8D-DFDAA2AA17CC}']
    function Str: PChar;
  end;
 
implementation
 
end.

A segunda é a impementação de um objeto (TString) que implementa uma interface IString:

unit StrIntImp;
 
interface
 
uses SysUtils, StrInt;
 
type
  TString = class(TInterfacedObject, IString)
  private
    FStr: PChar;
    { IString }
    function Str: PChar;
  public
    constructor Create(AString: string); overload;
    destructor Destroy; override;
  end;
 
implementation
 
{ TString }
 
constructor TString.Create(AString: string);
var
  Size: Integer;
begin
  inherited Create;
  Size := Length(AString) + 1;
  FStr := StrAlloc(Size);
  Move(Pointer(AString)^, FStr^, Size);
end;
 
destructor TString.Destroy;
begin
  StrDispose(FStr);
  inherited;
end;
 
function TString.Str: PChar;
begin
  Result := FStr;
end;
 
end.

Ao implementar a DLL, sempre que quiser retornar uma string, você deve fazê-lo através de IString, e não mais PChar. Exemplo:

library strdll;
 
uses
  SysUtils,
  Classes,
  StrIntImp,
  StrInt;
 
{$R *.RES}
 
function GetAutoexec: IString; stdcall;
var
  S: TStringList;
begin
  S := TStringList.Create;
  try
    S.LoadFromFile('c:\autoexec.bat');
    Result := TString.Create(S.Text);
  finally
    S.Free;
  end;
end;
 
exports
  GetAutoexec;
 
begin
end.

Perceba, então, que onde se retornaria um PChar, retornamos um IString criado através de TString.Create, que tem como parâmetro a string longa original que queremos retornar.

Para usar uma DLL criada com este método em um aplicativo host, você deve incluir na cláusula uses de todas as unidades que usarem a DLL a unidade StrInt e usar o valor retornado de forma parecida com o seguinte:

procedure ShowAutoexec;
var
  I: IString;
begin
  I := GetAutoexec;
  ShowMessage(I.Str);
end;

Note que podemos usar também a seguinte forma para fazer a mesma coisa:

procedure ShowAutoexec;
begin
  ShowMessage(GetAutoexec.Str);
end;

Perceba que a utilização é muito mais simples que quando com o primeiro método (o que usa uma procedure para liberar o PChar).

Para entender o funcionamento desse método, você deve ter um pouco de conhecimento de interfaces COM. Se quiser, pode pular os parágrafos seguintes.

O método baseia-se na chamada automática do método Release das interfaces quando a execução sai do escopo de uma variável do tipo interface COM. Ao sair do escopo de uma dessas variáveis o Delphi zera ela e chama o método Release automaticamente, de forma semelhante à liberação de strings.

Aproveitamos isso, então, para criarmos um objeto que implementa uma interface especial (IString). O objeto (TString) mantém uma PChar apontada para a string. Quando o objeto é destruído, ele automaticamente libera a variável PChar.

Vamos analisar o que ocorre no exemplo acima.

Quando o aplicativo chama a função GetAutoexec, esta (que está na DLL), cria uma string e a usa como parâmetro para a criação de um objeto TString. O construtor desse objeto, então, aloca o espaço necessário e copia os caracteres da string para lá. GetAutoexec retorna a interface IString associada ao objeto (o Delphi faz automaticamente a “conversão” entre TString e IString baseado no tipo que deve ser retornado por GetAutoexec).

No aplicativo host, usamos o método Str da interface como parâmetro para ShowMessage. Ao final da procedure ShowAutoexec, o Delphi executa automaticamente o método Release da interface IString.

Perceba que a classe TString é derivada de TInterfacedObject. Ela implementa os métodos de IUnknown de forma que o último Release destrua o objeto. Assim, quando o método Release é chamado ao final de ShowAutoexec, o objeto verifica se não existe mais nenhuma outra variável apontada para ele e executa o destrutor do objeto que, por suz vez, libera a PChar.

Claro que você não precisa entender isso para usar o método. Basta seguir os exemplos.

Importante

Só utilize estes métodos se realmente não quiser incluir a unidade Sharemem e a DLL BORLANDMM.DLL em seu projeto. A inclusão desses componentes facilita bastante a comunicação entre seu aplicativo e as DLLs criadas para ele, visto que as operações com strings longas em Delphi são bastante facilitadas pela linguagem.

You may also like...

1 Response

  1. Josenilson disse:

    Bom dia Cristiano.

    Ao utilizar o Método 2: Interface IString. Sempre ocorre erro no retorno da DLL quando utilizada em Java.

    Tem ideia o que pode ser?

Deixe uma resposta para Josenilson Cancelar resposta

O seu endereço de email não será publicado Campos obrigatórios são marcados *

Você pode usar estas tags e atributos de HTML: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

Translate »