「深入理解PHP内核」 - 准备工作和背景知识

TIPI:深入理解PHP内核

常用代码

在PHP源码中经常会看到一些很常见的宏,这些代码在PHP的源码中出现的频率极高

  • ‘##’ 和 ‘#’

宏是C/C++非常强大,使用也很多的一个功能,有时用来实现类似函数内联的效果,或者将复杂的代码进行简单封装,提高可读性或者可移植性。

在PHP的宏定义中经常使用双井号

双井号’##’:

在C语言的宏中,’##’被称为连接符,它是一种预处理运算符,用来把两个语言符号组合成单个语言符号。这里的语言符号不一定是宏的变量,并且双井号不能走位第一个或者最后一个元素存在。

1
2
3
4
5
6
7
8
9
#define PHP_FUNCTION                ZEND_FUNCTION
#define ZEND_FUNCTION(name) ZEND_NAMED_FUNCTION(ZEND_FN(name))
#define ZEND_FN(name) zif_##name
#define ZEND_NAME_FUNCTION(name) void name(INTERNAL_FUNCTION_PARAMETERS)
#define INTERNAL_FUNCTION_PARAMETERS int ht, zval *return_value, zval **return_value_ptr, zval *this_ptr, int return_value_used TSRMLS_DC

PHP_FUNCTION(count);
// 预处理器处理以后,PHP_FUNCTION(count)就展开为如下代码
void zif_count(int ht, zval *return_value, zval **return_value_ptr, zval *this_ptr, int return_value_used TSRMLS_DC)

宏ZEND_FN(name)中有一个’##’,它的作用一如之前所说,是一个连接符,将zif和宏的变量name的值连接起来,以这种连接的方式以基础,多次使用这种宏形式,可以将它作为一个代码生成器,这样可以在一定程度上较少代码密度,也可以将它理解为一种代码重用的手段,间接地减少不小心所造成的错误

单井号’#’:

“#”是一种预处理运算符,它的功能是将其后面的宏参数进行【字符串化】操作,简单说就是在对它所引用的宏变量通过替换后再起左右各加上一个双引号,用比较官方的话说就是讲语言符号转化为字符串,例如

1
2
3
4
5
6
7
#define STR(X) #x

int main(int argc, char** argv)
{
printf("%s\n",STR(It's a long string));// 输出 It's a long string
return 0;
}

  • 关于宏定义中的do-while循环

PHP源码中大量使用了宏操作,比如PHP5.3新增的垃圾收集机制中的一段代码:

1
2
3
4
5
6
#define ALLOC_ZVAL(z)

do {
(z) = (zval*)emalloc(sizeof(zval_gc_info));
GC_ZVAL_INIT(z);
} while(0)

这段代码,在宏定义中使用了do{}while(0)语句模式,多行宏这种格式已经是一宗公认的编写方式了。

为什么这么写呢?

因为在do-while循环语句是先执行循环再判断条件是否成立,所以说至少会执行一次,当使用do-while(0)时,由于条件肯定是false,代码也肯定只执行一次

肯定执行一次的代码为什么要放在do-while语句里呢,这种方式适用于宏定义中存在多语句的情况,如下所示代码:

1
2
3
4
5
6
#define TEST(a, b) a++;b++;

if (expr)
TEST(a, b);
else
do_else();

代码预处理后变成:

1
2
3
4
if (expr)
a++;b++;
else
do_else();

这样if-else的结构就被破坏了if后面有两个语句,这样是无法编译通过的,那为什么非要do-while而不是简单的用{}括起来呢。 这样也能保证if后面只有一个语句。例如上面的例子,在调用宏TEST的时候后面加了一个分号, 虽然这个分号可有可无, 但是出于习惯我们一般都会写上。 那如果是把宏里的代码用{}括起来,加上最后的那个分号。 还是不能通过编译。 所以一般的多表达式宏定义中都采用do-while(0)的方式。

了解了do-while循环在宏中的作用,再来看”空操作”的定义。由于PHP需要考虑到平台的移植性和不同的系统配置, 所以需要在某些时候把一些宏的操作定义为空操作。例如在sapi\thttpd\thttpd.c文件中的VEC_FREE():

1
2
3
4
5
#ifdef SERIALIZE_HEADERS
# define VEC_FREE() smart_str_free(&vec_str)
#else
# define VEC_FREE() do {} while (0)
#endif

这里涉及到条件编译,在定义了SERIALIZE_HEADERS宏的时候将VEC_FREE()定义为如上的内容,而没有定义时, 不需要做任何操作,所以后面的宏将VEC_FREE()定义为一个空操作,不做任何操作,通常这样来保证一致性, 或者充分利用系统提供的功能。

有时也会使用如下的方式来定义“空操作”,这里的空操作和上面的还是不一样,例如很常见的Debug日志打印宏:

1
2
3
4
5
#ifdef DEBUG
# define LOG_MSG printf
#else
# define LOG_MSG(...)
#endif

在编译时如果定义了DEBUG则将LOG_MSG当做printf使用,而不需要调试,正式发布时则将LOG_MSG()宏定义为空, 由于宏是在预编译阶段进行处理的,所以上面的宏相当于从代码中删除了。

上面提到了两种将宏定义为空的定义方式,看上去一样,实际上只要明白了宏都只是简单的代码替换就知道该如何选择了。

  • ‘#line’ 预处理
1
#line 838 "Zend/zend_language_scanner.c"

‘#line预处理’用于改变当前的行号(LINE)和文件名(FILE)。 如上所示代码,将当前的行号改变为838,文件名Zend/zend_language_scanner.c 它的作用体现在编译器的编写中,我们知道编译器对C 源码编译过程中会产生一些中间文件,通过这条指令, 可以保证文件名是固定的,不会被这些中间文件代替,有利于进行调试分析。

  • PHP中的全局变量宏

在PHP代码中经常能看到一些类似PG(), EG()之类的函数,他们都是PHP中定义的宏,这系列宏主要的作用是解决线程安全所写的全局变量包裹宏, 如$PHP_SRC/main/php_globals.h文件中就包含了很多这类的宏。例如PG这个PHP的核心全局变量的宏。 如下所示代码为其定义。

1
2
3
4
5
6
7
#ifdef ZTS   // 编译时开启了线程安全则使用线程安全库
# define PG(v) TSRMG(core_globals_id, php_core_globals *, v)
extern PHPAPI int core_globals_id;
#else
# define PG(v) (core_globals.v) // 否则这其实就是一个普通的全局变量
extern ZEND_API struct _php_core_globals core_globals;
#endif

PHP运行时的一些全局参数, 这个全局变量为如下的一个结构体,各字段的意义如字段后的注释:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
// 在PHP启动并读取php.ini文件时就会对这些字段进行赋值, 而用户空间的ini_get()及ini_set()函数操作的一些配置也是对这个全局变量进行操作的
struct _php_core_globals {
// 是否要求PHP输出层在每个输出块之后自动刷新数据
zend_bool implicit_flush;
// 输出缓冲区大小(字节)
zend_long output_buffering;
// 是否允许使用dl()函数,dl()函数仅在将PHP作为apache模块安装时才有效
zend_bool enable_dl;
// 将素有脚本的输出重定向到一个输出处理函数
char *output_handler;
// 如果解序列化处理器需要实例化一个未定义的类,这里指定的回调函数将以该未定义类的名字作为参数被unserialize()调用
char *unserialize_callback_func;
// 将浮点型和双精度型数据序列化存储时的精度(有效位数)
zend_long serialize_precision;

// 一个脚本能够申请到的最大的内存字节数(可以是使用K和M作为单位)
zend_long memory_limit;
// 每个脚本解析输入数据(POST, GET, UPLOAD)的最大允许时间(秒)
zend_long max_input_time;

// 是否在变量$php_errormsg中保存最近一个错误或警告消息
zend_bool track_errors;
// 是否将错误信息作为输出的一部分显示
zend_bool display_errors;
// 是否显示PHP启动时的错误
zend_bool display_startup_errors;
// 是否在日志文件里记录错误,具体在哪里记录取决于error_log指令
zend_bool log_errors;
// 设置错误日志中附加的与错误信息相关联的错误源的最大长度
zend_long log_errors_max_len;
// 记录错误日志时是否忽略重复的错误信息
zend_bool ignore_repeated_errors;
// 是否在忽略重复的错误信息时忽略重复的错误源
zend_bool ignore_repeated_source;
// 是否报告内存泄漏
zend_bool report_memleaks;
// 将错误日志记录到哪个文件中
char *error_log;


// PHP 的根目录
char *doc_root;
// 告诉PHP在使用/~username 打开脚本时去哪个目录下去找
char *user_dir;
// 指定一组目录用于require(), include(), fopen_with_path()函数寻找文件
char *include_path;
// 将PHP允许操作的所有文件(包括文件自身)都限制在此组目录列表下
char *open_basedir;
// 存放扩展库的目录
char *extension_dir;

char *php_binary;
char *sys_temp_dir;
// 文件上传时存放文件的临时目录
char *upload_tmp_dir;
// 允许上传的文件的最大尺寸
zend_long upload_max_filesize;

// 用于错误信息后输出的字符串
char *error_append_string;
// 用于错误信息前输出的字符串
char *error_prepend_string;

// 指定在主文件之前自动解析的文件名
char *auto_prepend_file;
// 指定在主文件之后自动解析的文件名
char *auto_append_file;

// 输入编码
char *input_encoding;
// 内部编码
char *internal_encoding;
// 输出编码
char *output_encoding;

// PHP所产生的URL中用来分割参数的分隔符
arg_separators arg_separator;
// PHP注册Environment, GET, POST, Cookie, Server变量的顺序
char *variables_order;
// RFC1867保护的变量名,在main/rfc1867.c文件中有用到此变量
HashTable rfc1867_protected_variables;

// 连接状态,有三个状态正常、中断、超时
short connection_status;

/* In 7.1/7.2 branches, this was initially a short,
* maintain struct alignment with subsequent padding.
*/
zend_bool ignore_user_abort;
char ignore_user_abort_reserved_padding;

// 是否头信息正在发送
unsigned char header_is_being_sent;
// 仅在main目录下的php_ticks.c文件中有用到,此处订单的函数在register_tick_function 等函数中有用到
zend_llist tick_functions;
// 存放GET、POST、SERVER等信息
zval http_globals[6];

// 是否展示PHP信息
zend_bool expose_php;

// 是否将E,G,P,C,S变量注册为全局变量
zend_bool register_argc_argv;
// 是否仅在用到$_SERVER和$_ENV变量时才创建(而不是脚本已启动就创建)
zend_bool auto_globals_jit;

// 如果打开了html_errors指令,PHP将会在出错信息上显示超链接
char *docref_root;
// 指定文件的扩展名(必须有'.')
char *docref_ext;

// 是否在出错信息中吃使用HTML标记
zend_bool html_errors;
zend_bool xmlrpc_errors;

zend_long xmlrpc_error_number;

zend_bool activated_auto_globals[8];

// 是否已经激活模块
zend_bool modules_activated;
// 是否允许HTTP文件上传
zend_bool file_uploads;
// 是否在请求初始化过程中
zend_bool during_request_startup;
// 是否允许打开远程文件
zend_bool allow_url_fopen;
// 是否总是生成$HTTP_RAW_POST_DATA变量(原始POST数据)
zend_bool enable_post_data_reading;
// 是否打开zend debug,仅在main/main.c文件中有使用
zend_bool report_zend_debug;


// 最后的错误类型
int last_error_type;
// 最后的错误信息
char *last_error_message;
// 最后的错误文件
char *last_error_file;
// 最后的错误行
int last_error_lineno;

//
char *php_sys_temp_dir;

// 该指定接受一个用逗号分隔的函数名列表,以禁用特定的函数
char *disable_functions;
// 该指令接受一个用逗号分隔的类名列表,以禁用特定的类
char *disable_classes;
// 是否允许include/require远程文件
zend_bool allow_url_include;
#ifdef PHP_WIN32
zend_bool com_initialized;
#endif
// 最大的嵌套层数
zend_long max_input_nesting_level;
// 最大的变量数量
zend_long max_input_vars;
// 是否在用户包含空间
zend_bool in_user_include;

// 用户的ini文件名
char *user_ini_filename;
// ini缓存过期限制
zend_long user_ini_cache_ttl;

// 优先级比variables_order高,在request变量生成时用到
char *request_order;

// 仅在ext/standard/mail.c文件中使用
zend_bool mail_x_header;
char *mail_log;

zend_bool in_error_log;

#ifdef PHP_WIN32
zend_bool windows_show_crt_warning;
#endif
};