quarta-feira, 29 de maio de 2013

Verificando a versão de bibliotecas e programas em Delphi

Um dos aspectos mais complicados de se gerenciar na entrega (deploy) de uma nova release de um programa é garantir a compatibilidade entre as bibliotecas DLL e outros executáveis que compõem a solução. Isso é particularmente verdade se esses arquivos também podem ser entregues de forma independente e/ou em pastas separadas, como em geral ocorre com os pacotes BPLs que compõem soluções em Delphi ou C++ Builder.

A ABC71 adota uma estratégia interessante para minimizar esse tipo de problema. Quando construímos aplicações Windows, sejam programas ou bibliotecas, podemos adicionar ao arquivo gerado informações extras úteis: os recursos. As informações incluídas como recursos num executável podem ser facilmente recuperadas via programação, do mesmo modo que o Windows Explorer faz quando apresenta a guia Versão nas propriedades de um arquivo.

O ERP comercializado pela ABC71 é composto por dezenas de bibliotecas e pacotes independentes, contendo as diversas funcionalidades do sistema. A ideia é incluir um número de versão em todos os executáveis, através de recursos do Windows. Como cada arquivo é carregado dinamicamente conforme a necessidade, podemos utilizar o momento da carga para verificar se a versão do arquivo é compatível com o programa e, em caso negativo, notificar o usuário para que ele atualize os arquivos. Esse mecanismo pode ser aplicado também a bibliotecas de terceiros, como o ADO ou pacotes de componentes visuais.

Para implementar uma verificação nesses moldes em suas próprias bibliotecas, o primeiro passo é adicionar a elas um arquivo de recursos do Windows com informações relevantes sobre a versão atual. O quadro a seguir mostra um trecho do arquivo RC que adicionamos a nossos projetos:
#define DATARELEASE "28/05/2013\0"
#define VERSAO "26.9.5.6\0"
#define VERSAO_DB "9.5\0"

#ifdef WCOMP1
#define DESCRICAO "Componentes auxiliares\0"
#define NOME_INTERNO "WComp1\0"
#else
#define DESCRICAO "ERP Omega\0"
#define NOME_INTERNO "Omega\0"
#endif

VS_VERSION_INFO VERSIONINFO
FILEVERSION 26,9,5,6
PRODUCTVERSION 26,9,5,6
FILEFLAGSMASK 0x3fL
FILEFLAGS 0x8L
FILEOS 0x4L
FILETYPE 0x1L
FILESUBTYPE 0x0L
BEGIN
BLOCK "StringFileInfo"
BEGIN
BLOCK "040904b0"
BEGIN
VALUE "Comments",DESCRICAO
VALUE "CompanyName","ABC71 Soluções em Informática\0"
VALUE "InternalName", NOME_INTERNO
VALUE "LegalCopyright", "2013 \xA9 ABC71 Soluções em Informática\0"
VALUE "ProductName", "ERP OMEGA\0"
VALUE "ProductVersion", VERSAO
VALUE "FileDescription", "ERP OMEGA\0"
VALUE "FileVersion", VERSAO
VALUE "VersaoBaseDados", VERSAO_DB
VALUE "DataRelease", DATARELEASE
END
END
BLOCK "VarFileInfo"
BEGIN
VALUE "Translation", 0x409, 1200
END
END
O exemplo usa compilação condicional para diferenciar os diversos projetos, permitindo reaproveitar o mesmo arquivo em todos eles. Isso elimina a necessidade de alterar cada um deles quando a versão (ou outra informação) muda; basta recompilá-los com a versão nova do arquivo de recursos. Note que a sintaxe do arquivo lembra mais a do C/C++ do que a do Pascal/Delphi, incluindo os IFDEF e o terminador nulo ao fim de cada texto fixo.

Uma vez incluídas essas informações em cada executável, pacote e biblioteca, o passo seguinte é extrai-las para comparar com as existentes no programa que está carregando o pacote/biblioteca. A API do Windows possui uma série de funções para essa tarefa, agrupadas sob o nome de Version Information Functions. O quadro abaixo mostra uma função simples usando essa API; ela é capaz de recuperar qualquer uma das informações contidas no arquivo de recursos.
function GetStringValue (AHandle: HMODULE; ATexto: String) : String;
var lIgnore, lTamInfo: DWORD;
lBuffer: LPVOID;
lInfo: PChar;
lPath, lTrans: String;
lModulo: array[0..MAX_PATH] of Char;
begin
{ Obtem o nome do executável, DLL ou pacote cujo handle foi informado no parâmetro }
GetModuleFileName (AHandle, lModulo, sizeof(lModulo));

{ Calcula o tamanho que deve ter o buffer para recuperar as informações de versão }
lTamInfo := GetFileVersionInfoSize(lModulo, lIgnore);
GetMem (lBuffer, lTamInfo);

{ Recupera as informações no buffer alocado }
if GetFileVersionInfo(lModulo, lIgnore, lTamInfo, lBuffer)
then begin
{ Lingua 0409 com code page 04B0. No arquivo de recurso desse exemplo existe apenas textos nessa lingua.}
lTrans := '040904B0';

{ Monta o caminho onde está o texto procurado no buffer }
lPath := '\StringFileInfo\' + lTrans + '\' + ATexto;

{ Recupera o valor do texto solicitado }
if VerQueryValue (lBuffer, pChar(lPath), Pointer (lInfo), lTamInfo)
then
Result := lInfo
else
Result := '';
end;
FreeMem(lBuffer);
end;
Pelo código no quadro, observamos três etapas distintas para obter o valor desejado. Primeiro, a função GetFileVersionInfoSize nos reporta o tamanho do bloco de informações de versão contido no módulo (executável, pacote ou DLL). Com essa informação, podemos alocar a memória necessária para ler o bloco todo.

Na segunda estapa, a função GetFileVersionInfo extrai o bloco de informações e o coloca na memória que alocamos anteriormente. Finalmente, o programa busca o texto solicitado montando um caminho e chamando a função VerQueryValue. Um arquivo de recursos pode ser organizado com blocos de textos em línguas específicas (português, inglês, etc.) de modo que o conteúdo apropriado pode ser exibido de acordo com as preferências do usuário; por questão de simplicidade, esse exemplo trata apenas uma língua, motivo pelo qual foi possível manter a busca fixa, sem me preocupar em levantar as línguas incluídas.

Com essa função, podemos obter, por exemplo, a data de release do módulo usando o seguinte código:
var data: String;
begin
{ Obtem a data de release contida no programa atual }
data := GetStringValue (0, 'DataRelease');
Para implementar a verificação de compatibilidade de versão, podemos incluir código similar ao anterior na área de initialization numa unit de um pacote:
var dataP, dataE : String;

initialization

dataE := GetStringValue (0, 'DataRelease');
dataP := GetStringValue (HInstance, 'DataRelease');

if (dataE <> dataP) then begin
ShowMessage ('Este pacote é incompatível com o executável.'#13#10 +
' Data do Pacote : ' + dataP + #13#10 +
' Data do Executável : ' + dataE );
ExitProcess(-1);
end;
end.
Com isso, quando nosso programa carregar esse pacote, o código acima obterá o campo DataRelease tanto do executável quanto do próprio pacote e, se não forem compatíveis, o programa é abortado. O exemplo é bastante simples mas a solução pode ser usada para contemplar informações mais complexas, como um número mínimo de versão ou release. Essa técnica também é aplicável às DLLs.

Há um artigo neste endereço do site delphiDabbler dando explicações mais detalhadas sobre o funcionamento das APIs de versão do Windows. Ele também constrói uma classe para encapsular a leitura dessas informações, facilitando bastante o processo de extrair aquelas que interessam para a validação de compatibilidade.

segunda-feira, 22 de abril de 2013

Estruturas de Dados com Delphi - parte II : Pilhas

No meu último post, comecei a falar sobre estruturas de dados e como o Delphi lida com as estruturas mais comuns, como as filas. Neste post, eu mostro o conceito de outra das estruturas padronizadas que o Delphi trata de forma inerente : pilhas.

A diferença central entre filas e pilhas está na ordem em que os elementos são retirados da estrutura. No caso das filas, o primeiro elemento inserido será o primeiro a ser retirado, mecanismo conhecido como FIFO (First In, First Out). Para as pilhas, o último elemento inserido é retirado primeiro; isto é, pilhas implementam o LIFO (Last In, First Out). Para compreender esse conceito, imagine que você guarde os pratos de sua casa empilhados no armário. Sempre que você precisa de um prato, retira o que está no topo da pilha. Quando vai guardá-los, os pratos são postos no topo da pilha novamente, disponíveis para o próximo uso.

Uma situação computacional prática onde o uso de pilhas se adequa é o cálculo do custo de um produto fabricado. Por exemplo, para calcular o custo de fabricação de um computador, temos que calcular o custo da CPU, do monitor e do teclado. No entanto, obter o custo da CPU requer que se calcule antes o custo da placa-mãe, transistores, capacitores, etc. A placa-mãe também é composta por outros itens, e assim sucessivamente.

Nessa estrutura, podemos somar o custo individual de cada componente até encontrar um que também seja composto. O item composto é incluído no topo da pilha enquanto se calcula seu custo. Tal processo deve ser feito recursivamente até que haja apenas itens simples. Nesse momento, o custo do último item adicionado à pilha está determinado e ele pode ser removido da pilha, prosseguindo o cálculo com o item seguinte.

Para implementarmos com as classes nativas do Delphi uma solução para essa situação, considere as classes abaixo:
TWProduto = class
public
Codigo: String;
Custo : Double;
Componentes: TList;

Constructor Create(ACodigo:String;ACusto: Double);
Destructor Destroy;override;
End;

TWContexto=class
public
produto: TWProduto;
indice : integer;
custo : double;

Constructor Create(AProduto: TWProduto);
end;

{ ... }

Constructor TWProduto.Create(ACodigo:String;ACusto: Double);
begin
Codigo := ACodigo;
Custo := ACusto;
Componentes:= TList.Create;
end;

Destructor TWProduto.Destroy;
begin
{ ... }
Componentes.Free;
inherited;
end;

Constructor TWContexto.Create(AProduto: TWProduto);
begin
produto := AProduto;
indice := 0; { iniciar cálculo nesse índice }
custo := 0.0; { custo inicial do produto }
end;
A primeira classe (TWProduto) representa um produto que pode ser composto de outros produtos, incluindo outros produtos compostos. Já a classe TWContexto serve para controlar o cálculo do custo de um único produto composto. Além do próprio produto, ela armazena o índice do último componente considerado pelos cálculos bem como o custo obtido até o momento.

No quadro a seguir, eu simulo manualmente a montagem simplificada de um computador. O resultado é um exemplo de estruturação complexa usando a classe de produto descrita acima e cujo custo poderá ser calculado com uma rotina usando pilha:
function TForm1.MontaComputador : TWProduto;
var computador, compon, subcompon: TWProduto;
begin
computador := TWProduto.Create('Computador', 0.0);

compon := TWProduto.Create('Teclado', 20.0);
computador.Componentes.Add(compon);

compon := TWProduto.Create('Monitor', 235.0);
computador.Componentes.Add(compon);

{ A CPU é um produto composto }
compon := TWProduto.Create('CPU', 0.0);
subcompon := TWProduto.Create('Placa-mãe', 250.0);
compon.Componentes.Add(subcompon);
subcompon := TWProduto.Create('Cooler', 32.0);
compon.Componentes.Add(subcompon);
computador.Componentes.Add(compon);

Result := Computador;
end;
A rotina abaixo usa pilha para controlar o cálculo do custo de um produto - no caso, o computador estruturado no quadro anterior. Em Delphi, a classe TStack implementa as operações necessárias para se trabalhar com uma pilha.
procedure TForm1.btnCalcularCustoClick(Sender: TObject);
var i : integer;
interromper : boolean;
Pilha: TStack;
contexto, ctxAux : TWContexto;
computador : TWProduto;
begin
computador := MontaComputador;
contexto := TWContexto.Create (computador);

{ Inicia a pilha com o produto original no contexto : o computador }
Pilha:= TStack.Create;
Pilha.Push(contexto);

while (pilha.Count > 0) do begin
{ Obtém o contexto no topo, sem removê-lo }
contexto := pilha.Peek;
compon := contexto.produto;
i := contexto.indice;

{ Percorre os componentes desse produto, somando o custo de cada um para obter o custo do total do produto em si }
interromper := false;
while (i < compon.Componentes.Count) And (not interromper) do begin
subcompon := compon.Componentes.Items[i];

{ Esse produto é composto ? }
interromper := (subcompon.Componentes.Count > 0);
if (interromper) then begin
{ Inclui o subcomponente composto no topo da pilha pra calcular seu custo }
ctxAux := TWContexto.Create (subcompon);
Pilha.Push(ctxAux);

{ Quando desempilhar esse produto, inicie no componente seguinte }
contexto.indice := i + 1;
end
else
contexto.custo := contexto.custo + subcompon.Custo;

Inc (i);
end;

{ Conseguiu totalizar o custo do produto; então, remove-o do topo da pilha }
if not interromper then begin
Pilha.Pop;
compon.Custo := contexto.custo;
contexto.Free;

{ Adiciona o custo calculado ao custo do produto do qual esse componente é parte }
if pilha.Count > 0 then begin
contexto := pilha.Peek;
contexto.custo := contexto.custo + compon.Custo;
end;
end;
end;

{ Apresenta o custo obtido }
lblCusto.Caption := FormatFloat ('#,##0.00', computador.Custo);

Pilha.Free;
computador.Free;
end;
Assim como no caso da fila tratado no outro post, o constructor da pilha exige a especificação do tipo de dado com o qual a pilha será capaz de trabalhar. No exemnplo, a classe TWContexto é informada para que possamos controlar os produtos que já foram considerados no cálculo do custo.

As funções mais importantes na pilha são a que acrescenta e a que remove um elemento. Para inserir um novo elemento no topo da pilha, use o método Push e para extraí-lo use Pop, como mostrado no código acima.

É possível ainda obter o elemento que está no topo da pilha sem, no entanto, extraí-lo. Para isso, há o método Peek, também utilizado no exemplo.

Embora não tenha sido necessário aqui, o TStack permite interceptar a adição e remoção de elementos através do evento OnNotify. Ele é frequentemente utilizado para liberar a memória associada aos elementos da pilha.

terça-feira, 26 de março de 2013

Estruturas de Dados com Delphi - parte I : Filas

Em maior ou menor grau, o trabalho de um programador de computadores envolve lidar com meios de organizar os dados internos de seus programas. Uma organização bem planejada é imprescindível para a implementação de algorítmos eficientes, o que afeta tanto a performance da aplicação quanto sua facilidade de manutenção. As formas de se organizar os dados em um programa são chamadas de Estruturas de Dados.

Pela sua importância, a maioria das linguagens de programação oferecem bibliotecas com implementações genéricas pré fabricadas para as formas tradicionais de organização de dados, tais como listas, pilhas e filas. No caso do Delphi, esses e outros mecanismos estão disponíveis na unit System.Generics.Collections. Neste post, começo a mostrar as principais estruturas existentes nessa biblioteca, complementando a explicação com exemplos práticos em Delphi.

Em primeiro lugar, precisamos compreender como funciona cada uma das estruturas para podermos decidir qual a mais apropriada para cada situação.

Por exemplo, as filas são desenhadas para o cenário onde precisamos tratar uma sequência de valores exatamente na mesma ordem em que esses valores vão surgindo. É o conceito da fila do caixa em uma loja: cada cliente é atendido sequencialmente de acordo com sua posição na fila; novos clientes entram no final da fila para serem atendidos. Em programação, esse conceito pode ser aplicado ao tratamento de requisições enviadas para execução num sistema. O quadro abaixo traz a declaração básica de uma classe Delphi representando uma requisição executável:
TWRequis = class
public
procedure Execute;virtual;abstract;
function Terminou : Boolean;virtual;abstract;
End;
As Collecions definidas pelo Delphi são estruturas de dados genéricas, mais ou menos como as STL do C++. Isso significa que, quando declaramos uma instância dessas estruturas, devemos informar o tipo de dado que essa Collection em particular vai tratar. Para deixar mais claro, veja a declaração da classe que controlará a execução de requisições em nosso exemplo de fila:
TWVerifRequis = class
{ ... }
public
Fila: TQueue<TWRequis>;
procedure DoOnNotify (Sender: TObject; const Item: TWRequis; Action: TCollectionNotification);

constructor Create;
destructor Destroy;override;
procedure InsereRequis(ATipo: integer);
procedure Start;
end;
No exemplo, declarei a fila associada à classe de requisição. Se for necessário, também é permitido associar a tipos atômicos (como integer e string) ou a estruturas (record). Agora, vamos dar uma olhada no construtor da classe:
constructor TWVerifRequis.Create;
begin
Fila := TQueue<TWRequis>.Create();
Fila.OnNotify := DoOnNotify;
end;
Veja que também ao criar uma instância da fila (o TQueue) devemos especificar o tipo de dado com o qual ela está apta a trabalhar. Um outro detalhe é o evento OnNotify; interceptá-lo nos permite reagir a alterações na fila, tais como saber que um novo registro foi incluído ou acabou de ser removido. Em ambos os casos, podemos atualizar o status da fila para o usuário, avisando-o quantos registros restam e qual requisição está sendo processada. Repare ainda que a assinatura do evento reflete o tipo de dado que nossa fila trata, restringindo a resposta do OnNotify a este tipo específico:
procedure TWVerifRequis.DoOnNotify (Sender: TObject;const Item: TWRequis; Action: TCollectionNotification);
begin
{ A requisição que está no início da fila foi removida; então, ela deve ser executada }
if Action = cnRemoved then begin
{ Atualiza o status, notificando o usuário sobre que requisição está em processamento }
NotificaRequisAtual(Item);
Item.Execute;
Item.Free;
end;

{ Atualiza o status, notificando o usuário sobre quantas requisições restam na fila p/ executar }
NotificaQtd (Fila.Count);
end;
As funções mais importantes para essa estrutura de dado são a que acrescenta e a que remove um elemento da fila. Para incluir um novo item ao fim da fila, use o método Enqueue e para remover o elemento que está no início da fila, use Dequeue, como mostra o exemplo a seguir:
procedure TWVerifRequis.InsereRequis(ATipo: integer);
var lRequis : TWRequis;
begin
{ Invoca a factory para criar a requisição correta de acordo com o tipo informado }
lRequis := CriaNovaRequisicao(ATipo);

{ Acrescenta ao fim da lista a nova requsição criada }
Fila.Enqueue(lRequis);
end;

procedure TWVerifRequis.Start;
var lRequis : TWRequis;
begin
{ Enquanto não foi solicitado o término da execução, continua monitorando a fila }
while Not Terminou() do begin
{ Se há elemento na lista, remove-o aqui. Isso dispara o evento OnNotify, permitindo a execução dessa requisição }
if (Fila.Count > 0) then
lRequis := Fila.Dequeue
else
Sleep(1000);
end;
end;
A requisição poderia ter sido executada imediatamente após a chamada ao Dequeue, ao invés de dentro do evento OnNotify. Ambas são soluções aceitáveis e, portanto, optar por uma ou outra abordagem é uma questão de gosto.

Uma última consideração: se, por alguma razão, for preciso descobrir quem é o próximo item da fila sem, no entanto, removê-lo, use o método Extract. Uma aplicação disso é mostrar no status informações sobre a requisição que está aguardando para ser executada.

Esse exemplo foi construído com o Delphi XE2; não encontrei informação a respeito da presença da biblioteca Collection em versões anteriores ou em qual versão ela foi introduzida. No próximo post, falo sobre pilhas (ou stacks) e suas aplicações.