Discussão do artigo "Guia para escrever uma DLL para MQL5 em Delphi"

 

Novo artigo Guia para escrever uma DLL para MQL5 em Delphi foi publicado:

O artigo examina o mecanismo de criação de um módulo DLL, usando a linguagem de programação popular de ObjectPascal, dentro de um ambiente de programação Delphi. Os materiais, fornecidos neste artigo, são designados a focar principalmente em programadores iniciantes, que estejam trabalhando com problemas que rompem os limites da linguagem de programação embutidos do MQL5, conectando os módulos DLL externos.

Autor: Andrey Voytenko

 
Há muito tempo estou esperando por um artigo como esse. Obrigado ao autor.
 
DC2008:
Há muito tempo estou esperando por um artigo como esse. Obrigado ao autor.

Obrigado a você. Você não é o primeiro a me agradecer. Terei prazer em ouvir todos os desejos e comentários críticos sobre o material do artigo.

No futuro, gostaria de desenvolver o tópico de programação em Delphi para MT5, acrescentando novas informações ao site.

 
Você deve acrescentar pelo menos um parágrafo sobre depuração. O artigo menciona uma situação em que o AV pode ocorrer, mas mesmo deixando de lado o mar de outras possíveis fontes de erros, tentar procurar manualmente (a olho nu ou mentalmente) o local do erro pode levar muito tempo e não ter sucesso.
 

Acho que é um artigo útil para muitas pessoas. Alguns comentários:

1. as unidades SysUtils e Classes deveriam ter sido deixadas no projeto. Apesar do fato de que sua presença "incha" um pouco o projeto, elas têm muitas funções pequenas, mas importantes. Por exemplo, a presença de SysUtils adiciona automaticamente o processamento de exceções ao projeto. Como você sabe, se uma exceção não for processada na dll, ela será passada para o mt5, onde fará com que a execução do programa mql5 seja interrompida.

2. Você não deve usar todos os tipos de procedimentos no DllEntryPoint (também conhecido como DllMain). Como a Microsoft afirma em seus documentos, isso está repleto de vários efeitos desagradáveis. Aqui está uma pequena lista de artigos sobre esse assunto:

Práticas recomendadas para a criação de DLLs pela Microsoft - http://www.microsoft.com/whdc/driver/kernel/DLL_bestprac.mspx.

DllMain e a vida antes do parto - http://transl-gunsmoker.blogspot.com/2009/01/dllmain.html

DllMain - uma história para dormir - http://transl-gunsmoker.blogspot.com/2009/01/dllmain_04.html

Algumas razões para não fazer nada assustador em seu DllMain - http://transl-gunsmoker.blogspot.com/2009/01/dllmain_05.html

Mais razões pelas quais você não deve fazer nada assustador em seu DllMain: bloqueio acidental -

http://transl-gunsmoker.blogspot.com/2009/01/dllmain_7983.html

 

Já apresentei um trecho de um artigo inacabado em algum lugar, acho que no fórum quad. Vou repeti-lo aqui.

início...fim

Ao criar um projeto Delphi destinado à compilação de DLL, a seção begin...end aparece no arquivo de projeto .DPR. Essa seção é sempre executada quando a DLL é projetada pela primeira vez no espaço de endereço do processo. Em outras palavras, pode ser considerada como uma espécie de seção de inicialização que todas as unidades têm. Nesse local, você pode executar algumas ações que precisam ser executadas logo no início e apenas uma vez, para o processo atual. Ao carregar a DLL no espaço de endereço de outro processo, essa seção será executada lá novamente. Mas como os espaços de endereço dos processos são separados um do outro, a inicialização em um processo não afetará o outro processo de forma alguma.

Esta seção tem algumas limitações das quais você deve estar ciente e levar em conta. Essas limitações estão relacionadas às sutilezas do mecanismo de carregamento da DLL do Windows. Falaremos sobre elas em mais detalhes posteriormente.

 

inicialização/finalização

Cada unidadedo Delphi tem seções especiais, as chamadas seções de inicialização e finalização. Assim que qualquer unidade é conectada ao projeto, essas seções são conectadas ao mecanismo especial de carga e descarga do módulo principal. E essas seções são executadas antes que a seção principal begin...end inicie seu trabalho e depois que o trabalho é concluído. Isso é muito conveniente, pois elimina a necessidade de escrever a inicialização e a finalização no próprio programa. Ao mesmo tempo, a conexão e a desconexão são realizadas automaticamente, sendo necessário apenas conectar ou desconectar a unidade ao projeto. E isso acontece não apenas nos arquivos EXE tradicionais, mas também nas DLLs. A ordem de inicialização da DLL, ao "carregá-la" na memória, é a seguinte: primeiro, todas as seções de inicialização da unidade são executadas, na ordem em que estão marcadas nos usos do projeto; em seguida, a seção begin...end é executada. A finalização é feita na ordem inversa, exceto pelo fato de não haver uma função de finalização especialmente projetada no arquivo de projeto da DLL. Esse é, em geral, outro motivo pelo qual se recomenda separar os projetos de DLL em um arquivo de projeto e uma unidade de usos.

 

DllMain

Esse é o chamado ponto de entrada da DLL. A questão é que o Windows ocasionalmente precisa relatar qualquer evento que ocorra dentro do processo para a própria DLL. Para fazer isso, existe um ponto de entrada. Ou seja, uma função especialmente predefinida que cada DLL possui e que pode tratar mensagens. E, embora ainda não tenhamos visto essa função em uma DLL escrita em Delphi, ela tem esse ponto. Apenas o mecanismo de seu funcionamento está oculto, mas você sempre pode chegar a ele. A resposta à pergunta - ela é mesmo necessária? - não é tão óbvia quanto parece.

Primeiro, vamos tentar entender o que o Windows está tentando dizer à DLL. Há um total de 4 mensagens que o sistema operacional envia à DLL. A primeira delas, a notificação DLL_PROCESS_ATTACH, é enviada sempre que o sistema anexa uma DLL ao espaço de endereço do processo de chamada. No caso da MQL4, trata-se de um carregamento implícito. Não importa que essa DLL já tenha sido carregada no espaço de endereço de outro processo, a mensagem ainda será enviada. E não importa que, de fato, o Windows carregue uma DLL específica na memória apenas uma vez, todos os processos que desejarem carregar essa DLL em seu espaço de endereço receberão apenas um reflexo dessa DLL. Esse é o mesmo código, mas os dados que a DLL pode ter são exclusivos de cada processo (embora seja possível que existam dados comuns). A segunda, a notificação DLL_PROCESS_DETACH, informa à DLL para se desconectar do espaço de endereço do processo de chamada. De fato, essa mensagem é recebida antes de o Windows começar a descarregar a DLL. Na verdade, se a DLL estiver sendo usada por outros processos, não haverá descarregamento; o Windows simplesmente "esquece" que a DLL existia no espaço de endereço do processo. Outras duas notificações, DLL_THREAD_ATTACH eDLL_THREAD_DETACH, são recebidas quando o processo que carregou a DLL gera ou destrói threads dentro do processo. Há algumas questões sutis relacionadas à ordem em que as notificações de thread são recebidas, mas não as consideraremos.

Agora vamos falar sobre como as DLLs escritas em Delphi são organizadas e o que geralmente fica oculto para os programadores. Depois que o Windows "projeta a DLL no espaço de endereço do processo de chamada", ou simplesmente carrega a DLL na memória, nesse ponto há uma chamada para a função localizada no ponto de entrada e a passagem da notificação DLL_PROCESS_ATTACH para essa função. Em uma DLL escrita em Delphi, esse ponto de entrada contém um código especial que faz muitas coisas diferentes, inclusive iniciar a inicialização das unidades. Ele se lembra de que a inicialização e a primeira execução da DLL foram feitas e executa begin...end do arquivo principal do projeto. Assim, esse código de carregamento inicial é executado apenas uma vez, todas as outras chamadas do Windows para o ponto de entrada são feitas para outra função, que lida com as notificações subsequentes - na verdade, ele as ignora, exceto a mensagem DLL_PROCESS_DETACH, que finaliza a unidade. É assim que o mecanismo de carregamento de uma DLL escrita em Delphi se parece em termos gerais. Na maioria dos casos, é suficiente escrever e usar DLLs em MQL4.

Se você ainda precisar de uma DllMain exatamente igual à de C, não será difícil organizá-la. Isso é feito da seguinte forma. Ao carregar uma DLL pela primeira vez, entre outras coisas, o módulo System (ele está sempre presente em um programa ou DLL) cria automaticamente uma variável processual global DllProc, que é inicializada com nil. Isso significa que não é necessário nenhum processamento adicional das notificações DllMain, além do que já existe. Assim que o endereço da função for atribuído a essa variável, todas as notificações de DLLs do Windows irão para essa função. Isso é o que é exigido do ponto de entrada. No entanto, a notificação DLL_PROCESS_DETACH ainda será rastreada pela função de encerramento da DLL, como antes, para permitir a finalização.

procedureDllEntryPoint(Reason: DWORD);

begin

case Reason of

DLL_PROCESS_ATTACH : ;//'Conexão processo'

DLL_THREAD_ATTACH : ; // 'Conectando a thread'

DLL_THREAD_DETACH : ; // 'Desconectando um thread'. stream'

DLL_PROCESS_DETACH : ;//'Desconectando o processo'

end;

fim;

begin

if not Assigned(DllProc) then begin

DllProc :=@DllEntryPoint;

DllEntryPoint (DLL_PROCESS_ATTACH);

end;

end.

Caso não estejamos interessados em notificações de thread, tudo isso é desnecessário. Só é necessário organizar as seções de inicialização/finalização na unidade, pois os eventos de conexão e desconexão do processo serão rastreados automaticamente.

 

A perfídia e a traição do DllMain

Agora, talvez, seja hora de abordar um assunto que, surpreendentemente, é pouco abordado na literatura sobre programação. Esse tópico diz respeito não apenas ao Delphi ou ao C, mas a qualquer linguagem de programação capaz de criar DLLs. Essa é uma propriedade do carregador de DLL do Windows. Da literatura traduzida, séria e difundida sobre programação no ambiente Windows, apenas um autor conseguiu encontrar uma menção a isso, e isso nos termos mais vagos. Esse autor é J. Richter, e ele é perdoado, pois seu maravilhoso livro foi publicado em 2001, quando o Windows de 32 bits em geral não era tão difundido.

É interessante que a própria MS nunca escondeu a existência do problema com a DllMain e até publicou um documento especial, algo como "A melhor maneira de usar a DllMain". Nele, ele explicou o que pode ser feito no DllMain e o que não é recomendado. E ressaltou que as coisas não recomendadas levam a erros difíceis de ver e inconsistentes. Aqueles que desejarem ler esse documento podem dar uma olhada aqui. Um resumo mais popular de várias traduções de vários relatórios alarmistas sobre o assunto está descrito aqui.

A essência do problema é muito simples. A questão é que o DllMain, especialmente ao carregar uma DLL, é um local especial. Um local onde você não deve fazer nada complicado e extraordinário. Por exemplo, não é recomendável criarProcess ou LoadLibrary em outras DLLs. Também não é recomendável criarThread ou CoInitializar COM. E assim por diante.

Você pode fazer as coisas mais simples. Caso contrário, nada é garantido. Portanto, não coloque nada desnecessário na DllMain, caso contrário, você ficará surpreso ao ver os aplicativos que usam sua DLL travarem. É melhor ser seguro e criar funções especiais exportadas de inicialização e finalização, que serão chamadas pelo aplicativo principal nos momentos certos. Isso pelo menos ajudará a evitar problemas com a DllMain.
 

ExitProc, ExitCode,MainInstance,HInstance....

O módulo System, que está sempre conectado à sua DLL no momento da compilação, tem algumas variáveis globais úteis que você pode usar.

ExitCode, - uma variável na qual você pode colocar um número diferente de 0 no carregamento; como resultado, o carregamento da DLL será interrompido.

ExitProc, - uma variável procedural que pode armazenar o endereço da função que será executada na saída. Essa variável é uma relíquia do passado distante, não funciona em DLLs e, além disso, os desenvolvedores do Delphi não recomendam usá-la em DLLs devido a possíveis problemas.

HInstance, - uma variável na qual, após o carregamento, é armazenado o descritor da própria DLL. Ela pode ser muito útil.

MainInstance, - descritor do aplicativo que carregou a DLL em seu espaço de endereço.

IsMultiThread, - uma variável que é automaticamente definida como True se a compilação da DLL detectar o trabalho com threads. Com base no valor dessa variável, o gerenciador de memória da DLL alterna para o modo multithread. Em princípio, é possível forçar o gerenciador de memória a mudar para o modo multithread, mesmo que os threads não sejam explicitamente usados na DLL. IsMultiThread:=True; Naturalmente, o modo multithread é mais lento do que o modo de thread único devido ao fato de que os threads são sincronizados entre si.

MainThreadID, - descritor do thread principal do aplicativo.

E assim por diante. Em geral, o módulo System executa aproximadamente as mesmas funções que o CRT em C. Incluindo funções de gerenciamento de memória. Uma lista de todas as funções e variáveis presentes na DLL compilada, não apenas as exportadas, mas todas elas, pode ser obtida se você ativar a opção Linker, Map file - Detailed, nas configurações do projeto.
 

Gerenciamento de memória

O próximo problema, bastante sério, que geralmente causa dificuldades é o gerenciamento de memória nas DLLs. Mais precisamente, o gerenciamento de memória em si não causa nenhum problema, mas assim que a DLL tenta trabalhar ativamente com a memória alocada pelo gerenciador de memória do aplicativo, é aí que os problemas geralmente começam.

O fato é que, normalmente, os aplicativos são compilados com o MemoryManager incorporado. A DLL compilada também contém seu próprio MemoryManager. Isso é especialmente verdadeiro para aplicativos e DLLs criados em diferentes ambientes de programação. Como no nosso caso, o terminal está no MSVC e a DLL está no Delphi. Está claro que esses gerenciadores são diferentes por sua estrutura, mas, ao mesmo tempo, também são gerenciadores fisicamente diferentes, cada um gerenciando sua própria memória dentro do espaço de endereço comum do processo. Em princípio, eles não interferem um no outro, não tiram a memória um do outro, existem em paralelo e geralmente não "sabem" nada sobre a existência de concorrentes. Isso é possível porque ambos os gerenciadores acessam a memória a partir da mesma fonte, o gerenciador de memória do Windows.

Os problemas começam quando uma função DLL e um aplicativo tentam gerenciar seções de memória distribuídas por um gerenciador de memória diferente. Por esse motivo, existe uma regra geral entre os programadores que diz que "a memória não deve ultrapassar os limites de um módulo de código".

Essa é uma boa regra, mas não é totalmente correta. Seria mais correto usar apenas o mesmo MemoryManager na DLL que o aplicativo usa. Na verdade, gosto da ideia de conectar o gerenciador de memória do MT4ao gerenciador de memória do Delphi FastMM, mas essa não é uma ideia muito viável. De qualquer forma, o gerenciamento de memória deve ser uma coisa só.

No Delphi , é possível substituir o gerenciador de memória padrão por qualquer outro gerenciador de memória que atenda a alguns requisitos. Assim, é possível fazer com que a DLL e o aplicativo tenham um único gerenciador de memória, que será o gerenciador de memória do MT4.