プログラムの制御(Delphi)
Delphi 言語ガイド:インデックス への移動
パラメータの受け渡しと関数の結果処理の概念は、アプリケーション プロジェクトに着手する前に理解しておくべき重要な事がらです。パラメータや関数の結果の扱いは、呼び出し規約、パラメータの意味、渡される値の型とサイズなど、いくつかの要因によって決まります。
ここでは、以下のトピックについて説明します。
- パラメータの受け渡し
- 関数の結果の取り扱い
- メソッド呼び出しの取り扱い
- 終了手続きの概要
目次
パラメータの受け渡し
パラメータは CPU レジスタかスタックを介して手続きや関数に渡され、そのどちらを経由するかはルーチンの呼び出し規約によって決まります。呼び出し規約の詳細は、「呼び出し規約」のトピックを参照してください。
値渡しと参照渡し
変数(var)パラメータは常に参照渡しです。つまり、実際の記憶場所を指す 32 ビット ポインタとして渡されます。
値パラメータと定数(const)パラメータは、そのパラメータの型とサイズに応じて値渡しまたは参照渡しになります。
- 順序型パラメータは、対応する型の変数と同じ形式を使って、8 ビット、16 ビット、32 ビット、64 ビットのいずれかの値として渡されます。
- 実数型パラメータは常にスタックに渡されます。Single パラメータは 4 バイトを占有し、Double、Comp、Currency の各パラメータは 8 バイトを占有します。Real48 は 8 バイトを占有し、Real48 値はそのうちの下位 6 バイトに格納されます。Extended は 12 バイトを占有し、Extended 値はそのうちの下位 10 バイトに格納されます。
- 短い文字列型パラメータは短い文字列への 32 ビット ポインタとして渡されます。
- 長い文字列型パラメータまたは動的配列パラメータは、長い文字列用に割り当てられた動的メモリ ブロックへの 32 ビット ポインタとして渡されます。空の長い文字列の場合は、値 nil が渡されます。
- ポインタ型、クラス型、クラス参照型、手続きポインタ型の各パラメータは 32 ビット ポインタとして渡されます。
- メソッド ポインタは 2 つの 32 ビット ポインタとしてスタックに渡されます。インスタンス ポインタがメソッド ポインタより先にプッシュされるため、メソッド ポインタは最下位アドレスを占有します。
- register 規約および pascal 規約の場合、バリアント型パラメータは Variant 値への 32 ビット ポインタとして渡されます。
- 1 バイト、2 バイト、または 4 バイトの集合、レコード、静的配列は、8 ビット値、16 ビット値、32 ビット値として渡されます。これより大きい集合、レコード、静的配列の場合は、値への 32 ビット ポインタとして渡されます。この規則の例外として、cdecl 規約、stdcall 規約、safecall 規約の場合、レコードは常にスタックに直接渡されます。その際、渡されるレコードのサイズは最も近いダブルワード境界まで切り上げられます。
- オープン配列型パラメータは 2 つの 32 ビット値として渡されます。第 1 の値は配列データへのポインタで、第 2 の値は配列の要素数から 1 を引いた値となります。
2 つのパラメータがスタックに渡されると、各パラメータは 4 バイト(ダブルワード)の倍数を占有します。8 ビットまたは 16 ビットのパラメータの場合、パラメータが 1 バイトまたは 1 ワードしか占有しなくても、ダブルワードとして渡されます。このダブルワードの未使用部分の内容は未定義です。
pascal、cdecl、stdcall、safecall の各規約
pascal、cdecl、stdcall、safecall の各規約に従う場合、パラメータはすべてスタックに渡されます。pascal 規約の場合、パラメータは宣言順(左から右)にプッシュされるため、最終的に、最初のパラメータが最上位アドレスに置かれ、最後のパラメータが最下位アドレスに置かれます。cdecl、stdcall、safecall の各規約の場合、パラメータは宣言とは逆の順(右から左)にプッシュされるため、最終的に、最初のパラメータが最下位アドレスに置かれ、最後のパラメータが最上位アドレスに置かれます。
register 規約
register 規約に従う場合、最大 3 つのパラメータが CPU レジスタに渡され、残りがあれば、それらはスタックに渡されます。これらのパラメータは宣言順に渡され(pascal 規約と同様)、適格な最初の 3 つのパラメータが EAX、EDX、ECX の各レジスタにこの順に渡されます。実数型、メソッドポインタ型、バリアント型、Int64 型、構造化型は、レジスタ パラメータとしては不適格ですが、その他のパラメータはすべてレジスタ パラメータとして使用できます。レジスタ パラメータとして適格なパラメータが 4 つ以上ある場合、最初の 3 つのパラメータが EAX、EDX、ECX に渡され、残りのパラメータは宣言順にスタックにプッシュされます。たとえば、次のような宣言があるとします。
procedure Test(A: Integer; var B: Char; C: Double; const D: string; E: Pointer);
Test を呼び出すと、A が 32 ビット整数として EAX に、B が Char へのポインタとして EDX に、D が長い文字列メモリ ブロックへのポインタとして ECX にそれぞれ渡され、C と E は 2 つのダブルワードと 32 ビットポインタとしてこの順にスタックにプッシュされます。
レジスタ保存規約
手続きと関数では EBX、ESI、EDI,、EBP の各レジスタの値を保持する必要がありますが、EAX、EDX、ECX の各レジスタの値については変更することができます。コンストラクタまたはデストラクタをアセンブリ言語で実装する場合は、必ず DL レジスタの値を保持してください。手続きと関数は CPU のディレクション フラグがクリアされている(CLD 命令に対応)ことを前提として呼び出され、戻り時にもディレクション フラグがクリアされている必要があります。
メモ:Delphi 言語の手続きと関数は一般に、FPU スタックが空であることを前提に呼び出されます。コンパイラは、コード生成時に FPU スタックの 8 個のエントリをすべて使用しようとします。
MMX 命令および XMM 命令を扱う際は、xmm レジスタおよび mm レジスタの値を必ず保持してください。Delphi の関数は、x87 浮動小数点命令で x87 FPU データ レジスタが使用可能であることを前提に呼び出されます。つまり、コンパイラは、MMX 操作の後に EMMS/FEMMS 命令が呼び出されたものと仮定します。Delphi 関数は、xmm レジスタの状態と内容については何も仮定しません。また、xmm レジスタの内容が変わらないことを保証しません。
関数の結果の取り扱い
関数が結果値を返す際には、以下の規約が用いられます。
- 順序型の関数結果は、可能な限り CPU レジスタで返されます。バイトは AL で、ワードは AX で、ダブルワードは EAX で、それぞれ返されます。
- 実数型の関数結果は、浮動小数点コプロセッサのスタックトップ レジスタ(ST(0))で返されます。Currency 型の関数結果の場合、ST(0) 内の値は 10000 倍されます。たとえば、Currency 値 1.234 は 12340 として ST(0) で返されます。
- 文字列型、動的配列型、メソッド ポインタ型、バリアント型の関数結果の場合は、宣言されたパラメータに続く追加の var パラメータとして関数結果が宣言されたものとして扱われます。つまり、呼び出し側は、関数結果を返す変数を指す追加の 32 ビット ポインタを渡します。
- Int64 型の関数結果は EDX:EAX で返されます。
- ポインタ型、クラス型、クラス参照型、手続きポインタ型の関数結果は EAX で返されます。
- 静的配列型、レコード型、集合型の関数結果については、値が 1 バイトを占有する場合は AL で、値が 2 バイトを占有する場合は AX で、値が 4 バイトを占有する場合は EAX で、それぞれ結果が返されます。それ以外の場合は、宣言されたパラメータの後に関数に渡される追加の var パラメータで結果が返されます。
メソッド呼び出しの取り扱い
メソッドは、暗黙の追加パラメータ Self(メソッドが呼び出されるインスタンスまたはクラスへの参照)を持つ点を除き、通常の手続きや関数と同じ呼び出し規約に従います。Self パラメータは 32 ビット ポインタとして渡されます。
- register 規約に従う場合、Self パラメータは、他のすべてのパラメータより前に宣言されたかのように動作します。そのため、Self パラメータは常に EAX レジスタに渡されます。
- pascal 規約に従う場合、Self パラメータは、(関数結果用に渡されることがある追加の var パラメータも含めて)他のすべてのパラメータより後に宣言されたかのように動作します。そのため、Self パラメータは最後にプッシュされ、結果的に他のすべてのパラメータより下位のアドレスに置かれることになります。
- cdecl 規約、stdcall 規約、safecall 規約に従う場合、Self パラメータは、他のすべてのパラメータより前に(ただし、関数結果用に渡されることがある追加の var パラメータより後に)宣言されたかのように動作します。そのため、Self パラメータは、追加の var パラメータを除き、最後にプッシュされます。
コンストラクタとデストラクタは、その呼び出しのコンテキストを示すために追加の論理型(Boolean 型)フラグ パラメータが渡される点を除き、他のメソッドと同じ呼び出し規約に従います。
コンストラクタ呼び出しのフラグ パラメータの値が偽(False)の場合、そのコンストラクタがインスタンス オブジェクトまたはキーワード inherited を通じて呼び出されたことを示します。この場合、コンストラクタは通常のメソッドのように動作します。コンストラクタ呼び出しのフラグ パラメータの値が真(True)の場合は、そのコンストラクタがクラス参照を通じて呼び出されたことを示します。この場合、コンストラクタは Self で与えられたクラスのインスタンスを作成し、新規作成されたオブジェクトへの参照を EAX で返します。
デストラクタ呼び出しのフラグ パラメータの値が偽(False)の場合は、そのデストラクタがキーワード inherited を使って呼び出されたことを示します。この場合、デストラクタは通常のメソッドのように動作します。デストラクタ呼び出しのフラグ パラメータの値が真(True)の場合は、そのデストラクタがインスタンス オブジェクトを通じて呼び出されたことを示します。この場合、デストラクタは Self で与えられたインスタンスの割り当てを、戻る直前に解除します。
フラグ パラメータは、他のすべてのパラメータより前に宣言されたかのように動作します。register 規約に従う場合は、DL レジスタに渡されます。pascal 規約に従う場合は、他のすべてのパラメータよりも前にプッシュされます。cdecl、stdcall、safecall の各規約に従う場合は、Self パラメータの直前にプッシュされます。
DL レジスタは、コンストラクタまたはデストラクタが呼び出し履歴内で最も外側に位置するかどうかを示しているため、終了する前に DL の値を元に戻して、BeforeDestruction または AfterConstruction が適切に呼び出されるようにする必要があります。
終了手続きの概要
終了手続きを使用すると、ファイルの更新やクローズなどの特定のアクションをプログラムの終了前に確実に実行できます。ポインタ型変数 ExitProc を使用すると、終了手続きを組み込んで、正常終了、Halt の呼び出しによる強制終了、実行時エラーによる終了のいずれかを問わず、プログラムの終了処理の一環として必ず呼び出されるようにすることができます。終了手続きはパラメータを取りません。
メモ:あらゆる終了動作には、終了手続きではなく終了処理部を使用することをお勧めします。Exit 手続きは実行可能ファイルにのみ使用可能です。.DLL(Win32)の場合は、DllProc という同様の変数を使用できます。この変数は、ライブラリの読み込み時とアンロード時に呼び出されます。パッケージの場合は、終了処理部に終了動作を実装する必要があります。終了手続きはすべて、終了処理部の実行前に呼び出されます。
プログラムだけでなく、ユニットも終了手続きを組み込むことができます。ユニットでは、初期化コードの一部として終了手続きを組み込み、ファイルを閉じるなどのクリーンアップ タスクの実行をその終了手続きに任せることができます。
終了手続きは、適切に実装されると、終了手続きチェーンの一部となります。終了手続きは組み込みとは逆の順に実行されるため、あるユニットの終了コードがそれに依存しているあらゆるユニットの終了コードより先に実行されることはありません。終了手続きの連鎖を常に正常に機能させるには、独自の終了手続きのアドレスを指すように設定する前に ExitProc の現在の内容を保存しておく必要があります。また、終了手続きの最初の文で、保存した ExitProc の値を組み込み直す必要もあります。
次のコードは終了手続きのスケルトン実装を示しています。
var
ExitSave: Pointer;
procedure MyExit;
begin
ExitProc := ExitSave; // always restore old vector first
.
.
.
end;
begin
ExitSave := ExitProc;
ExitProc := @MyExit;
.
.
.
end.
このコードでは、エントリ時に ExitProc の内容を ExitSave に保存した後、MyExit 手続きを組み込んでいます。終了プロセスの一部として呼び出されたとき、MyExit は、まず最初に前の終了手続きを組み込み直します。
ランタイム ライブラリの終了ルーチンは ExitProc が nil になるまで終了手続きを呼び出し続けます。無限ループを避けるために、毎回呼び出しの前に ExitProc が nil に設定されるので、現在の終了手続きで ExitProc にアドレスが代入された場合にのみ、次の終了手続きが呼び出されます。終了手続きでエラーが発生した場合、その終了手続きが再び呼び出されることはありません。
終了手続きでは、整数型変数 ExitCode とポインタ型変数 ErrorAddr を調べることで、終了の原因がわかります。正常終了の場合、ExitCode はゼロになり、ErrorAddr は nil になります。Halt の呼び出しによる終了の場合、ExitCode には Halt に渡された値が格納されており、ErrorAddr は nil になります。実行時エラーによる終了の場合は、ExitCode にエラー コードが格納され、ErrorAddr にはエラーの発生した文のアドレスが格納されています。
最後の終了手続き(ランタイム ライブラリによって組み込まれた終了手続き)は Input ファイルと Output ファイルを閉じます。ErrorAddr が nil 以外の場合は、実行時エラー メッセージが出力されます。独自の実行時エラー メッセージを出力するには、ErrorAddr の値を調べ、それが nil 以外の場合にメッセージを出力するような終了手続きを組み込みます。その終了手続きから戻る前には、ErrorAddr を nil に設定して、他の終了手続きでこのエラーが再度報告されないようにしてください。
いったんすべての終了手続きを呼び出すと、ランタイム ライブラリはオペレーティング システムに制御を戻し、ExitCode に格納されている値をリターン コードとして渡します。