Pois é...antes de mais, um pedido de desculpa aos meus (poucos, mas bons :) ) fieis leitores: a série de artigos sobre COM já está um pouco atrasada, mas irá continuar nos próximos dias. Actualmente a principal causa desse artigo prende-se com o título do artigo...
Hoje, após dois dias de procura de erro, lá consegui encontrar e corrigir os dois erros que impediam a conclusão do meu componente desenvolvido em C#. Apesar de gostar muito do Interop, acho que a Microsoft deveria ter conseguido arranjar outro tipo de estratégia que permitisse a interacção entre os componentes .Net e os componentes COM. A construção de um projecto não trivial dá dores de cabeça! Para além disso, causa a queda de cabelo :) (eu que o diga! Agora está cada vez mais dificil encontrar cabelo na minha cabeça...). Até a instrução mais simples pode trazer problemas que depois são dificeis de detectar. Por exemplo, vamos analisar o seguinte excerto de código:
void Test( App app )
{
//efectuar outros calculos
//obter apontador para objecto doc
Doc aux = app.Doc;
//efectuar varios calculos
//ja nao e preciso mais o aux, entao vamos liberta-lo
//atraves do metodo utilitario da nossa autoria InteropUtils.Release
//que evoca Marshal.ReleaseComObject ate que a contagem chegue a zero
InteropUtils.Release( aux );
}
//codigo normal
App app = obter_apontado_atraves_ou_de_outra_forma ;
//obter doc
Doc doc = app.Doc;
//efectuar outros calculos
//e mais calculos ainda -:)
//agora vamos chamar a nossa funcao
Test( app );
//mais codigo
//agora vamos aceder a propriedade Title do elemento Doc
MessageBox.Show( doc.Title ); //CABRUM!!!CABRUM!!!
Pois...acontece que o apontador doc já libertou o objecto COM e por isso todas as operações sobre esse objecto resultam em erro. Podem dizer: mas então porque não chamar apenas uma vez o método Marshal.ReleaseComObject de forma a decrementar a referência interna relativa ao objecto COM? Resposta: porque não é garantido que funcione todas as vezes pois a contagem interna depende de vários factores (como descrevi nos artigos).
Então como fazer? Bem, o que tenho feito para evitar estas situações é, neste tipo de situações, passar o parâmetro doc para a função e assim apenas elimino (ou melhor, utilizo o meu método auxiliar para evocar o Marshal.ReleaseComObject) eventuais referências a elementos obtidos de forma explicita no interior da função.
Ah, uma nota: se antes de evocar a MessageBox tivesse voltado a pedir o objecto a partir da propriedade Doc do elemento App (ou seja, se voltasse a fazer doc = app.Doc), não haveria o perigo do objecto COM não existir pois a framework ecarregar-se-ia de voltar a obter uma referência COM válida.
Presumo que devem estar a pensar numa estratégia melhor: e que tal se, em vez de tentarmos controlar a contagem de forma a procedermos à eliminação manual, utilizássemos o GC.Collect para limparmos a memória. Bom, funcionar não funciona sempre (como pude comprovar ontem e hoje!). Para além disso, a penalização a nível de performance pode ser grande.
Bom, e então porque não apenas deixar os objectos em paz e deixar o GC trabalhar quando for necessário? hum...infelizmente, também não funciona em todos os casos. É verdade que o GC iria acabar por destruir os objectos wrappers .Net (o que implicaria a libertação dos objectos COM). O problema é que nem sempre nos podemos dar a este luxo. Exemplo: actualmente estou a desenvolver componentes add-in para uma aplicação "maravilhosa" chamada GeoMedia.
Como sempre acontece, os add-ins têm de respeitar um determinado interface. Se deixarmos os apontadores "abandonados à sua sorte", então, nesta aplicação, não iremos ter a oportunidade de processar um evento necessário ao encerramento do comando (este é o nome técnico para os add-ins nesta aplicação - até parece um jogo!) pois a aplicação limita-se a "crashar" (internamente após executar as instruções associadas a um método nosso, mas antes de evocar o método que nos dava jeito para utilizarmos o GC.Collect- que engraçado! ah, mais engraçado ainda são as samples deles que fazem isso - também, as samples contém código tão tão avançado, que até me pergunto o que é que era suposto aprender com aquilo... por outras palavras, são samples tipo hello world).
Nestes casos, tudo tem de ser controlado ao milimetro, ou então está tudo "lixado". Ah, e lembram-se de eu dizer que chamar apenas uma vez o método ReleaseComObject pode não ser suficiente? Pois, então imaginem que, uma vez, após passar uma variável (um wrapper COM) para um método de outro objecto COM fez com que o método ReleaseComObject tivesse que ser executado 23 vezes até libertar totalmente as referências, libertando-se assim o objecto COM interno (como é que eu sei isto? simples: um dos primeiros erros que contribui para a queda de cabelo mencionada acima foi um crash relacionado com um dos tais apontadores "abandonados"; nessa altura estava apenas a chamar o método Marshal.ReleaseComObject e pensava que já tinha libertado todas as referências. Foi com grande espanto que resolvi colocar o método dentro dum ciclo e depois imprimir o número de vezes que tinha sido executado para libertar o objecto COM - incrivel!!! Estava na hora de construir um método auxilair para este tipo de situações ).
Já agora, vou aproveitar para fazer uma observação ao GeoMedia: as type libs que desenvolveram são uma vergonha! Estão a ouvir??? Já vão na versão 5 e ainda continuam a produzir type libs com redefinições de tipos. Quando é que vão perceber que alguns de nós gostamos de fazer algumas coisas em C++? Esse tipo de livrarias só funciona em VB porque o compilador é amigo e elimina os duplicados! Acho que a Microsoft deveria utilizar essas livrarias num case study de forma a demonstrar o que não se deve fazer quando implementamos uma typelib! Agora já me sinto melhor :)
Bem, está na hora de encerrar...espero ter o próximo artigo já na próxima semana (para os mais curiosos, o artigo fala sobre a construção de ActiveX controls usando C#).
posted on Wednesday, September 15, 2004 12:09 AM