如何給 PHP 新增新特性

笨小孩發表於2019-03-29

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

原文連結

正文

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

我提前為這篇過長的文章道歉。

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

我將使用你可能從其他語言獲知的 in 運算子作為一個例子。它表現如下:

$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。你可以嘗試以下操作:

$ 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 操作符只是一個示例。我並不打算提議包含這個特性 ;)

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

相關文章