如何給 PHP 添加新特性

譯者注: 文中的操做都是基於 PHP5.6 進行的修改,翻譯這篇文章的時候 PHP7 都已經出了,有不少方法已經被遺棄,但願各位注意不要踩坑。php

原文連接html

正文

最近有好多人問我怎麼給 PHP 添加新語法特性。我仔細想了想,確實沒有這方面的教程,接下來我會闡述整個流程。同時這篇文章也是對 Zend 引擎的一個簡介。node

我提早爲這篇過長的文章道歉。git

這篇文章假設你已經掌握了一些 C 的基本知識,而且瞭解 PHP 的一些基本概念(像 zvals 結構體)。若是你不具有這些條件,建議先去了解一下。github

我將使用你可能從其餘語言獲知的 in 運算符做爲一個例子。它表現以下:api

$words = ['hello', 'world', 'foo', 'bar'];
var_dump('hello' in $words); // true
var_dump('foo' in $words);   // true
var_dump('blub' in $words);  // false

$string = 'PHP is fun!';
var_dump('PHP' in $string);    // true
var_dump('Python' in $string); // false
複製代碼

基本上來講,in 操做符和 in_array 函數在數組中的使用同樣(可是沒有 needle/haystack 問題),和字符函數 false != strpos($str2, $str1) 也相似。數組

準備工做

在開始以前,你必須檢出並編譯 PHP。因此接下來咱們須要安裝一些工具。大部分可能都預先在系統上安裝好了,可是你必須使用本身選擇的包管理工具安裝 "re2c" 和 「bison」。若是你用的是 Ubuntu:緩存

$ sudo apt-get install re2c
$ sudo apt-get install bison
複製代碼

接下來,從 git 上克隆 php-src 並進行編譯:安全

// 獲取源碼
$ git clone http://git.php.net/repository/php-src.git
$ cd php-src
// 建立新分支
$ git checkout -b addInOperator
// 構建 ./configure (預編譯)腳本
$ ./buildconf
// 使用 debug 模式和 線程安全模式 預編譯
$ ./configure --disable-all --enable-debug --enable-maintainer-zts
// 編譯 (4 是你擁有的核心數)
$ make -j4
複製代碼

PHP 二進制包應該在 sapi/cli/php。你能夠嘗試如下操做:bash

$ sapi/cli/php -v
$ sapi/cli/php -r 'echo "Hallo World!";'
複製代碼

如今你可能已經有了一個編譯過的 PHP,接下來咱們看下 PHP 在運行一個腳本的時候都作了哪些事。

PHP 腳本的生命週期

運行一個 PHP 腳本有三個主要階段:

  1. Tokenization(符號化)
  2. Parsing & Compilation(解析和編譯)
  3. Execute(運行)

接下來我會詳細解釋每一個階段都在作什麼,如何實現以及咱們須要修改什麼地方纔能讓 in 操做符運行。

符號化

第一階段 PHP 讀取源代碼,把源碼切分紅更小的 「token」 單元。舉個例子 <?php echo "Hello World!"; 會被拆解成下面的 token:

T_OPEN_TAG (<?php )
T_ECHO (echo)
T_WHITESPACE ( )
T_CONSTANT_ENCAPSED_STRING ("Hello World!")
';'
複製代碼

(譯者注: 這裏是官方的 token表)

如你所見原始代碼被切分紅具備語義的 token。處理過程被稱爲符號化,掃描和詞法解析的實如今 Zend 目錄下的 zend_language_scanner.l 文件。

若是你打開文件向下滾動到差很少 1000 行(譯者注: php 8.0.0 在 1261 行),你會發現大量的 token 定義語句像下面這樣:

<ST_IN_SCRIPTING>"exit" {
    return T_EXIT;
}
複製代碼

上述代碼的意思很明顯是: 若是在源代碼中遇到了 exit ,lexer 應該標記它爲 T_EXIT< 和 > 中間的內容是文本應該被匹配的狀態。

ST_IN_SCRIPTING 是對 PHP 源碼來講是正常狀態。還有一些其餘的狀態像 ST_DOUBLE_QUOTE (在雙引號中間),ST_HEREDOC (在 heredoc 字符串中間),等等。

另外一個能夠在掃描期間作的是指定一個「語義」值(也能夠稱爲"lower case" 或者簡稱"lval")。下面是例子:

<ST_IN_SCRIPTING,ST_VAR_OFFSET>{LABEL} {
    zend_copy_value(zendlval, yytext, yyleng);
    zendlval->type = IS_STRING;
複製代碼

{LABEL} 匹配一個 PHP 標識(能夠被定義爲[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*),代碼返回 token T_STRING。另外它複製 token 的文本到 zendlval。因此若是 lexer 遇到一個標識像 FooBarClass,它將設置 FooBarClass 做爲lval。字符串,數字和變量名稱也同樣。

幸運的是 in 操做符並不須要深層次的 lexer 知識。咱們只須要添加如下代碼段到文件中(與上面的 exit 相似):

<ST_IN_SCRIPTING>"in" {
    return T_IN;
}
複製代碼

(譯者注: 新版已經不是上面的寫法了)

除此以外咱們須要讓引擎知道咱們添加了一個新的 token。打開 zend_language_parser.y 加入下面的行在它的相似代碼中(在定義操做符的代碼段中):

%token T_IN "in (T_IN)"
複製代碼

如今你應該用 make -j4 從新編譯下 PHP (必須在頂級目錄 php-src 中執行,不是 Zend/)。這會產生一個新由 re2c 生成的 lexer 並編譯它。爲了測試咱們的修改是否生效。須要執行如下命令:

$ sapi/cli/php -r 'in'
複製代碼

這將會給出一個解析錯誤:

Parse error: syntax error, unexpected 'in' (T_IN) in Command line code on line 1
複製代碼

咱們須要作的最後一件事就是使用 Tokenizer 擴展 從新生成數據,你須要使用 cd 進入 ext/tokenizer 目錄而且執行 ./tokenizer_data_gen.sh

若是你運行 git diff --stat,你會看見下面的信息:

Zend/zend_language_parser.y       |    1 +
Zend/zend_language_scanner.c      | 1765 +++++++++++++++++++------------------
Zend/zend_language_scanner.l      |    4 +
Zend/zend_language_scanner_defs.h |    2 +-
ext/tokenizer/tokenizer_data.c    |    4 +-
5 files changed, 904 insertions(+), 872 deletions(-)
複製代碼

zend_language_scanner.c 內容的變動是 re2C 從新生成的 lexer。由於它包含了行號信息,每一個對 lexer 的改變都會產生巨大的不一樣。因此不用擔憂;)

解析和編譯

目前爲止源碼已經被分解成有含義的 token,PHP已經能夠識別更大的結構像"this is an if block"或者"you are defining function here"。這個過程被稱爲解析,規則被定義在 zend_language_parser.y 文件中。這只是一個定義文件,真正的解析器仍是由 bison 生成的。

爲了瞭解解析器的定義是如何運行的,咱們來看個例子:

class_statement:
        variable_modifiers { CG(access_type) = Z_LVAL($1.u.constant); } class_variable_declaration ';'
    |   class_constant_declaration ';'
    |   trait_use_statement
    |   method_modifiers function is_reference T_STRING { zend_do_begin_function_declaration(&$2, &$4, 1, $3.op_type, &$1 TSRMLS_CC); } '('
           parameter_list ')' method_body { zend_do_abstract_method(&$4, &$1, &$9 TSRMLS_CC); zend_do_end_function_declaration(&$2 TSRMLS_CC); }
;
複製代碼

咱們把花括號中的內容去掉,剩下的內容以下:

class_statement:
        variable_modifiers class_variable_declaration ';'
    |   class_constant_declaration ';'
    |   trait_use_statement
    |   method_modifiers function is_reference T_STRING '(' parameter_list ')' method_body
;
複製代碼

你能夠這樣解讀:

A class statement is
        a variable declaration (with access modifier)
    or  a class constant declaration
    or  a trait use statement
    or  a method (with method modifier, optional return-by-ref, method name, parameter list and method body)
複製代碼

想知道什麼是「methid modifer」,你須要去看 method_modifier 的定義。這就至關直白了。

爲了讓解析器支持 in,咱們須要把 expr T_IN expr 規則加到 expr_without_variable 裏面:

expr_without_variable:
    ...
    |   expr T_IN expr
    ...
;
複製代碼

若是你運行 make -j4,bison 會嘗試從新構建解析器,可是會報如下的錯誤:

conflicts: 87 shift/reduce
/some/path/php-src/Zend/zend_language_parser.y: expected 3 shift/reduce conflicts
make: *** [/some/path/php-src/Zend/zend_language_parser.c] Error 1
複製代碼

shift/reduce 意思是解析器在某些狀況下不知道怎麼去作。PHP 語法有 3 個 shift/reduce 自相矛盾的衝突(意料之中,由於相似 elseif/else 的歧義)。其他的 84 個衝突是由於新規則形成的。 

緣由是咱們沒有規定 in 如何在其餘運算符之間運行。舉個例子:

// if you write
$foo in $bar && $someOtherCond
// should PHP interpret this as
($foo in $bar) && $someOtherCond
// or as
$foo in ($bar && $someOtherCond)
複製代碼

上述被成爲」運算符的優先級「。還有一個相關的概念是」運算符的關聯性「,它決定了你寫$foo in $bar in $baz時會發生什麼。

爲了解決 shift/reduce 的衝突,你須要在解析器的開始處找到下面的行並把 T_IN 追加在這行後面

%nonassoc '<' T_IS_SMALLER_OR_EQUAL '>' T_IS_GREATER_OR_EQUAL
複製代碼

這意味着 in 和 < 比較運算符有相同的優先級,並且沒有關聯性。下面是 in 如何運行的一些示例:

$foo in $bar && $someOtherCond
// 被解釋爲
($foo in $bar) && $someOtherCond
// because `&&` has lower precedence than `in`

$foo in ['abc', 'def'] + ['ghi', 'jkl']
// 被解釋爲
$foo in (['abc', 'def'] + ['ghi', 'jkl'])
// 由於 `+` 的優先級比 `in`&emsp;高

$foo in $bar in $baz
// 會拋出解析異常,由於 `in` 是無關聯性的
複製代碼

若是運行 make -j4,會發現報錯沒了。而後你能夠嘗試運行 sapi/cli/php -r '"foo" in "bar";'。這什麼也不會作,除了打印除一個內存泄漏信息:

[Thu Jul 26 22:33:14 2012]  Script:  '-'
Zend/zend_language_scanner.l(876) :  Freeing 0xB777E7AC (4 bytes), script=-
=== Total 1 memory leaks detected ===
複製代碼

預料之中,由於到目前爲止咱們尚未告訴解析器匹配到 in 的時候該怎麼作。這就是花括號裏的內容的做用(譯者注: 還記得上面講解析器定義的時候簡化的花括號嗎),接下來咱們用下面的內容替換掉 expr T_IN expr:

expr T_IN expr { zend_do_binary_op(ZEND_IN, &$$, &$1, &$3 TSRMLS_CC); }
複製代碼

花括號裏的內容被成爲語義動做,在解析器匹配到固定規則的時候運行。$$$1 和 $3 這些看起來奇奇怪怪的東西是節點。$1 關聯第一個 expr$3 關聯第二個 expr($3 是規則裏的第三個元素),$$ 是存儲結果的節點。

zend_do_binary_op 是一個編譯器指令。它告訴編譯器發行 ZEND_IN 操做指令,指令將會把 $1 和 $3 做爲操做數,將計算結果存入 $$ 中。

編譯指令在 zend_compole.c 中定義(裏面帶有 zend_compile.h 頭文件)。 zend_do_binary_op 定義以下:

void zend_do_binary_op(zend_uchar op, znode *result, const znode *op1, const znode *op2 TSRMLS_DC)
{
    zend_op *opline = get_next_op(CG(active_op_array) TSRMLS_CC);

    opline->opcode = op;
    opline->result_type = IS_TMP_VAR;
    opline->result.var = get_temporary_variable(CG(active_op_array));
    SET_NODE(opline->op1, op1);
    SET_NODE(opline->op2, op2);
    GET_NODE(result, opline->result);
}

複製代碼

代碼應該比較好理解,下節咱們會把它放到一個有上下文的環境中。最後提醒一件事,在大多數狀況下當你想要添加本身的語法的時候,你必須添加本身的 _do_* 方法。添加一個二進制操做符是爲數很少的狀況中的一個。若是你必需要加一個新的 _do_* 函數,先看看現存的函數能不能知足你的需求。它們中的大部分都挺簡單的。

執行

在上節我提到了編譯器在發行操做碼。接下來咱們近距離看下這些操做碼(看 zend_compile.h):

struct _zend_op {
    opcode_handler_t handler;
    znode_op op1;
    znode_op op2;
    znode_op result;
    ulong extended_value;
    uint lineno;
    zend_uchar opcode;
    zend_uchar op1_type;
    zend_uchar op2_type;
    zend_uchar result_type;
};
複製代碼

對上述結構一個簡短的介紹:

  • opcode: 這是一個真正被執行的操做。能夠用 ZEND_ADD 或者 ZEND_SUB 當例子。

  • op1op2, result: 每一個操做最多能夠擁有兩個操做數(它能夠只選擇其中用一個或者一會也不用)和一個結果節點。op1_typeop2_typeresult_type 決定了節點的類型。稍後咱們會去了解節點和節點的類型。

  • extended_value: 擴展值用來存儲標記和一些別的整型值。好比說變量獲取指定用它存儲變量的類型(像 ZEND_FETCH_LOCAL 或者 ZEND_FETCH_GLOBAL)

  • handler: 用來優化操做碼的執行,它存儲處理函數與操做碼和操做數類型相關。 這是自動肯定的,所以沒必要在編譯代碼中設置。

  • lineno: 這就很少說了..

這裏有五種基本的類型能夠詳細解釋 *_type 屬性:

  • IS_TMP_VAR: 臨時變量,一般用在一些表達式的結果像 $foor + $bar上。臨時變量不能共享,因此不能使用引用計數。它們的生命週期很短,因此在使用完成後立刻被銷燬。臨時變量一般被寫成 ~n~0 表示第一個臨時變量,~1 表示第二個,以此類推。

  • IS_CV: 編譯變量。用來存儲哈希表查詢結果,PHP 緩存簡單變量的位置像 $foo 在數組中的地址(C 數組)。此外,編譯變量容許 PHP 徹底優化哈希表。編譯變量使用 !n 表示(n 表示編譯變量數組的偏移量)

  • IS_VAR: 只是一些簡單的變量能夠被轉換爲編譯變量。 全部其餘類型的變量訪問,如 $foo['bar']$foo->bar 返回一個 IS_VAR 變量。它基本上就是一個正常的 zval (有引用計數和其餘的全部屬性)。Vars 這樣 $n 表示。

  • IS_CONST: 常量在代碼中的表示比較隨意。舉個例子,"foo" 或者 3.141 都是 IS_CONST 類型。常量容許更近一步的優化,像複用 zvals,預先計算哈希值。

  • id_UNUSED: 操做數沒有被使用。

與此相關的 znode_op 的結構:

typedef union _znode_op {
    zend_uint      constant;
    zend_uint      var;
    zend_uint      num;
    zend_ulong     hash;
    zend_uint      opline_num;
    zend_op       *jmp_addr;
    zval          *zv;
    zend_literal  *literal;
    void          *ptr;
} znode_op;
複製代碼

咱們能夠看到節點就是一個聯合體。它能夠包含上述元素中的一個(只有一個),具體哪一個取決於上下文。好比 zv 用來存儲 IS_CONST zvals,var 用來存儲 IS_CVIS_VARIS_TMP_VAR 變量。剩下的使用在不一樣的特殊環境下。例如 jmp_addrJMP* 指令結合使用(在循環和條件判斷中使用)。其他都只在編譯期間使用,不是在執行期間(像 constant)。

如今咱們瞭解了單個操做碼的結構,惟一剩下的問題就是這些操做碼存在什麼地方: PHP 爲每一個函數(和文件)建立一個 zend_op_array,裏面存儲了操做碼和不少其餘的信息。我不想深刻去講每一個部分都是幹什麼的,你只須要了解這個結構體存在就好了。

接下來咱們回到 in 操做符的實現!咱們已經指示編譯器去發行一個 ZEND_IN 操做碼。如今咱們須要定義這個操做碼能夠幹什麼。

這部分在 zend_vm_def.h 中實現。若是你看過這個文件,你會發現裏面全是下面這樣的定義:

ZEND_VM_HANDLER(1, ZEND_ADD, CONST|TMP|VAR|CV, CONST|TMP|VAR|CV)
{
    USE_OPLINE
    zend_free_op free_op1, free_op2;

    SAVE_OPLINE();
    fast_add_function(&EX_T(opline->result.var).tmp_var,
        GET_OP1_ZVAL_PTR(BP_VAR_R),
        GET_OP2_ZVAL_PTR(BP_VAR_R) TSRMLS_CC);
    FREE_OP1();
    FREE_OP2();
    CHECK_EXCEPTION();
    ZEND_VM_NEXT_OPCODE();
}
複製代碼

ZEND_IN 操做碼的定義和這個基本同樣,因此咱們來了解下這個定在在幹什麼。我會逐行解釋:

// 頭部定義個四個事情:
//   1. 這是一個 ID 爲 1 的操做碼
//   2. 這個操做碼叫 ZEND_ADD
//   3. 這個操做碼接受 CONST, TMP, VAR 和 CV 做爲第一個操做數
//   4. 這個操做碼接受 CONST, TMP, VAR 和 CV 做爲第二個操做數
ZEND_VM_HANDLER(1, ZEND_ADD, CONST|TMP|VAR|CV, CONST|TMP|VAR|CV)
{
    // USE_OPLINE 意味着咱們想像 `opline` 同樣操做 zend_op.
    // 這個對全部存取操做數或者設置返回值的操做碼都是必須的
    USE_OPLINE
    // For every operand that is accessed a free_op* variable has to be defined.
    // 這個用來判斷操做數是否須要釋放.
    zend_free_op free_op1, free_op2;

    // SAVE_OPLINE() 加載 zend_op 到 `opline`。
    // USE_OPLINE 只是聲明。
    SAVE_OPLINE();
    // 調用 fast add 函數
    fast_add_function(
        // 告訴函數把結果放在 tmp_var 裏
        // EX_T 使用 ID opline->result.var 來操做臨時變量
        &EX_T(opline->result.var).tmp_var,
        // 以讀取模式獲取第一個操做數 ( R 在 BP_VAR_R 的含義是讀取,read 的縮寫)
        GET_OP1_ZVAL_PTR(BP_VAR_R),
        // 以讀取模式獲取第二個操做數
        GET_OP2_ZVAL_PTR(BP_VAR_R) TSRMLS_CC);
    // 釋放兩個操做數 (必須的狀況下)
    FREE_OP1();
    FREE_OP2();
    // 檢查異常。異常可能發生在任何地方,因此必須在全部操做碼中檢查異常。
    // 若是有疑問,加上異常檢測。
    CHECK_EXCEPTION();
    // 處理下一個操做碼
    ZEND_VM_NEXT_OPCODE();
}
複製代碼

你可能會注意到這個文件裏的東西大部分都是 大寫 的。由於 zend_vm_def.h 只是一個定義文件。真正的 ZEND VM 根據它生成,最終存儲在 zend_vm_execute.h(巨...大的一個文件)。PHP 有三個不一樣的虛擬機類型,CALL(默認) GOTO SWITCH。由於他們有不一樣的實現細節,定義文件使用了大量的僞宏(像 USE_OPLINE ),它們最終會被具體實現替代掉。

此外,生成的 VM 爲全部可能的操做數類型的組合建立專門的實現。因此最後不會只有一個 ZEND_ADD 函數,會有不一樣的函數實現,像 ZEND_ADD_CONST_CONSTZEND_ADD_CONST_TMPZEND_ADD_CONST_VAR

如今爲了實現 ZEND_IN 操做碼,你應該在 zend_vm_def.h 文件結尾處新增一個操做碼定義框架:

// 159 是我這裏下個沒有被使用的操做碼編號。 或許你須要選擇一個更大的數字。
ZEND_VM_HANDLER(159, ZEND_IN, CONST|TMP|VAR|CV, CONST|TMP|VAR|CV)
{
    USE_OPLINE
    zend_free_op free_op1, free_op2;
    zval *op1, *op2;

    SAVE_OPLINE();
    op1 = GET_OP1_ZVAL_PTR(BP_VAR_R);
    op2 = GET_OP2_ZVAL_PTR(BP_VAR_R);

    /* TODO */

    FREE_OP1();
    FREE_OP2();
    CHECK_EXCEPTION();
    ZEND_VM_NEXT_OPCODE();
}
複製代碼

上面的代碼只會獲取操做數而後丟棄。

爲了生成一個新的 VM ,你須要在 Zend/ 目錄內運行 php_zend_vm_gen.php。(若是它給了你一堆 /e modifier being deprecated 警告,忽略掉就好了)。運行完之後,去頂級目錄運行 make -j4 從新編譯。

終於,咱們能實現真正的邏輯了。咱們開始寫字符串類型的狀況吧:

if (Z_TYPE_P(op2) == IS_STRING) {
    zval op1_copy;
    int use_copy;

    // 把要 needle(要找的數據) 轉換爲 string 
    zend_make_printable_zval(op1, &op1_copy, &use_copy);

    if (Z_STRLEN_P(op1) == 0) {
        /* 空的 needle 直接返回 true */
        ZVAL_TRUE(&EX_T(opline->result.var).tmp_var);
    } else {
        char *found = zend_memnstr(
            Z_STRVAL_P(op2),                  /* haystack */
            Z_STRVAL_P(op1),                  /* needle */
            Z_STRLEN_P(op1),                  /* needle length */
            Z_STRVAL_P(op2) + Z_STRLEN_P(op2) /* haystack end ptr */
        );

        ZVAL_BOOL(&EX_T(opline->result.var).tmp_var, found != NULL);
    }

    /* Free copy */
    if (use_copy) {
        zval_dtor(&op1_copy);
    }
}
複製代碼

最難的部分是把 needle 轉換成字符串,這裏使用了 zend_make_printable_zval。這個函數也許會建立一個新的 zval。這就是咱們傳 op1_copyuse_copy 的緣由。 若是函數複製了值,咱們只需將它放入op1變量中(因此咱們沒必要處處處理兩個不一樣的變量)。此外,必須在最後釋放複製的值(最後三行的內容)。

若是你添加了上面的代碼到/* TODO */所在的位置,再運行 zend_vm_gen.php 而後從新編譯 make -j4,你已經完成了 in 操做符一半的工做:

$ sapi/cli/php -r 'var_dump("foo" in "bar");'
bool(false)
$ sapi/cli/php -r 'var_dump("foo" in "foobar");'
bool(true)
$ sapi/cli/php -r 'var_dump("foo" in "hallo foo world");'
bool(true)
$ sapi/cli/php -r 'var_dump(2 in "123");'
bool(true)
$ sapi/cli/php -r 'var_dump(5 in "123");'
bool(false)
$ sapi/cli/php -r 'var_dump("" in "test");'
bool(true)
複製代碼

接下來咱們進行實現數組的部分:

else if (Z_TYPE_P(op2) == IS_ARRAY) {
    HashPosition pos;
    zval **value;

    /* Start under the assumption that the value isn't contained */ ZVAL_FALSE(&EX_T(opline->result.var).tmp_var); /* Iterate through the array */ zend_hash_internal_pointer_reset_ex(Z_ARRVAL_P(op2), &pos); while (zend_hash_get_current_data_ex(Z_ARRVAL_P(op2), (void **) &value, &pos) == SUCCESS) { zval result; /* Compare values using == */ if (is_equal_function(&result, op1, *value TSRMLS_CC) == SUCCESS && Z_LVAL(result)) { ZVAL_TRUE(&EX_T(opline->result.var).tmp_var); break; } zend_hash_move_forward_ex(Z_ARRVAL_P(op2), &pos); } } 複製代碼

這裏咱們簡單的遍歷了 haystack 中的每一個值,並檢查是否和 needle 相等。咱們在這裏使用 == 對比,要使用 == 對比的話,必須使用 is_identical_function 代替 is_equal_function

再次運行完 zend_vm_gen.phpmake -j4 後,in 操做符號就支持數組類型的操做了:

$ sapi/cli/php -r 'var_dump("test" in []);'
bool(false)
$ sapi/cli/php -r 'var_dump("test" in ["foo", "bar"]);'
bool(false)
$ sapi/cli/php -r 'var_dump("test" in ["foo", "test", "bar"]);'
bool(true)
$ sapi/cli/php -r 'var_dump(0 in ["foo"]);'
bool(true) // because we're comparing using == 複製代碼

最後一件須要考慮的事情是,若是第二個參數既不是數組又不是字符串咱們該如何處理。這裏我選擇最簡單的辦法: 拋出一個警告並返回 false:

else {
    zend_error(E_WARNING, "Right operand of in has to be either string or array");
    ZVAL_FALSE(&EX_T(opline->result.var).tmp_var);
}
複製代碼

從新生成 VM,再編譯後:

$ sapi/cli/php -r 'var_dump("foo" in new stdClass);'

Warning: Right operand of in has to be either string or array in Command line code on line 1
bool(false)
複製代碼

終篇想法

我但願這篇文章能夠幫你理解如何給 PHP 添加新特性,理解 Zend 引擎 如何運行 php 腳本。儘管這篇文章很長,可是我只覆蓋到了整個系統的一小部分。當你想對 ZE 作出一些修改的時候,工做量最大的部分就是閱讀已經存在的代碼。交叉引用工具在閱讀代碼的時候會提供很大幫助。除此之外,也能夠在 efnet 的 #php.pecl 房間問問題。

當你添加完你想加的特性後,下一步就是把它放到內部郵件列表。人們會查看你加的特性並決定是否應該把它加進項目中。

對了,還有最後一件事: in 操做符只是一個示例。我並不打算提議包含這個特性 ;)

若是你有任何問題或意見,請在下方留言。

相關文章
相關標籤/搜索