如何给 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 操做符只是一个示例。我并不打算提议包含这个特性 ;)

若是你有任何问题或意见,请在下方留言。

相关文章
相关标签/搜索