動的にロードされるライブラリの作成
ライブラリとパッケージ:インデックス への移動
ライブラリは、パッケージと比較すると、エクスポートできるものに関してより大きな制限があります。 ライブラリは定数、型、および通常の変数をエクスポートできません。 つまり、ライブラリで定義されたクラス型は、そのライブラリを使用するプログラムでは見えません。 単純なプロシージャや関数以外の項目をエクスポートするには、代わりにパッケージを使用することを推奨します。 ライブラリを検討しなければならないのは、他のプログラムとの相互運用性が要件の 1 つになる場合だけです。
この後のトピックでは、動的ロード可能ライブラリを書くときの、以下の要素について説明します。
- exports 句
- ライブラリ初期化コード
- グローバル変数
- ライブラリとシステム変数
ライブラリでのエクスポート句の使用
動的ロード可能ライブラリのメインのソースは、library という予約語(program ではなく)で始まる以外は、プログラムのものと同じです。
ライブラリで明示的にエクスポートしたルーチンだけが、他のライブラリやプログラムにインポート可能になります。 次の例に挙げるライブラリでは、Min および Max という 2 つの関数をエクスポートしています。
library MinMax; function Min(X, Y: Integer): Integer; stdcall; begin if X < Y then Min := X else Min := Y; end; function Max(X, Y: Integer): Integer; stdcall; begin if X > Y then Max := X else Max := Y; end; exports Min, Max; begin end.
作成したライブラリを他の言語で書かれたアプリケーションで利用できるようにしたい場合、エクスポートする関数の宣言内で stdcall を指定するのが最も安全です。 他の言語では、Delphi のデフォルトである register 呼び出し規則をサポートしていない可能性があるからです。
ライブラリは複数のユニットを基に構築することができます。 その場合、ライブラリのソース ファイルは、uses 句と、exports 句、そして初期化コードだけになることがよくあります。 次に例を示します。
library Editors;
uses EdInit, EdInOut, EdFormat, EdPrint;
exports
InitEditors,
DoneEditors name Done,
InsertText name Insert,
DeleteSelection name Delete,
FormatSelection,
PrintSelection name Print,
.
.
.
SetErrorHandler;
begin
InitLibrary;
end.
exports 句は、ユニットの interface セクションまたは implementation セクションに含めることができます。 そのようなユニットを uses 句に含んでいるライブラリでは、ライブラリ自身に exports 句が記述されていなくても、ユニットの exports 句に含まれるルーチンが自動的にエクスポートされます。
ルーチンをエクスポートするには、次の形式で exports 句に指定します。
exports entry1, ..., entryn;
ここでの各エントリは、プロシージャか関数か変数の名前(exports 句より前で宣言したものでなければなりません)の後に、パラメータ リスト(オーバーロードされているルーチンをエクスポートする場合のみ)と、オプションで name 指定子を続けたものです。 プロシージャや関数の名前は、ユニット名で修飾することができます。
(これらのエントリには、resident 指令も含めることができます。ただしこれは下位互換性のために維持されているもので、コンパイラには無視されます。)
Win32 プラットフォームでは、index 指定子は、index 指令とその後に続く 1 ~ 2,147,483,647 の間の数値定数で構成されます。(プログラムの効率を上げるには、小さいインデックス値を使用してください)。 エントリに index 指定子がない場合、ルーチンにはエクスポート テーブル内の 1 つの番号が自動的に割り当てられます。
index 指定子は下位互換性のためだけにサポートされているものなので、使用は推奨されておらず、使用すると他の開発ツールで問題が生じる可能性があります。
name 指定子は、name 指令とそれに続く文字列定数で構成されます。 エントリに name 指定子がない場合、ルーチンは、大文字/小文字の区別を含めて元々宣言されたものとまったく同じスペルの名前でエクスポートされます。 ルーチンを別の名前でエクスポートしたい場合に name 句を指定してください。 次に例を示します。
exports DoSomethingABC name 'DoSomething';
オーバーロードされた関数やプロシージャを動的ロード可能ライブラリからエクスポートする場合は、パラメータ リストを exports 句に指定しなければなりません。 次に例を示します。
exports Divide(X, Y: Integer) name 'Divide_Ints', Divide(X, Y: Real) name 'Divide_Reals';
Win32 では、オーバーロードされたルーチンのエントリに index 指定子を含めないでください。
exports 句は、プログラムまたはライブラリの宣言部分や、ユニットの interface セクションまたは implementation セクションの中の、任意の場所で任意の回数だけ記述することができます。 プログラムに exports 句が含まれることはほとんどありません。
ライブラリ初期化コード
ライブラリのブロック内の文は、ライブラリの初期化コードとなります。 これらの文は、ライブラリがロードされるたびに 1 度だけ実行されます。 一般的にこのコードでは、ウィンドウ クラスの登録や変数の初期化などの処理を行います。 ライブラリの初期化コードでは、DllProc 変数を使ってエントリ ポイント プロシージャを導入することもできます。 DllProc 変数は、終了プロシージャと同じようなものです(Exit プロシージャの説明を参照してください)。エントリ ポイント プロシージャは、ライブラリがロードまたはアンロードされるときに実行されます。
ライブラリ初期化コードで ExitCode 変数を 0 以外の値に設定すると、エラーの発生を知らせることができます。 ExitCode は System ユニットで宣言されているもので、デフォルト値の 0 は初期化が成功したことを示します。 ライブラリの初期化コードで ExitCode が別の値に設定されると、ライブラリはアンロードされ、呼び出し側のアプリケーションに失敗が通知されます。 同様に、初期化コード実行中に未処理の例外が発生すると、ライブラリをロードしようとして失敗したことが呼び出し側のアプリケーションに通知されます。
次に示すのは、初期化コードとエントリ ポイント プロシージャを持つライブラリの例です。
library MyDll;
uses
System.SysUtils,
System.IOUtils,
{$IFDEF MSWINDOWS}
Windows,
{$ENDIF}
CrossPlatform.Methods in 'CrossPlatform.Methods.pas';
{$R *.res}
exports
//mac requires _understaore methods in the export or it will not find the entrypoint
{$IFDEF MACOS}
_myMethod
{$ELSE}
MyMethod
{$ENDIF}
{$IFDEF MSWINDOWS}
//unload windows method here
var
SaveDllProc: TDLLProc;
procedure LibExit(Reason: Integer);
begin
if Reason = DLL_PROCESS_DETACH then
begin
//unload code
end;
if Assigned(SaveDllProc) then
SaveDllProc(Reason); // Call the saved entry point procedure
end;
{$ENDIF}
begin
{$IFDEF MSWINDOWS}
SaveDllProc := DllProc; // Save the original DLL procedure
DllProc := @LibExit; // Set our exit procedure
{$ENDIF}
// Any additional library initialization code
end.
DllProc は、ライブラリが初めてメモリにロードされたときや、スレッドが開始または停止したとき、ライブラリがアンロードされたときに呼び出されます。 ライブラリが使用するすべてのユニットの初期化部分は、ライブラリの初期化コードの前に実行され、それらのユニットの終了処理部分は、ライブラリのエントリ ポイント プロシージャの後に実行されます。
ライブラリ内のグローバル変数
共有ライブラリ内で宣言されたグローバル変数を Delphi アプリケーションにインポートすることはできません。
1 つのライブラリを一度に複数のアプリケーションで使用することは可能ですが、それは、アプリケーションそれぞれのプロセス空間内にライブラリとグローバル変数群のコピーが独自に作られるということです。 複数のライブラリ、つまりライブラリの複数のインスタンスでメモリを共有するには、メモリ マップド ファイルを使用する必要があります。 詳細は、お使いのシステムのマニュアルを参照してください。
ライブラリとシステム変数
System ユニットで宣言されているいくつかの変数は、ライブラリをプログラミングするにあたって非常に重要です。 IsLibrary は、コードがアプリケーションで実行されているのかライブラリで実行されているのかを判断するためのもので、アプリケーションの場合には必ず False に、ライブラリの場合には必ず True になります。 ライブラリが存続している間はずっと、HInstance にそのインスタンス ハンドルが含まれます。 ライブラリでは CmdLine は必ず nil になっています。
DLLProc 変数を使うことで、ライブラリは、オペレーティング システムがライブラリ エントリ ポイントに対して行う呼び出しを監視することができます。 通常、この機能を使用するのは、マルチスレッド処理をサポートするライブラリだけです。 DLLProc はアプリケーションをマルチスレッド化するのに使われます。 終了時の振る舞いはすべて、終了プロシージャではなく終了処理部分に記述してください。
オペレーティング システムの呼び出しを監視するには、整数のパラメータを 1 つ取るコールバック プロシージャを作成します。たとえば次のようなものです。
procedure DLLHandler(Reason: Integer);
そして、そのプロシージャのアドレスを DLLProc 変数に代入します。 このプロシージャが呼び出されるときには、以下の値のいずれかが渡されます。
|
DLL_PROCESS_DETACH |
正常に終了処理が行われた、または FreeLibrary が呼び出された結果、ライブラリが呼び出し側プロセスのアドレス空間から切り離されていることを示します。 |
|
DLL_PROCESS_ATTACH |
LoadLibrary が呼び出された結果、ライブラリが呼び出し側プロセスのアドレス空間に接続されていることを示します。 |
|
DLL_THREAD_ATTACH |
現在のプロセスが新しいスレッドを作成していることを示します。 |
|
DLL_THREAD_DETACH |
スレッドが正常に終了していることを示します。 |
プロシージャの本体では、プロシージャにどのパラメータが渡されたかに応じて、行う処理を指定することができます。
ライブラリの例外と実行時エラー
発生した例外が動的ロード可能ライブラリの中で処理されなかった場合、その例外はライブラリから呼び出し側に伝播します。 呼び出し側のアプリケーションまたはライブラリ自体が Delphi で書かれている場合、例外は標準的な try...except 文で処理することができます。
Win32 で、呼び出し側のアプリケーションまたはライブラリが別の言語で書かれている場合、例外は、$0EEDFADE という例外コードを持つオペレーティング システム例外として処理することができます。 オペレーティング システム例外レコードの ExceptionInformation 配列の最初のエントリには例外アドレスが、2 番目のエントリには Delphi 例外オブジェクトの参照が含まれます。
一般には、例外をライブラリの外に出すべきではありません。 Delphi 例外は OS の例外モデルにマッピングしています。
ライブラリで SysUtils ユニットを使用していなければ、例外のサポートは無効になっています。 その場合、ライブラリで実行時エラーが発生すると、呼び出し側のアプリケーションは終了します。 ライブラリ側では自分が Delphi プログラムから呼び出されたのかどうかを知ることができないため、アプリケーションの終了プロシージャを呼び出すことができず、アプリケーションは単純に異常終了し、メモリから削除されます。
共有メモリ マネージャ
Win32 で、パラメータや関数の結果として長い文字列や動的配列を渡すルーチンを DLL からエクスポートする場合には(直接渡す場合もレコードやオブジェクトにネストさせる場合も含みます)、その DLL とクライアント アプリケーション(またはクライアント DLL)はすべて ShareMem ユニットを使用する必要があります。 あるアプリケーションや DLL が New または GetMem で割り当てたメモリを、別のモジュールが Dispose または FreeMem を呼び出して解放する場合にも、同じことが当てはまります。 ShareMem は、プログラムやライブラリの uses 句に列挙するユニットの中で、必ず最初に記述しなければなりません。
ShareMem は、動的に割り当てられるメモリをモジュール間で共有できるようにするための BORLANDMM.DLL メモリ マネージャのインターフェイス ユニットです。 BORLANDMM.DLL は、ShareMem を使用するアプリケーションや DLL と一緒に配置する必要があります。 アプリケーションや DLL で ShareMem を使用する場合、元のメモリ マネージャは BORLANDMM.DLL のメモリ マネージャに置き換えられます。