Solaris 動的トレースガイド

第 8 章 型と定数の定義

この章では、D で型の別名と名前付き定数を宣言する方法について説明します。D プログラムでの型と名前空間の管理、オペレーティングシステムの型と識別子についても説明します。

Typedef

キーワード typedef は、既存の型の別名として識別子を宣言するときに使用します。D 型の宣言がすべてそうであるように、キーワード typedef は、以下の形式の宣言で、プローブ節の外側に記述します。

typedef existing-type new-type ;

existing-type は任意の型の宣言、new-type はこの型の別名として使用する識別子です。たとえば次のような宣言があるとします。

typedef unsigned char uint8_t;

この宣言が D コンパイラで内部使用されることにより、uint8_t という型の別名が生成されます。型の別名は、標準の型と同じように使用できます。たとえば、変数の型、連想配列値の型、組のメンバーの型として使用できます。typedef は、新しい struct の宣言など、さらに複雑な宣言と組み合わせることができます。

typedef struct foo {
	int x;
	int y;
} foo_t;

この例の struct foo には、その別名 foo_t と同じ型が定義されています。Solaris の C システムヘッダーでは、typedef の別名は、接尾辞 _t で表されることがよくあります。

列挙

プログラム内の定数の記号名を定義すると、読みやすく、将来的に管理しやすいプログラムになります。このためには、「列挙」を定義します。列挙は、一連の整数と、「列挙子」と呼ばれる一連の識別子を関連付けます。コンパイラは、この列挙子を認識し、その位置に対応する整数値を代入します。列挙の定義には、次のような宣言を使用します。

enum colors {
	RED,
	GREEN,
	BLUE
};

この列挙の最初の列挙子 RED には値ゼロ、以降の列挙子には、前の列挙子に割り当てられた値の次の整数値が割り当てられます。各列挙子に明示的に整数値を指定することもできます。この場合は、次の例のように、列挙子と整数定数を等号で結びます。

enum colors {
	RED = 7,
	GREEN = 9,
	BLUE
};

列挙子 BLUE には値が指定されていませんが、前の列挙子が 9 に設定されているので、コンパイラによって値 10 が割り当てられます。列挙の定義後は、D プログラム内のどこでも整数定数と同じように列挙子を使用できます。列挙 enum colors は、int と同等の型としても定義されています。enum 型の変数は、int 型と同じように使用できます。また、enum 型の変数には、任意の整数値を割り当てることができます。また、型名が不要なときは、宣言内で enum 名を省略できます。

列挙子は、プログラム内の以降のすべての節、すべての宣言で使用できます。そのため、複数の列挙で同じ列挙子を定義することはできません。ただし、1 つの列挙、または複数の列挙内に、同じ値を持つ列挙子を複数個定義することは問題ありません。列挙型の変数に、対応する列挙子を持たない整数を割り当てることもできます。

D の列挙の構文は、ANSI-C の場合と同じです。D でも、オペレーティングシステムカーネルやそのロード可能なモジュールに定義された列挙にアクセスできます。ただし、これらの列挙の列挙子は、D プログラム全体で使用可能であるわけではありません。カーネル列挙子が使用可能であるのは、二項比較演算子を使って対応する列挙型のオブジェクトとの比較を行う際、その比較演算子の引数として指定された場合にかぎられます。たとえば、関数 uiomove(9F) は、次のように定義された enum uio_rw 型のパラメータを持ちます。

enum uio_rw { UIO_READ, UIO_WRITE };

列挙子 UIO_READUIO_WRITE は、通常、D プログラム内で使用可能ではありません。enum uio_rw 型の値の比較を行うことによって列挙子を拡張すれば、使用可能になります。次の例を参照してください。

fbt::uiomove:entry
/args[2] == UIO_WRITE/
{
	...
}

この例では、enum uio_rw 型の変数 args[2] と列挙子 UIO_WRITE の比較によって、書き込み要求の uiomove(9F) 関数呼び出しがトレースされます。左側の引数は列挙型なので、D コンパイラは、右側の識別子を解決する際、列挙を検索します。オペレーティングシステムカーネルには大量の列挙が定義されていますが、この機能を利用すれば、D プログラム内で重複した識別子名を誤って使用してしまうのを防ぐことができます。

インライン

D の名前付き定数は、inline 指令を使って定義することもできます。この指令を使用すると、より一般的な方法で、コンパイル時に事前に定義された値や式で置き換えられる識別子を作成できます。インライン指令は、C プリプロセッサで使用される #define 指令よりも効果的な文字列置換形式です。これは、インライン指令では置換に実際の型が割り当てられ、単なる構文素ではなくコンパイル済み構文ツリーを使って置換できるからです。インライン指令は、次の形式の宣言を使って指定します。

inline type name = expression ;

type は既存の型の型定義、name は以前にインライン変数または大域変数として定義されていない任意の有効な D 識別子、expression は任意の有効な D 式です。インライン指令の処理が完了すると、D コンパイラにより、プログラムソース内の後続の各 name インスタンスが、コンパイル済みの expression で置き換えられます。たとえば、次の D プログラムでは、文字列 "hello" と整数値 123 がトレースされます。

inline string hello = "hello";
inline int number = 100 + 23;

BEGIN
{
	trace(hello);
	trace(number);
}

インライン名は、対応する型の大域変数と同じように使用できます。コンパイル時に、インライン式が整数定数または文字列定数に入れられた場合、スカラー配列サイズなど、定数式を必要とする状況でもインライン名を使用できます。

インライン指令の評価時には、インライン式の構文エラーのチェックも行われます。式の結果の型は、D 代入演算子 (=) と同じ規則に従って、インラインで定義された型と互換性のある型でなければなりません。インライン式でインライン識別子そのものを参照することはできません。 再帰的定義は禁じられています。

DTrace ソフトウェアパッケージは、システムディレクトリ /usr/lib/dtrace に多数の D ソースファイルをインストールします。このディレクトリには、D プログラム内で使用できるインライン指令が格納されています。たとえば、signal.d ライブラリには、次のような指令が格納されています。

inline int SIGHUP = 1;
inline int SIGINT = 2;
inline int SIGQUIT = 3;
...

これらのインライン定義を使用すると、signal(3HEAD) のマニュアルページに記載されている最新の Solaris のシグナル名群にアクセスできます。同様に、errno.d ライブラリには、Intro(2) のマニュアルページに記載されている C の errno 定数のインライン指令が格納されています。

デフォルトでは、D コンパイラには、提供されているすべての D ライブラリファイルが自動的に格納されます。そのため、これらの定義はすべての D プログラムで使用できます。

型の名前空間

ここでは、D の名前空間と型関連の問題について説明します。ANSI-C をはじめとする従来の言語では、型の可視性は、その型が関数やその他の宣言内に入れ子になっているかどうかによって決定されます。C プログラムの外側 (外部スコープ) で宣言された型には、単一の大域名前空間が関連付けられます。このため、これらの型は、プログラム内のどの位置でも可視的に使用できます。外部スコープの代表例として、C ヘッダーファイルに定義された型が挙げられます。D では、従来の言語とは異なり、複数の外部スコープの型にアクセスできます。

D 言語は、オペレーティングシステムカーネル、これに関連付けられているロード可能なカーネルモジュールのセット、システム上で実行されるユーザープロセスなど、ソフトウェアスタックの複数の層を動的に監視できるように設計されています。単一の D プログラムでプローブをインスタンス化し、複数のカーネルモジュールのデータや、独立したバイナリオブジェクトにコンパイルされるその他のソフトウェア構成要素のデータを収集できます。このため、DTrace や D コンパイラに、使用可能な型として、同じ名前の複数のデータ型 (通常、定義はそれぞれ異なる) が提供されることがあります。この問題に対処するため、D コンパイラでは、各型に、それを含むプログラムオブジェクト別の名前空間が割り当てられます。特定のプログラムオブジェクトの型にアクセスするには、型名に、オブジェクト名と逆引用符 (`) で表されるスコープ演算子を指定します。

たとえば、カーネルモジュール foo に次の C 型宣言が含まれているとします。

typedef struct bar {
	int x;
} bar_t;

この場合、D から struct bar 型や bar_t 型にアクセスするには、次のように型名を使用します。

struct foo`bar				foo`bar_t

逆引用符の演算子は、型名が適切であれば、どのような状況でも使用できます。たとえば、D 変数宣言の型を指定するときや、D プローブ節にキャスト式を指定するときに使用できます。

D コンパイラでは、それぞれ名前 CD を使用する、2 つの特殊な組み込み型の名前空間を使用できます。C の型の名前空間は、最初に、int のような標準 ANSI-C 組み込み型に割り当てられています。さらに、C プリプロセッサ cpp(1) によって実行された dtrace -C オプションの結果得られた型定義が処理され、C スコープに追加されます。その結果、コンパイルエラーを発生させずに、別の型の名前空間で使用可能な型宣言を含む C ヘッダーファイルを追加できます。

D の型の名前空間は、最初に、intstring をはじめとする D の組み込み型と、uint32_t などの D の組み込み型の別名に割り当てられています。D プログラムソース内の型宣言は、新しく宣言されるたびに、自動的に D の型の名前空間に追加されます。D プログラム内で、ほかの名前空間のメンバーの型から成る複合型 (struct など) を作成した場合、宣言により、そのメンバーの型が D の名前空間にコピーされます。

D コンパイラは、名前空間が明示的に指定されていない (逆引用符の演算子が使用されていない) 型宣言を検出すると、有効な型の名前空間を検索し、指定された型名と一致する型名を見つけようとします。常に C の名前空間が最初に検索され、そのあとで D の名前空間が検索されます。C と D のどちらの名前空間でも型名が見つからない場合、有効なカーネルモジュールの型の名前空間が、カーネルモジュール ID の昇順で検索されます。この順番では必ず、コアカーネルを構成するバイナリオブジェクトがロード可能なカーネルモジュールより先に検索されます。ただし、ロード可能なモジュール間の検索順序は指定されていません。ロード可能なカーネルモジュールに定義された型にアクセスするときは、ほかのカーネルモジュールとの型名の競合を避けるため、スコープ演算子を使用してください。

D コンパイラは、C のインクルードファイルにアクセスすることなくオペレーティングシステムのソースコードに関連付けられた型に自動的にアクセスするため、Solaris のコアカーネルモジュールから提供される圧縮形式の ANSI-C デバッグ情報を使用します。このシンボリックなデバッグ情報は、システム上のすべてのカーネルモジュールに提供されるわけではありません。ユーザーがアクセスしようとした型の名前空間のモジュールに、DTrace で使用される圧縮形式の C デバッグ情報が含まれていない場合、D コンパイラからエラーが返されます。