記事"DelphiでDLLをMQL5向けに書くためのガイド"についてのディスカッション

 

新しい記事 DelphiでDLLをMQL5向けに書くためのガイド はパブリッシュされました:

本稿は、人気のプログラム言語ObjectPascalを使用しDelphiプログラム環境でDLLモジュールの作成メカニズムを検証します。本稿で使用している資料は、まずは問題を抱えたプログラム初心者用に考えられでおります。外部DLLに接続することでMQL5プログラム言語に埋め込まれた境界を破ります。

DLLウィザードを用いたプロジェクト作成

作者: Andrey Voytenko

 
このような記事をずっと待っていた。著者に感謝する。
 
DC2008:
このような記事を長い間待っていた。著者に感謝する。

ありがとう。ありがとうございます。記事の内容に関する希望や批判的なコメントも喜んでお聞きします。

将来的には、Delphi forMT5での プログラミングのトピックを発展させ、サイトに新しい情報を追加したいと思います。

 
少なくともデバッグに関する段落を加えるべきだ。この記事ではAVが発生する可能性のある状況について触れていますが、他の潜在的なエグジステーションの原因はさておき、手動で(目で見て、あるいは頭で考えて)エラーの場所を探そうとすると、非常に長い時間がかかり、成功しないことがあります。
 

多くの人にとって有益な記事だと思う。いくつかコメントがある:

1.SysUtilsとClassesユニットはプロジェクトに残すべきだった。これらのユニットが存在するとプロジェクトが多少「肥大化」してしまうが、これらは小さいながらも重要な機能をたくさん持っている。例えば、SysUtilsがあると、例外処理が自動的にプロジェクトに追加される。ご存知のように、もし例外処理がdllで処理されない場合、それはmt5に渡され、mql5プログラムの実行を停止させる。

2.2.DllEntryPoint(別名DllMain)内であらゆる種類のプロシージャを使用しないでください。Microsoftのドキュメントにあるように、これはさまざまな不快な影響をはらんでいる。以下は、この件に関する記事の小さなリストである:

MicrosoftによるDLL作成のベストプラクティス - http://www.microsoft.com/whdc/driver/kernel/DLL_bestprac.mspx。

DllMainと出産前の生活 -http://transl-gunsmoker.blogspot.com/2009/01/dllmain.html

DllMain - 就寝前のお話 -http://transl-gunsmoker.blogspot.com/2009/01/dllmain_04.html

DllMainで怖いことをしてはいけないいくつかの理由 -http://transl-gunsmoker.blogspot.com/2009/01/dllmain_05.html

DllMain で怖いことをしてはいけないもうひとつの理由: 偶発的なロック

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

 

未完成の記事の抜粋は、クワッドフォーラムだったと思うが、すでにどこかで紹介した。ここではそれを繰り返すことにする。

はじめに...終わり

DLLコンパイルを 目的としたDelphiプロジェクトを作成すると、.DPRプロジェクトファイルにbegin...endセクションが現れます。このセクションは、DLLが最初にプロセスアドレス空間に投影されるときに常に実行されます。言い換えれば、すべてのユニットが持つ初期化セクションの一種と考えることができます。このセクションでは、現在のプロセスに対して一度だけ、最初に実行する必要があるアクションを実行することができます。DLLを別のプロセスのアドレス空間にロードするとき、このセクションはそこで再び実行されます。しかし、プロセスのアドレス空間は互いに分離されているため、一方のプロセスで初期化を行っても、他方のプロセスには何の影響もありません。

このセクションには、注意して考慮すべきいくつかの制限がある。これらの制限は、WindowsのDLLロード・メカニズムの微妙な点に関連しています。これらについては後で詳しく説明します。

 

初期化/最終化

Delphiの 各ユニットには、初期化セクションと最終化セクションと呼ばれる特別なセクションがあります。任意のユニットがプロジェクトに 接続されるとすぐに、これらのセクションはメインモジュールの特別なロードとアンロードメカニズムに接続されます。そして、これらのセクションは、メインの begin...end セクションが 作業を開始する前と、作業が終了した後に 実行されます 。これは非常に便利で、プログラム自体に初期化と最終化を書く必要がなくなるからです。同時に、接続と切断は自動的に行われるため、ユニットをプロジェクトに接続または切断するだけでよい。そして、これは従来のEXEファイルだけでなく、DLLでも起こります。 DLLをメモリに "ロード "する際の 初期化の順序は 以下の通りです。まず、ユニットのすべての初期化セクションが、プロジェクトのusesでマークされた順序で実行され、次にbegin...endセクションが実行されます。ただし、DLLプロジェクトファイルには特別に設計された終了関数がありません。一般的に、DLLプロジェクトをプロジェクト・ファイルとユーズ・ユニットに分けることが推奨されるもう1つの理由はここにあります。

 

DllMain

これはいわゆるDLLのエントリーポイントである。ポイントは、Windowsが時折、プロセス内で発生したイベントをDLL自身に報告する必要があることだ。そのためにエントリー・ポイントが存在する。つまり、各DLLが持っている、メッセージを処理できる特別に定義された関数である。そして、Delphiで書かれたDLLではまだこの関数を見たことがないが、それでもそのようなポイントがある。ただ、その機能の仕組みはベールに包まれているが、いつでもそれを手に入れることができる。必要なのか?- という質問に対する答えは、見かけほど明らかではない。

まず、WindowsがDLLに何を伝えようとしているのかを理解しよう。オペレーティング・システムがDLLに伝えるメッセージは全部で4つある。最初のものはDLL_PROCESS_ATTACH通知で、システムがDLLを呼び出し元プロセスのアドレス空間にアタッチするたびに送られます。 MQL4の場合 、これは暗黙のロードです。このDLLがすでに別のプロセスのアドレス空間にロードされていても、メッセージは送信されます。また、実際にWindowsが特定のDLLを1度だけメモリにロードすることは問題ではなく、このDLLを自分のアドレス空間にロードしたいすべてのプロセスは、このDLLのリフレクションだけを受け取ります。これは同じコードですが、DLLが持つ可能性のあるデータは各プロセスに固有です(共通のデータが存在する可能性はありますが)。2つ目のDLL_PROCESS_DETACH通知は、DLLに呼び出し元プロセスのアドレス空間から切り離すように指示します。実際、このメッセージはWindowsがDLLのアンロードを開始する前に受け取られる。実際、DLLが他のプロセスによって使用されている場合、アンロードは行われず、Windowsは単にプロセスのアドレス空間にDLLが存在したことを「忘れる」だけである。さらに2つの通知、 DLL_THREAD_ATTACH DLL_THREAD_DETACHは 、DLLをロードしたプロセスがプロセス内でスレッドを生成または破棄するときに受け取られる。スレッド通知の受信順序には微妙な問題がいくつかあるが、ここでは考慮しない。

さて、Delphiで書かれたDLLがどのように配置され、プログラマから通常隠されているかについてである。WindowsがDLLを "呼び出し元プロセスのアドレス空間に投影 "した後、つまり簡単に言えば、DLLをメモリにロードした後、この時点でエントリポイントに位置する関数への呼び出しが あり、この関数に通知DLL_PROCESS_ATTACHを渡します。Delphiで書かれたDLLでは、このエントリーポイントには、ユニットの初期化を開始するなど、さまざまなことを行う特別なコードが含まれています。初期化とDLLの最初の実行が行われたことを記憶し、メイン・プロジェクト・ファイルのbegin...endを実行します。したがって、この最初のロード・コードは一度しか実行されず、エントリー・ポイントへの他のWindows呼び出しはすべて別の関数に行われ、その後の通知を処理します。これが、Delphiで書かれたDLLをロードするメカニズムの一般的な姿です。ほとんどの場合、MQL4でDLLを書いて使うだけで十分です。

それでもC言語と全く同じDllMainが必要な場合、それを整理するのは難しくありません。それは次のように行う。DLLを初めてロードするとき、とりわけSystemモジュール(これはプログラムまたはDLLに常に存在する)が自動的にグローバル手続き変数DllProcを作成し、これはnilで初期化される。これは、存在するDllMain通知以外の追加処理が必要ないことを意味します。この変数に関数のアドレスが代入されると、WindowsからのDLLに対するすべての通知はこの関数に送られるようになる。これがエントリー・ポイントに要求されることである。しかし、DLL_PROCESS_DETACH通知は、これまで通り、DLL終了関数によって追跡され、最終処理を行うことができます。

手続きDllEntryPoint(Reason: DWORD);

開始

ケースの 理由

DLL_PROCESS_ATTACH : ;//'接続 プロセス'

DLL_THREAD_ATTACH : ;//'スレッドの接続' スレッドの 接続

DLL_THREAD_DETACH : ;//' スレッドの切断' ストリーム'

DLL_PROCESS_DETACH : ;//'プロセスを切断する プロセスの 切断'

終了する

を終了する

開始

if not Assigned(DllProc) then begin

DllProc :=@DllEntryPoint

DllEntryPoint (DLL_PROCESS_ATTACH);

end

end; end.

スレッド通知に興味がない場合は、このような処理はすべて不要である。プロセスの接続と切断のイベントは自動的に追跡されるので、初期化/最終化のセクションを単位で整理するだけでよい。

 

DllMainの不実と裏切り

さて、そろそろプログラミングの文献では驚くほどあまり取り上げられていない問題に触れてもいい頃だろう。このトピックは、DelphiやCだけでなく、DLLを作成できるあらゆるプログラミング言語に関係する。これはWindowsのDLLローダーの特性です。Windows環境でのプログラミングに関する本格的で広範な文献を翻訳した中で、それに関する記述を見つけることができたのはたった一人の著者だけで、それも最も曖昧な表現で。この著者はJ.リヒターであり、彼の素晴らしい本が出版されたのは2001年である。

MS自身がDllMainの問題の存在を隠すことなく、「DllMainを使う最良の方法」というような特別な文書まで掲載しているのは興味深い。その中で、DllMainで何ができて、何が推奨されないかを説明している。そして、推奨されていないことは、見づらく一貫性のないエラーにつながることが指摘されている。この文書を読みたい人は、ここを見てほしい。この件に関するいくつかの憂慮すべき報告書の翻訳をまとめた、より一般的な要約がここにある。

問題の本質は非常に単純である。特にDLLをロードするときのDllMainは特別な場所だということだ。複雑で目立つことをしてはいけない場所だ。例えば 、他のDLLを CreateProcessしたり、 LoadLibraryしたりすることは推奨されない 。また、 CreateThreadや CoInitialise COMも 推奨されない 。などなど。

最も単純なことはできる。そうでなければ何も保証されない。したがって、DllMainには不要なものを入れないでください。そうしないと、あなたのDLLを使ったアプリケーションがクラッシュして驚くことになります。安全策をとって、初期化および最終化のための 特別なエクスポート関数を 作成し、適切なタイミングでメイン・アプリケーションから呼び出されるようにするのがよいでしょう。そうすることで、少なくともDllMainの問題を避けることができます。
 

ExitProc、ExitCode、MainInstanceHInstance...。

コンパイル時に常にDLLにフックされるSystemモジュールには、使える便利なグローバル変数が いくつかあります。

ExitCode, - この変数に0以外の数値を入れると、DLLのロードが停止します。

ExitProc、 - 終了時に実行される関数のアドレスを格納できる手続き変数。この変数は遠い過去の遺物であり、DLLでは機能せず、また、問題が発生する可能性があるため、Delphi開発者はDLLでの使用を推奨していません。

HInstanceは、ロード後にDLL自体のディスクリプタが格納される変数である。非常に便利である。

MainInstance - DLLをそのアドレス空間にロードしたアプリケーションの記述子。

IsMultiThread, - DLLのコンパイル時にスレッドによる作業が検出された場合、自動的にTrueに設定される変数。この変数の値に基づいて、 DLLメモリー・マネージャー はマルチスレッド・モードに切り替わる。原理的には、DLL内でスレッドが明示的に使用されていなくても、メモリ・マネージャを強制的にマルチスレッド・モードに切り替えることが可能です。IsMultiThread:=True; 当然ながら、マルチスレッド・モードは、スレッド同士が同期しているため、シングル・スレッド・モードよりも遅くなる。

MainThreadID, - アプリケーションのメイン・スレッドの記述子。

など。一般に、Systemモジュールは、C言語のCRTとほぼ同じ機能を実行する。メモリ管理関数を含む。エクスポートされたものだけでなく、コンパイルされた DLL に存在するすべての関数と変数のリストは、プロジェクト設定のリンカーオプション「マップファイル-詳細」をオンにすると取得できます。
 

メモリー管理

次に、しばしば問題を引き起こす深刻な問題は、DLL のメモリ管理である。より正確には、メモリ管理自体は何の問題も引き起こさないのですが、DLLがアプリケーション自体のメモリ・マネージャによって割り当てられたメモリを積極的に使用しようとした途端に、問題が発生します。

通常、アプリケーションはMemoryManagerを組み込んでコンパイルされます。コンパイルされたDLLもまた、それ自身のMemoryManagerを含んでいます。これは特に、異なるプログラミング環境で作成されたアプリケーションやDLLに当てはまります。私たちのケースのように、端末はMSVCで、DLLはDelphiです。これらはその構造から異なるマネージャであることは明らかですが、同時に物理的にも異なるマネージャであり、それぞれがプロセスの共通アドレス空間内で独自のメモリを管理しています。原則的に、これらは互いに干渉せず、互いのメモリを奪うこともなく、互いに並行して存在し、通常は競合の存在について何も知らない。これが可能なのは、両マネージャーが同じソース、つまりWindowsメモリーマネージャーからメモリーにアクセスするからである。

問題は、DLL関数やアプリケーションが、異なるメモリ・マネージャによって分散されたメモリ・セクションを管理しようとしたときに始まります。このため、プログラマーの間では「メモリはコード・モジュールの境界を越えてはならない」という経験則がある。

これは良いルールだが、正確ではない。アプリケーションが使用するDLLと同じMemoryManagerを使用する方が正しいのです。実は、 MT4 のメモリーマネージャーを Delphi のメモリーマネージャー FastMMに接続するというアイデアは割と好きなの ですが、それはあまり実現可能なアイデアではありません。とにかく、メモリ管理は一つのことであるべきです。

Delphi では、デフォルトのメモリマネージャを、いくつかの条件を満たす他のメモリマネージャで置き換えることが可能です。従って、DLLとアプリケーションに単一のメモリ・マネージャを持たせることが可能で、それはMT4のメモリ・マネージャになります。