在PHP的源碼中經常會看到的一些很常見的宏,或者有些對于才開始接觸源碼的讀者比較難懂的代碼。這些代碼在PHP的源碼中出現的頻率極高,基本在每個模塊都會他們的身影。本小節我們提取中間的一些進行說明。
## 1. "##"和"#"
宏是C/C++是非常強大,使用也很多的一個功能,有時用來實現類似函數內聯的效果,或者將復雜的代碼進行簡單封裝,提高可讀性或可移植性等。在PHP的宏定義中經常使用雙井號。下面對"##"及"#"進行詳細介紹。
### 雙井號(##)
在C語言的宏中,"##"被稱為 **連接符**(concatenator),它是一種預處理運算符,用來把兩個語言符號(Token)組合成單個語言符號。這里的語言符號不一定是宏的變量。并且雙井號不能作為第一個或最后一個元素存在。如下所示源碼:
~~~
#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);
?
// 預處理器處理以后, PHP_FUCNTION(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)轉化為字符串。 例如:
~~~
#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;
}
~~~
如前文所說,It's a long string 是宏STR的參數,在展開后被包裹成一個字符串了。所以printf函數能直接輸出這個字符串,當然這個使用場景并不是很適合,因為這種用法并沒有實際的意義,實際中在宏中可能會包裹其他的邏輯,比如對字符串進行封裝等等。
## 2. 關于宏定義中的do-while循環
PHP源碼中大量使用了宏操作,比如PHP5.3新增加的垃圾收集機制中的一段代碼:
~~~
#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語句里呢? 這種方式適用于宏定義中存在多語句的情況。如下所示代碼:
~~~
#define TEST(a, b) a++;b++;
?
if (expr)
TEST(a, b);
else
do_else();
~~~
代碼進行預處理后,會變成:
~~~
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():
~~~
#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日志打印宏:
~~~
#ifdef DEBUG
# define LOG_MSG printf
#else
# define LOG_MSG(...)
#endif
~~~
在編譯時如果定義了DEBUG則將LOG_MSG當做printf使用,而不需要調試,正式發布時則將LOG_MSG()宏定義為空,由于宏是在預編譯階段進行處理的,所以上面的宏相當于從代碼中刪除了。
上面提到了兩種將宏定義為空的定義方式,看上去一樣,實際上只要明白了宏都只是簡單的代碼替換就知道該如何選擇了。
## 3. #line 預處理
~~~
#line 838 "Zend/zend_language_scanner.c"
~~~
[#line](http://www.cppreference.com/wiki/preprocessor/line)預處理用于改變當前的行號(__LINE__)和文件名(__FILE__)。 如上所示代碼,將當前的行號改變為838,文件名Zend/zend_language_scanner.c 它的作用體現在編譯器的編寫中,我們知道編譯器對C 源碼編譯過程中會產生一些中間文件,通過這條指令,可以保證文件名是固定的,不會被這些中間文件代替,有利于進行調試分析。
## 4.PHP中的全局變量宏
在PHP代碼中經常能看到一些類似PG(), EG()之類的**函數**,他們都是PHP中定義的宏,這系列宏主要的作用是解決線程安全所寫的全局變量包裹宏,如$PHP_SRC/main/php_globals.h文件中就包含了很多這類的宏。例如PG這個PHP的核心全局變量的宏。如下所示代碼為其定義。
#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
如上,ZTS是線程安全的標記,這個在以后的章節會詳細介紹,這里就不再說明。下面簡單說說,PHP運行時的一些全局參數,這個全局變量為如下的一個結構體,各字段的意義如字段后的注釋:
struct _php_core_globals {
zend_bool magic_quotes_gpc; // 是否對輸入的GET/POST/Cookie數據使用自動字符串轉義。
zend_bool magic_quotes_runtime; //是否對運行時從外部資源產生的數據使用自動字符串轉義
zend_bool magic_quotes_sybase; // 是否采用Sybase形式的自動字符串轉義
?
zend_bool safe_mode; // 是否啟用安全模式
?
zend_bool allow_call_time_pass_reference; //是否強迫在函數調用時按引用傳遞參數
zend_bool implicit_flush; //是否要求PHP輸出層在每個輸出塊之后自動刷新數據
?
long output_buffering; //輸出緩沖區大小(字節)
?
char *safe_mode_include_dir; //在安全模式下,該組目錄和其子目錄下的文件被包含時,將跳過UID/GID檢查。
zend_bool safe_mode_gid; //在安全模式下,默認在訪問文件時會做UID比較檢查
zend_bool sql_safe_mode;
zend_bool enable_dl; //是否允許使用dl()函數。dl()函數僅在將PHP作為apache模塊安裝時才有效。
?
char *output_handler; // 將所有腳本的輸出重定向到一個輸出處理函數。
?
char *unserialize_callback_func; // 如果解序列化處理器需要實例化一個未定義的類,這里指定的回調函數將以該未定義類的名字作為參數被unserialize()調用,
long serialize_precision; //將浮點型和雙精度型數據序列化存儲時的精度(有效位數)。
?
char *safe_mode_exec_dir; //在安全模式下,只有該目錄下的可執行程序才允許被執行系統程序的函數執行。
?
long memory_limit; //一個腳本所能夠申請到的最大內存字節數(可以使用K和M作為單位)。
long max_input_time; // 每個腳本解析輸入數據(POST, GET, upload)的最大允許時間(秒)。
?
zend_bool track_errors; //是否在變量$php_errormsg中保存最近一個錯誤或警告消息。
zend_bool display_errors; //是否將錯誤信息作為輸出的一部分顯示。
zend_bool display_startup_errors; //是否顯示PHP啟動時的錯誤。
zend_bool log_errors; // 是否在日志文件里記錄錯誤,具體在哪里記錄取決于error_log指令
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; //PHP的”根目錄”。
char *user_dir; //告訴php在使用 /~username 打開腳本時到哪個目錄下去找
char *include_path; //指定一組目錄用于require(), include(), fopen_with_path()函數尋找文件。
char *open_basedir; // 將PHP允許操作的所有文件(包括文件自身)都限制在此組目錄列表下。
char *extension_dir; //存放擴展庫(模塊)的目錄,也就是PHP用來尋找動態擴展模塊的目錄。
?
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; //PHP所產生的URL中用來分隔參數的分隔符。
?
char *variables_order; // PHP注冊 Environment, GET, POST, Cookie, Server 變量的順序。
?
HashTable rfc1867_protected_variables; // RFC1867保護的變量名,在main/rfc1867.c文件中有用到此變量
?
short connection_status; // 連接狀態,有三個狀態,正常,中斷,超時
short ignore_user_abort; // 是否即使在用戶中止請求后也堅持完成整個請求。
?
unsigned char header_is_being_sent; // 是否頭信息正在發送
?
zend_llist tick_functions; // 僅在main目錄下的php_ticks.c文件中有用到,此處定義的函數在register_tick_function等函數中有用到。
?
zval *http_globals[6]; // 存放GET、POST、SERVER等信息
?
zend_bool expose_php; // 是否展示php的信息
?
zend_bool register_globals; // 是否將 E, G, P, C, S 變量注冊為全局變量。
zend_bool register_long_arrays; // 是否啟用舊式的長式數組(HTTP_*_VARS)。
zend_bool register_argc_argv; // 是否聲明$argv和$argc全局變量(包含用GET方法的信息)。
zend_bool auto_globals_jit; // 是否僅在使用到$_SERVER和$_ENV變量時才創建(而不是在腳本一啟動時就自動創建)。
?
zend_bool y2k_compliance; //是否強制打開2000年適應(可能在非Y2K適應的瀏覽器中導致問題)。
?
char *docref_root; // 如果打開了html_errors指令,PHP將會在出錯信息上顯示超連接,
char *docref_ext; //指定文件的擴展名(必須含有’.')。
?
zend_bool html_errors; //是否在出錯信息中使用HTML標記。
zend_bool xmlrpc_errors;
?
long xmlrpc_error_number;
?
zend_bool activated_auto_globals[8];
?
zend_bool modules_activated; // 是否已經激活模塊
zend_bool file_uploads; //是否允許HTTP文件上傳。
zend_bool during_request_startup; //是否在請求初始化過程中
zend_bool allow_url_fopen; //是否允許打開遠程文件
zend_bool always_populate_raw_post_data; //是否總是生成$HTTP_RAW_POST_DATA變量(原始POST數據)。
zend_bool report_zend_debug; // 是否打開zend debug,僅在main/main.c文件中有使用。
?
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; //是否允許include/require遠程文件。
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; // 用戶的ini文件名
long user_ini_cache_ttl; // ini緩存過期限制
?
char *request_order; // 優先級比variables_order高,在request變量生成時用到,個人覺得是歷史遺留問題
?
zend_bool mail_x_header; // 僅在ext/standard/mail.c文件中使用,
char *mail_log;
?
zend_bool in_error_log;
};
上面的字段很大一部分是與php.ini文件中的配置項對應的。 在PHP啟動并讀取php.ini文件時就會對這些字段進行賦值,而用戶空間的ini_get()及ini_set()函數操作的一些配置也是對這個全局變量進行操作的。
在PHP代碼的其他地方也存在很多類似的宏,這些宏和PG宏一樣,都是為了將線程安全進行封裝,同時通過約定的 **G** 命名來表明這是全局的,一般都是個縮寫,因為這些全局變量在代碼的各處都會使用到,這也算是減少了鍵盤輸入。我們都應該[盡可能的**懶**](http://blogoscoped.com/archive/2005-08-24-n14.html)不是么?
如果你閱讀過一些PHP擴展話應該也見過類似的宏,這也算是一種代碼規范,在編寫擴展時全局變量最好也使用這種方式命名和包裹,因為我們不能對用戶的PHP編譯條件做任何假設。
- 第一章 準備工作和背景知識
- 第一節 環境搭建
- 第二節 源碼結構、閱讀代碼方法
- 第三節 常用代碼
- 第四節 小結
- 第二章 用戶代碼的執行
- 第一節 生命周期和Zend引擎
- 第二節 SAPI概述
- Apache模塊
- 嵌入式
- FastCGI
- 第三節 PHP腳本的執行
- 詞法分析和語法分析
- opcode
- opcode處理函數查找
- 第四節 小結
- 第三章 變量及數據類型
- 第一節 變量的結構和類型
- 哈希表(HashTable)
- PHP的哈希表實現
- 鏈表簡介
- 第二節 常量
- 第三節 預定義變量
- 第四節 靜態變量
- 第五節 類型提示的實現
- 第六節 變量的生命周期
- 變量的賦值和銷毀
- 變量的作用域
- global語句
- 第七節 數據類型轉換
- 第八節 小結
- 第四章 函數的實現
- 第一節 函數的內部結構
- 函數的內部結構
- 函數間的轉換
- 第二節 函數的定義,傳參及返回值
- 函數的定義
- 函數的參數
- 函數的返回值
- 第三節 函數的調用和執行
- 第四節 匿名函數及閉包
- 第五節 小結
- 第五章 類和面向對象
- 第一節 類的結構和實現
- 第二節 類的成員變量及方法
- 第三節 訪問控制的實現
- 第四節 類的繼承,多態及抽象類
- 第五節 魔術方法,延遲綁定及靜態成員
- 第六節 PHP保留類及特殊類
- 第七節 對象
- 第八節 命名空間
- 第九節 標準類
- 第十節 小結
- 第六章 內存管理
- 第一節 內存管理概述
- 第二節 PHP中的內存管理
- 第三節 內存使用:申請和銷毀
- 第四節 垃圾回收
- 新的垃圾回收
- 第五節 內存管理中的緩存
- 第六節 寫時復制(Copy On Write)
- 第七節 內存泄漏
- 第八節 小結
- 第七章 Zend虛擬機
- 第一節 Zend虛擬機概述
- 第二節 語法的實現
- 詞法解析
- 語法分析
- 實現自己的語法
- 第三節 中間代碼的執行
- 第四節 PHP代碼的加密解密
- 第五節 小結
- 第八章 線程安全
- 第二節 線程,進程和并發
- 第三節 PHP中的線程安全
- 第九章 錯誤和異常處理
- 第十章 輸出緩沖
- 第十六章 PHP語言特性的實現
- 第一節 循環語句
- foreach的實現
- 第二十章 怎么樣系列(how to)
- 附錄
- 附錄A PHP及Zend API
- 附錄B PHP的歷史
- 附錄C VLD擴展使用指南
- 附錄D 怎樣為PHP貢獻
- 附錄E phpt測試文件說明
- 附錄F PHP5.4新功能升級解析
- 附錄G:re2c中文手冊