在PHP的源码中经常会看到的一些很常见的宏,或者有些对于才开始接触源码的读者比较难懂的代码。 这些代码在PHP的源码中出现的频 率极高,基本在每个模块都会有他们的身影。本篇文章我们提取中间的一些进行说明。
1. “##” 和 “#” 宏是C/C++是非常强大,使用也很多的一个功能,有时用来实现类似函数内联的效果,或者将复杂的代码进行简单封装,提高可读性或可 移植性等。在PHP的宏定义中经常使用双井号。下面对 “##” 及 “#” 进行详细介绍。
- 双井号(##) 在C语言的宏中,”##”被称为 连接符(concatenator),它是一种预处理运算符, 用来把两个语言符号(Token)组合成单个语言符号。 这里的语言符号不一定是宏的变量。并且双井号不能作为第一个或最后一个元素存在。如下所示源码:
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_NAMED_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); 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 的值连接起来。以这种连接的方式为基 础,多次使用这种宏形式,可以将它当作一个代码生成器,这样可以在一定程度上减少代码密度,我们也可以将它理解为一种代码重用的手段,间接地减少不小心所造成的错误。
-单井号(#) “#”是一种预处理运算符,它的功能是将其后面的宏参数进行字符串化操作 , 简单说就是在对它所引用的宏变量通过替换后在其左右各加上一个双引号,用比较官方的话说就是将语言符号(Token)转化为字符串。例如:
1 2 3 4 5 6 #define STR(x) #x int main (int argc char ** argv) { printf ("%s\n" , STR(It' s a long string )); return 0 ; }
如前文所说,It's a long string
是宏 STR 的参数,在展开后被包裹成一个字符串了。所以 printf 函数能直接输出这个字符串, 当然这个使用场景并不是很适合,因为这种用法并没有实际的意义,实际中在宏中可能会包裹其他的逻辑,比如对字符串进行封装等等。
关于宏定义中的 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) 语句格式。如果我们搜索整个PHP的源码目录,会发现这样的语句还有很多。在其他使用 C/C++ 编写的程序中也会有很多这种编写宏的代码,多行宏的这种格式已经是一种公认的编写方式了。为什么在宏定义时需要使用 do-while 语句呢?我们知道 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 预处理 #line 838 "Zend/zend_language_scanner.c"
#line预处理用于改变当前的行号 (__LINE__)
和文件名 (__FILE__)
。如上所示代码,将当前的行号改变为838,文件名 Zend/zend_language_scanner.c
它的作用体现在编译器的编写中,我们知道编译器对 C 源码编译过程中会产生一些中间文件,通过这条指令,可以保证文件名是固定的,不会被这些中间文件代替,有利于进行调试分析。
- PHP 中的全局变量宏 在PHP代码中经常能看到一些类似 PG()
、EG()
之类的函数,他们都是 PHP 中定义的宏,这系列宏主要的作用是解决线程安全所写的全局变量包裹宏,如 main/php_globals.h
文件中就包含了很多这类的宏。例如 PG 这个 PHP 的核心全局变量的宏。如下所示代码为其定义:
1 2 3 4 5 #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) #endif
如上,ZTS是线程安全的标记。下面简单说说,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 struct _php_core_globals { zend_bool magic_quotes_gpc; zend_bool magic_quotes_runtime; zend_bool magic_quotes_sybase; zend_bool safe_mode; zend_bool allow_call_time_pass_reference; zend_bool implicit_flush; long output_buffering; char *safe_mode_include_dir; zend_bool safe_mode_gid; zend_bool sql_safe_mode; zend_bool enable_dl; char *output_handler; char *unserialize_callback_func; long serialize_precision; char *safe_mode_exec_dir; long memory_limit; long max_input_time; zend_bool track_errors; zend_bool display_errors; zend_bool display_startup_errors; zend_bool log_errors; long log_errors_max_len; zend_bool ignore_repeated_errors; zend_bool ignore_repeated_source; zend_bool report_memleaks; char *error_log; char *doc_root; char *user_dir; char *include_path; char *open_basedir; char *extension_dir; char *upload_tmp_dir; long upload_max_filesize; char *error_append_string; char *error_prepend_string; char *auto_prepend_file; char *auto_append_file; arg_separators arg_separator; char *variables_order; HashTable rfc1867_protected_variables; short connection_status; short ignore_user_abort; unsigned char header_is_being_sent; zend_llist tick_functions; zval *http_globals[6 ]; zend_bool expose_php; zend_bool register_globals; zend_bool register_long_arrays; zend_bool register_argc_argv; zend_bool auto_globals_jit; zend_bool y2k_compliance; char *docref_root; char *docref_ext; zend_bool html_errors; zend_bool xmlrpc_errors; long xmlrpc_error_number; zend_bool activated_auto_globals[8 ]; zend_bool modules_activated; zend_bool file_uploads; zend_bool during_request_startup; zend_bool allow_url_fopen; zend_bool always_populate_raw_post_data; zend_bool report_zend_debug; int last_error_type; char *last_error_message; char *last_error_file; int last_error_lineno; char *disable_functions; char *disable_classes; zend_bool allow_url_include; zend_bool exit_on_timeout; #ifdef PHP_WIN32 zend_bool com_initialized; #endif long max_input_nesting_level; zend_bool in_user_include; char *user_ini_filename; long user_ini_cache_ttl; char *request_order; zend_bool mail_x_header; char *mail_log; zend_bool in_error_log; };
上面的字段很大一部分是与 php.ini 文件中的配置项对应的。在 PHP 启动并读取 php.ini 文件时就会对这些字段进行赋值,而用户空间的 ini_get()
及 ini_set()
函数操作的一些配置也是对这个全局变量进行操作的。
在 PHP 代码的其他地方也存在很多类似的宏,这些宏和 PG 宏一样,都是为了将线程安全进行封装,同时通过约定的 G 命名来表明这是全局的, 一般都是个缩写,因为这些全局变量在代码的各处都会使用到,这也算是减少了键盘输入。 我们都应该尽可能的懒不是么?
如果你阅读过一些 PHP 扩展话应该也见过类似的宏,这也算是一种代码规范,在编写扩展时全局变量最好也使用这种方式命名和包裹,因为我们不能对用户的 PHP 编译条件做任何假设。