Solaris 动态跟踪指南

第 8 章 类型和常量定义

本章介绍如何在 D 中声明类型别名和命名的常量。本章还将讨论程序和操作系统类型及标识符的 D 类型和名称空间管理。

Typedef

typedef 关键字用于将一个标识符声明为现有类型的别名。与所有 D 类型声明一样,typedef 关键字在探测器子句外部的以下格式的声明中使用:

typedef existing-type new-type ;

其中,existing-type 是任何类型的声明,new-type 是要用作此类型别名的标识符。例如,D 编译器在内部使用以下声明:

typedef unsigned char uint8_t;

创建 uint8_t 类型别名。可以使用常规类型(例如变量类型、关联数组值类型或元组成员类型)的位置都可以使用类型别名。您也可以将 typedef 与更详细的声明(如新的 struct 定义)组合在一起。

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

在此示例中,struct foo 定义为与其别名 foo_t 相同的类型。Solaris C 系统头通常使用后缀 _t 表示 typedef 别名。

枚举

在程序中为常量定义符号名称,会增强程序的可读性,并使将来对程序的维护变得简单明了。一种方法是定义枚举,即,将一组整数与一组称为枚举器的标识符进行关联,编译器可以识别这些枚举器并将其替换为相应的整数值。使用如下所示的声明定义枚举:

enum colors {
	RED,
	GREEN,
	BLUE
};

枚举中的第一个枚举器 RED 被赋予的值为零,每个后续标识符被赋予下一个整数值。您也可以通过在任何枚举器的后面加上等号和整型常数,来为该枚举器指定显式的整数值。

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

由于枚举器 BLUE 没有指定值,且前一个枚举器设置为 9,所以编译器将为其赋予值 10。定义枚举之后,D 程序中可以使用整型常数的任何位置都可以使用该枚举器。此外,enum colors 还定义为与 int 等效的类型。D 编译器允许在可以使用 int 的任何位置使用 enum 类型的变量,并允许将任何整数值赋给 enum 类型的变量。如果不需要类型名称,也可以在声明中省略 enum 名称。

枚举器在程序的所有后续子句和声明中都可见,因此不能在多个枚举中定义相同的枚举器标识符。但是,您可以在相同或不同的枚举中定义具有相同值的多个枚举器。您也可以将没有相应枚举器的整数赋给枚举类型的变量。

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/
{
	...
}

此示例通过将 args[2]enum uio_rw 类型的变量)与枚举器 UIO_WRITE 进行比较,来跟踪写入请求对 uiomove(9F) 函数的调用。因为左边的参数为枚举类型,D 编译器将在尝试解析右边的标识符时,在该枚举中进行搜索。此功能可以避免 D 程序无意中与操作系统内核中定义的大枚举集合产生标识符名称冲突。

内置

也可以使用 inline 指令定义 D 命名的常量,该指令提供了一种更常规的方法来创建编译期间将替换为预定义的值或表达式的标识符。与 C 预处理程序提供的 #define 指令相比,内置指令是一种功能更强大的词法替换。内置指令使用以下格式的声明指定:

inline type name = expression ;

其中,type 是现有类型的类型声明,name 是先前未定义为内置或全局变量的任何有效 D 标识符,expression 是任何有效 D 表达式。在处理内置指令之后,D 编译器将使用程序源代码中已编译格式的 expression 替换 name 的每个后续实例。例如,以下 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;
...

这些内置定义使您可以访问当前的 Solaris 信号名称集,如 signal(3HEAD) 中所述。类似地,errno.d 库包含 C errno 常量的内置指令,如 Intro(2) 中所述。

缺省情况下,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 barbar_t

struct foo`bar				foo`bar_t

在类型名称合适的任何上下文中(包括当为 D 探测子句中的 D 变量声明或强制类型转换表达式指定类型时)都可以使用反引号运算符。

D 编译器还提供了两种特殊的内置类型名称空间(分别使用名称 CD)。C 类型名称空间最初使用标准 ANSI-C 内部类型(如 int)填充。此外,使用 C 预处理程序 cpp(1) 并使用 dtrace -C 选项获取的类型定义将由 C 范围处理并添加到其中。因此,可以将已可见的类型指令所在的 C 头文件包含到另一类型名称空间中,而不会引起编译错误。

D 类型名称空间初始使用 D 内部类型(如 intstring)及内置 D 类型别名(如 uint32_t)填充。出现在 D 程序源代码中的任何新类型声明都将自动添加到 D 类型名称空间中。如果在 D 程序中创建由来自其他名称空间的成员类型组成的复杂类型(如 struct),这些成员类型将会根据声明复制到 D 名称空间中。

当 D 编译器遇到未使用反引号运算符指定显式名称空间的类型声明时,编译器将会使用指定的类型名称搜索活动的类型名称空间集以找到匹配项。将始终首先搜索 C 名称空间,然后搜索 D 名称空间。如果在 C 或 D 名称空间中未找到类型名称,则将会按内核模块 ID 的升序搜索活动的内核模块的类型名称空间。此顺序保证,将首先搜索构成核心内核的二进制对象,然后搜索任何可装入的内核模块,但不能保证可装入模块间的任何顺序属性。访问可装入内核模块中定义的类型时,应使用作用域运算符以避免与其他内核模块发生类型名称冲突。

D 编译器使用为核心 Solaris 内核模块提供的压缩 ANSI-C 调试信息,以便自动访问与操作系统源代码关联的类型,而无需访问相应的 C 包含文件。此符号调试信息可能并不对系统上的所有内核模块都可用。如果尝试访问模块的名称空间内的类型,而该模块缺少要用于 DTrace 的压缩 C 调试信息,则 D 将会报告错误。