Resolvendo o problema dos breakpoints fora de sincronia

Categoria: Artigos
Categoria Pai: Addicted 2 Delphi!
Acessos: 9248
Imagem meramente ilustrativa

Se você nunca viu este problema acontecer, você é um felizardo que não deve ter entendido bem o que significa um "Breakpoint fora de sincronia", mas vou tentar explicar.

Ao compilar um programa com informações de depuração[1] aplicadas, cada linha efetivamente compilada fica marcada com uma bola azul na margem esquerda do editor. Ao pressionar F5 em cima de uma destas linhas você adiciona um Breakpoint, ou seja, um ponto de parada, o que significa que durante a depuração, imediatamente antes da linha em questão ser executada, o depurador do Delphi vai parar a execução do programa e permitir que você execute uma série de testes nos valores de variáveis e execute linha após linha, por exemplo. Ao fazer isso, a bola que era azul se torna vermelha e a linha fica destacada, para indicar o Breakpoint. Bom, eu tenho certeza que você sabe como funciona um depurador, mas preciso destacar uma coisa que eu disse: cada linha efetivamente compilada fica marcada com uma bola azul. Esta afirmação é importantíssima para entender o que é um Breakpoint fora de sincronia.

Uma linha efetivamente compilada é uma linha que será executada durante a execução do programa. Estas linhas evidentemente devem SEMPRE POSSUIR CÓDIGO VÁLIDO, porém o que acontece é que quando os breakpoints ficam fora de sincronia, linhas vazias, comentários e partes do código que são inatingíveis[2] aparecem como válidas para depuração e linhas efetivamente válidas para a depuração aparecem como não compiladas (não possuem a bola azul). Abaixo está um exemplo disso.

Na imagem acima se pode ver que existem linhas consideradas compiláveis (marcadas com bolas azuis) pelo depurador, mas que de fato não são, como por exemplo o primeiro comentário, cuja linha está marcada como compilável, porém sabemos que não é. Podemos observar também que existem linhas em branco consideradas compiláveis, e linhas com código válido que aparecem como não compiláveis (não tem as bolas azuis).

Agora você deve estar se perguntando se isso significa que meu programa não vai funcionar corretamente e a resposta é, NÃO, SEU PROGRAMA VAI FUNCIONAR DA FORMA COMO VOCÊ O ESCREVEU! O efeito dos Breakpoints (BP) fora de sincronia é apenas visual. Por mais que você veja um BP numa linha que não possui código válido, não é aquela linha que está sendo executada no momento, é outra! É por isso que esse problema se chama BP fora de sincronia, porque ele afeta apenas a forma como o depurador mostra as linhas consideradas válidas. Novamente, se você nunca passou por esse problema eu preciso dizer que isso é extremamente inconveniente e atrapalha MUITO o desenvolvimento e depuração de um programa, porque você fica impossibilitado de testá-lo corretamente por não saber com exatidão qual linha está realmente executando.


O que causa um BP fora de sincronia?

Eu identifiquei duas causas para esse problema: caracteres de final de linha misturados e arquivo fonte (.pas) com binário correspondente (.dcu) diferente. Imagino que existam apenas estas duas causas para este problema. Se você conhecer outras, deixa um comentário no final do artigo.

Caracteres de final de linha misturados

Muita gente (incluindo eu) adora encontrar códigos prontos na web e normalmente não hesitamos em copiar, colar e rodar. Não há problema algum nisso, a não ser que você não pare para analisar o que faz o código a fim de tentar aprender alguma coisa nova, porém isso é outro assunto. O problema de copiar código da web, e isso inclui também textos quaisquer, é que muitos destes códigos foram escritos em outros sistemas operacionais, os quais possuem caracteres marcadores de fim de linha diferentes.

Observe que nos 3 maiores sistemas operacionais os caracteres de final de linha são os mesmos, o que muda de um OS para outro é apenas a quantidade utilizada destes caracteres (2 no Windows, 1 no Unix e Mac) e qual caractere usar no caso de apenas um ser necessário (Unix e Mac).

Por padrão, no Windows, quando você usa o Delphi, todos os arquivos novos criados possuem os terminadores padrão do Windows (CRLF) e isso não pode ser alterado no próprio Delphi, porém se você usar o Notepad++ e sua opção para converter os finais de linha (Menu Editar > Conversão Final de Linha), você pode converter os finais de linha para o formato usado no UNIX (LF) ou no MAC (CR). Ao abrir um arquivo com formato do UNIX ou MAC no Delphi tudo funcionará perfeitamente[3], inclusive, ao se pressionar ENTER no fonte, serão inserido os caracteres corretos, de acordo com os demais, isto é, se um arquivo no formato MAC estiver aberto e você pressionar ENTER, será inserido apenas um caractere CR.

Vimos que o Delphi lida corretamente com os 3 formatos de fim de linha, então, qual o problema? O problema é quando você MISTURA, num mesmo código finais de linha distintos, ou seja, parte do código usa CRLF outra parte usa só CR ou só LF. Quando isso acontece o editor do Delphi não consegue identificar o que precisa ser usado como caractere de fim de linha e, eventualmente poderá gerar a combinação "LFCR" que não corresponde a nenhum dos 3 SO, causando o problema dos BPs fora de sincronia!

É possível ter vários CRLF um após o outro, o que gera várias linhas em branco. Também é perfeitamente válido ter vários CR em sequencia ou vários LF em sequência, que vão gerar linhas em branco do mesmo modo. Isso não é problema algum em um arquivo homogêneo, porém acompanhe o seguinte raciocínio.

Inicialmente seu arquivo estava em formato Windows (CRLF), que é o padrão do Delphi:

Linha 1 CRLF
Linha 2 CRLF
Linha 3 CRLF
Linha 4 CRLF

Você achou algum código na web e precisou inserir ele na linha 3, porém este código veio com formato MAC (CR):

Linha 1 CRLF
Linha 2 CRLF
Linha 3 Colada CR
Linha 4 Colada CR
Linha 5 Colada CR
Linha 6 CRLF
Linha 7 CRLF

Note que este arquivo ainda é válido, pois todos os caracteres de fim de linha estão de fato em uma linha com conteúdo. Isso vai manter "distantes" todos os caracteres de fim de linha e o editor vai conseguir lidar com isso, mantendo o arquivo correto.

Agora suponha que você não queira mais a linha 3 e resolva removê-la mantendo a separação entre a linha 2 e a linha 4:

Linha 1 CRLF
Linha 2 CRLF
CR
Linha 4 Colada CR
Linha 4 Colada CR
Linha 6 CRLF
Linha 7 CRLF

Pronto, aconteceu a tragédia! Note que o final da linha 2 (CRLF) está colado com o final da linha em branco 3 (CR), gerando a combinação inválida "LFCR", responsável por causar o problema dos BPs fora de sincronia e quanto mais dessas combinações inválidas existirem no código, mais fora de sincronia os BPs estarão, tornando tudo ainda mais confuso.

Para gerar o código mostrado na primeira imagem deste artigo eu forcei esta situação, pegando um caractere de fim de linha CRLF e invertendo a posição dos bytes. Abaixo uma imagem do Notepad++ mostrando todos os caracteres. As setas mostram os caracteres trocados de posição, primeiro LF e depois CR:

Imagine como ficaria o fonte se eu tivesse trocado de lugar aleatoriamente mais alguns destes terminadores de linha. Se ficou curioso, pode tentar. Eu usei o HxD para manipular os bytes do arquivo.


Arquivo fonte (.pas) com binário correspondente (.dcu) diferente

Arquivos .pas, quando compilados, geram arquivos .dcu, que são a representação binária destes arquivos .pas. Normalmente estes arquivos caminham mais ou menos juntos, ou seja, toda vez que compilamos um .pas um .dcu correspondente é gerado e o linker do Delphi utiliza o .dcu na montagem do binário final (bpl, exe, dll, etc.). O depurador do Delphi utiliza informações contidas no arquivo .dcu para identificar as linhas que foram compiladas no arquivo .pas correspondente. Grosso modo, é o .dcu que sabe quais foram as linhas do arquivo .pas que foram compiladas.

De acordo com este artigo que escrevi há algum tempo, arquivos .pas têm precedência sobre arquivos .dcu, o que significa que quando ambos estão disponíveis, o Delphi sempre compilará o arquivo .pas e gerará um arquivo .dcu a ser utilizado pelo linker na montagem do binário final. Esta é a forma correta de trabalho. Ainda de acordo com o artigo, caso o Delphi ache o arquivo .dcu, mas não ache seu .pas correspondente, ele usará o arquivo .dcu sem qualquer alteração, ou seja, com o conteúdo que ele contiver, pois sendo um arquivo binário, ele não precisa ser compilado, apenas usado pelo linker para a montagem do binário final. Esta característica é uma otimização do Delphi que pode causar o efeito colateral de fazer os BP ficarem fora de sincronia, porém para que isso aconteça algo muito peculiar precisa acontecer: Você precisa estar editando um fonte que você acha que será compilado, porém ou ele não está sendo compilado ou está sendo compilado e seu .dcu está sendo salvo em um local que o linker não conhece, porém o linker acha um outro .dcu com o mesmo nome do arquivo .pas sendo editado e o usa no lugar. Achou confuso? Eu te entendo, por isso vou tentar explicar de outra forma.

Suponha que você seja um pouco mal informado[4] e esteja usando em seu projeto units que são encontradas por meio da lista contida em seu Search Path. Imagine que você tenha uma pasta lib e que dentro dela você coloque a unit MinhaUnit.pas que contém uma única função de nome MinhaFuncao. Se você incluir no Search Path o caminho para a pasta lib, você poderá incluir a unit MinhaUnit em qualquer cláusula uses do projeto. O projeto vai compilar corretamente e os BPs aparecerão no local correto.

Suponha agora que você coloque no Search Path o caminho onde os arquivos .dcu são gerados. Nesta situação o compilador continua achando o arquivo .pas e por conta disso tudo continua funcionando como deve. Se você agora remover do search path o caminho para a pasta lib e compilar, notará que a compilação e construção do binário será efetiva e correta, porque o compilador achou o arquivo MinhaUnit.dcu e "encaminhou" ele para o linker utilizar na criação do binário final. Neste momento tudo ainda está funcionando (BPs síncronos), porém se você resolver fazer uma modificação no arquivo MinhaUnit.pas, incluindo ou removendo linhas da função MinhaFunção, notará que as linhas válidas continuam as mesmas, mesmo quando tais linhas não possuem código válido! Isso acontece porque o Delphi usa o arquivo .dcu para marcar as linhas válidas no arquivo .pas, um arquivo que você alterou, mas que não está sendo encontrado pelo compilador, que continua usando apenas o .dcu. Nesta situação os BPs não poderão ser colocados em novas linhas que você porventura tenha criado porque estas linhas não estão no arquivo .dcu correspondente, porque o arquivo .pas simplesmente não está sendo compilado!

A situação descrita anteriormente é totalmente forçada, eu admito, mas isso foi proposital, porque eu queria simplificar ao máximo o entendimento do porquê BPs podem ficar fora de sincronia quando arquivos fonte e arquivos .dcu não "caminham" juntos, entretanto, existe um contexto onde o que foi descrito acima pode acontecer com uma maior frequência e sem forçação de barra: o desenvolvimento de componentes.

Ao desenvolver ou mesmo instalar componentes, um programador precisa tomar cuidado para manter seu ambiente sempre limpo, principalmente no tocante ao Library Path (LP). Se um desenvolvedor está testando ou instalando várias versões de um mesmo componente ele pode colocar mais de um caminho no LP que disponibiliza arquivos .dcu de mesmo nome em locais distintos. Se estes arquivos .dcu tiverem sido gerados a partir de arquivos .pas ligeiramente diferentes, eles proverão linhas válidas para breakpoints em locais distintos, tal como foi explicado na forçação de barra anterior. Se o arquivo .pas estiver em um dos locais listados no Browsing Path, a IDE potencialmente vai mostrar a você um arquivo .pas que corresponde a um arquivo .dcu errado, causando os problemas de sincronismo.


Como corrigir BPs fora de sincronia?

A correção deste problema é mais fácil do que aparenta, mesmo para resolver o problema quando ele é causado por arquivos .dcu "órfãos". Basta seguir as orientações adiante.

Caracteres de final de linha misturados

Se o seu arquivo fonte estiver sendo compilado com BPs fora do lugar a primeira coisa a se fazer é a homogeneização dos caracteres terminadores de linha, isto é, manter todos eles iguais. Para isso utilize o Notepad++, abra o arquivo e acesse o menu Editar > Conversão Final de Linha. Existem 3 opções disponíveis Windows, UNIX e MAC, selecione Windows para transformar todos os terminadores em CRLF.

Caso a opção Windows não esteja habilitada, significa que o Notepad++ identificou o arquivo como tendo o formato Windows, mas isso pode não ser verdade, sendo assim, neste caso, use a opção UNIX e depois clique na opção Windows. Agora salve o arquivo e você pode ter certeza que todos os terminadores de linha são CRLF.

Após salvar o arquivo, execute um build no seu programa e verifique se as linhas válidas são de fato válidas. Se forem, você resolveu o problema, se não forem, o seu problema é outro. Continue lendo.

Arquivo fonte (.pas) com binário correspondente (.dcu) diferente

Caso a homogeneização de caracteres de final de linha não tenha surtido efeito você deve ter um problema relacionado a um .dcu orfão. Para resolver o problema primeiro feche o Delphi (esse passo é importante!), faça uma busca em todo seu sistema pelo arquivo da unit com BPs fora de sincronia, só que com extensão .dcu, ou seja, se o arquivo fora de sincronia for MinhUnit.pas, procure no seu sistema o arquivo MinhaUnit.dcu. Muito provavelmente você vai encontrar mais de um, quando na verdade deveria haver apenas um! Para procurar arquivos eu recomendo o Agent Ransack, que funciona muito melhor que a ferramenta de busca do Windows.

Ao achar arquivos .dcu, você precisa verificar qual deles é o arquivo considerado correto e isso vai depender bastante do seu ambiente. Como arquivos .dcu são sempre recriados a partir de seus arquivos .pas correspondentes eu recomendo que você apague todos que você encontrar, em seguida, abara seu projeto novamente e o reconstrua. Neste momento o arquivo fonte será compilado de fato, gerando seu dcu correspondente sem interferência de qualquer outro arquivo .dcu de mesmo nome. Ao abrir o fonte que estava com problema, as linhas válidas marcadas serão as corretas.

Como aviso especial para os desenvolvedores de componentes eu recomendo que sempre que se desejar reconstruir um componente tendo a certeza de que seus fontes estão sendo realmente compilados, que se execute um comando Clean antes de executar um comando Build. Este comando vai remover todos os arquivos .dcu daquele projeto específico, obrigando o Delphi a recompilar todos os arquivos de fonte (.pas). Se seu Delphi não possui o comando Clean, você tem duas opções: apagar todos os .dcu do projeto manualmente antes de executar o comando Build ou remover do Library Path temporariamente qualquer referência a pastas de saída de arquivos .dcu.



1 Em Delphis antigos, as informações de depuração são aplicadas apenas na caixa de diálogo "Project Options". Em Delphis mais recentes, que possuem "Build Configurations" acessíveis diretamente no "Project Manager" do Delphi a aplicação de informações de depuração pode ser aplicada simplesmente utilizando a configuração "Debug". Nestes Delphis, ao utilizar a configuração "Release", informações de depuração não serão aplicadas e você não verá as bolas azuis marcando as linhas, o que significa que, invariavelmente, qualquer breakpoint aplicado será inválido
2 Um código é inatingível quando ele está inserido em um local onde o fluxo de execução jamais passará. Isso é considerado um erro de lógica. Um exemplo disso seria um código que executa apenas se uma condição (if) for true, porém esta condição sempre será false. O código controlado por uma condição desta natureza jamais será executado, e portanto ele é um código inatingível
3 Apesar de a depuração funcionar corretamente sem BPs fora de sincronia, observei que ao utilizar o formato do MAC (CR), as linhas de "Structural Highlight" ficaram totalmente fora do lugar e vi também que o "Error insight" mostra erros em todas as linhas, reportando incorretamente "Expected Integer but received end of file" em todas elas. Portanto, não recomendo o uso do formato do MAC com o Delphi
4 Na verdade eu queria dizer burro, mas ia ficar pesado para isso aparecer no corpo do artigo. Enfim, utilizar o Search Path de um projeto é algo que dificilmente se faz e eu posso dizer com muita segurança, que se você o utiliza, você não está fazendo isso certo. Se você discorda de mim, provavelmente você sabe o que faz e está correto, se você ficar na dúvida, você está errado e recomendo fortemente que você pare de usar o Search Path para encontrar units