附录G:re2c中文手册

最后更新于:2022-04-01 20:39:56

在PHP的实现过程中,包括PHP语言本身的词法分析,一共有多达8处的地方使用了re2c,如我们常用的时间函数、pdo扩展等。对re2c的了解更能促进我们进PHP内核实现的认知。 本手册是re2c官网的manual.html文件翻译稿,仅适用于对re2c的初步了解,更多的资料见[re2c项目](http://sourceforge.net/projects/re2c/)中lessons目录和doc目录。 ## Name[]() re2c - 将正则表达式转化成C/C++代码 Synopsisre2c [-bdDefFghisuvVw1] [-o output] [-c [-t header]] file ## Description[]() re2c是一个将正则表达式转化成基于C语言标识的预处理器。 re2c的输入包含C/C++代码,并且以/*!re2c... */注释的格式将扫描标识交错嵌入到这些代码中。在它的输出中,这些注释将会被生成的代码替换掉,当执行时,它将会查找到下一个token,并且执行用户提供的针对该token的特定代码。如下示例: char *scan(char *p) { /*!re2c re2c:define:YYCTYPE = "unsigned char"; re2c:define:YYCURSOR = p; re2c:yyfill:enable = 0; re2c:yych:conversion = 1; re2c:indent:top = 1; [0-9]+ {return p;} [^] {return (char*)0;} */ } re2c将生成如下代码: /* Generated by re2c on Sat Apr 16 11:40:58 1994 */ char *scan(char *p) { { unsigned char yych;   yych = (unsigned char)*p; if(yych <= '/') goto yy4; if(yych >= ':') goto yy4; ++p; yych = (unsigned char)*p; goto yy7; yy3: {return p;} yy4: ++p; yych = (unsigned char)*p; {return char*)0;} yy6: ++p; yych = (unsigned char)*p; yy7: if(yych <= '/') goto yy3; if(yych <= '9') goto yy6; goto yy3; }   } 你可以通过添加注释:/*!max:re2c_/ 来输出一个宏定义 YYMAXFILL 来保存输入解析时字符的最大个数。如果使用了-1, YYMAXFILL 只能在最后的 /*!re2c_/ 后触发一次。同时,你也可以使用 /*!ignore:re2c */ 来为扫描代码添加注释文档,它们被输出。 ## Options[]() re2c提供如下的选项: - -? - -h 帮助 - -b 当指定-b参数时,-s参数也会被默认同时指定。 使用位向量尝试着从编译器捣鼓出更好的代码。它对于关键字比较多的规则很有用,比如大部分的编程语言。 re2c的实现是通过生成256个ascii字符的映射表,直接判断对应的字符串是否应该跳转到下一个字符,从而实现优化。 - -c 支持类lex或flex的表达式 - -d 创建一个解析器用来打印当前位置的信息,这对于调试非常有用。如果你要使用它,你需要定义一个供解析器调用的YYDEBUG宏,它像一个函数一样,接受两个参数:void YYDEBUG(int state,char current)。第一个参数是state或者-1,第二个参数是当前所解析的代码位置。 在每个++YYCURSOR、不同的goto跳转变化处,re2c自动添加YYDEBUG宏调用。如果在规则文件中没定义YYDEUBG宏,在编译C文件时会出错。 - -D 输出Graphviz dot 格式的数据,比如可以使用" dot -Tpng input.dot > output.png"来处理生成图片。注意扫描器中如果包含太多的状态可能会让dot程序崩溃 - -e 从ASCII平台交叉编译EBCDIC - -f 生成带可存储状态的扫描器。更多详情见下面的可存储的扫描器小节。 - -F 部分支持flex语法。当-F标记有效时,flex的变量用大括号括起来,并且在定义时不需要等号,在结束时不需要用分号。否则,名字被认为是直接被引号的字符串。 - -g 使用GCC的goto特性生成扫描器。当决策复杂时re2c会生成决策跳转表,使用goto针对不同的情况做不同的跳转。仅适用于GCC编译器。注意,这里默认指定了-b参数。re2c的实现中,-g参数会生成yytarget决策跳转表,其实就是一个256个元素的一维数据,针对不同的字符,直接跳转,以优化扫描器。 - -i 不输出行信息,当你的用户从你的代码编译,而你又不要求他们拥有re2c环境,此时你可以使用CMS工具管理re2c的输出文件时,此参数就有用武之地了。-o参数指定输出文件。指在生成的.c文件中不使用#line宏。 - -r 允许扫描器在每个 '/_!use:re2c'块后面重用定义的 '/_!use:re2c' 块。这些块可以包含适当的配置,特别是 're2c:flags:w'和re2c:flags:u'。这种方法可能会为不同的字符类型,不同的输入机制或不同的输出机制多次创建相同的扫描器。'/_!use:re2c' 块也可以在 '/_!rules:re2c'中的规则集中包含额外的规则。 - -s 为一些switch语句生成嵌套的if语句。许多编译器需要这个参数的辅助以便生成更好的代码。 - -t 生成一个类型定义的头文件,以支持类(f)lex条件,当需要使用-t参数时,需同时指定-c参数,-t参数后面接生成的头文件名称。如果只指定re2c会报错:re2c: error: Can only output a header file when using -c switch - -u 生成一个支持Unicode编码的解析器。这意味着生成的代码能处理任何有效的Unicode字符,直到x10FFFF。当需要支持UTF-8或UTF-16时,你需要自己将输入的数据转化成UTF-32编码。 - -v 查看版本信息。如:re2c 0.13.6 - -V 以数字格式查看版本信息。如:001306 - -w 创建支持宽字符格式的解析器,默认指定-s参数,不能和-e参数共存。 - -1 强制一次生成,它不能和-f组合在一起使用,并且在re2c块结束之前不能禁用YYMAXFILL。 - --no-generation-date 禁止输出生成日志,所以只会输出re2c的版本信息。 - --case-insensitive 所有字符串不区分大小写,所以,双引号中的字符和单引号的意义一样。 - --case-inverted 颠倒单引号和双引号包含的字符中的意思,比如,有了这个开关,单引号内的字符串区分大小写,双引号内的字符串不区分大小写。 - ## Interface Code(接口代码)[]() 不像其他的扫描器程序,re2c 不会生成完整的扫描器:用户必须提供一些接口代码。用户必须定义下面的宏或者是其他相应的配置。 - YYCONDTYPE 用-c 模式你可以使用-t参数来生成一个包含了会被作为条件使用的枚举类型的文件。枚举类型中的每个值都会在规则集合里面作为条件来使用。 - YYCTYPE 用来维持一个输入符号。通常是 char 或者unsigned char。 - YYCTXMARKER *YYCTYPE类型的表达式,生成的代码回溯信息的上下文会保存在 - YYCTXMARKER。如果扫描器规则需要使用上下文中的一个或多个正则表达式,则用户需要定义这个宏。 - YYCURSOR *YYCTYPE类型的表达式指针指向当前输入的符号,生成的代码作为符号相匹配,在开始的地方,YYCURSOR假定指向当前token的第一个字符。在结束时,YYCURSOR将会指向下一个token的第一个字符。 - YYDEBUG(state,current) 这个只有指定-d标记的时候才会需要。调用用户定义的函数时可以非常容易的调试生成的代码。这个函数应该有以下签名:void YYDEBUG(int state,char current)。第一个参数接受 state ,默认值为-1第二个参数接受输入的当前位置。 - YYFILL(n) 当缓冲器需要填充的时候,生成的代码将会调用YYFILL(n):至少提供n个字符。YYFILL(n)将会根据需要调整YYCURSOR,YYLIMIT,YYMARKER 和 YYCTXMARKER。注意在典型的程序语言当中,n等于最长的关键词的长度加一。用户可以在/*!max:re2c_/一次定义YYMAXFILL来指定最长长度。如果使用了-1,YYMAXFILL将会在/*!re2c_/之后调用一次阻塞。 - YYGETCONDITION() 如果使用了-c模式,这个定义将会在扫描器代码之前获取条件集。这个值必须初始化为枚举YYCONDTYPE的类型。 - YYGETSTATE() 如果指定了-f模式,用户就需要定义这个宏。此种情况下,扫描器在开始时为了获取保存的状态,生成的代码将会调用YYGETSTATE()。YYGETSTATE()必须返回一个有符号的整数,这个值如果是-1,告诉扫描器这是第一次执行,否则这个值等于以前YYSETSTATE(s) 保存的状态。否则,扫描器将会恢复操作之后立即调用YYFILL(n)。 - YYLIMIT 这是一个类型为*YYCTYPE的表达式,它标记了缓冲器的结尾(YYLIMIT[-1]是缓冲区的最后一个字符)。生成的代码将会不断的比较YYCORSUR 和 YYLIMIT 以决定 什么时候填充缓冲区。 - YYSETCONDITION(c) 这个宏用来在转换规则中设置条件,它只有在指定-c模式和使用转换规则时有用。 - YYSETSTATE(s) 用户只需要在指定-f模式时定义这个宏,如果是这样,生成的代码将会在YYFILL(n)之前调用YYSETSTATE(s),YYSETSTATE的参数是一个有符号整型,被称为唯一的标示特定的YYFILL(n)实例。 - YYMARKER 类型为*YYCTYPE的表达式,生成的代码保存回溯信息到YYMARKER。一些简单的扫描器可能用不到。 - 解析器支持条件 当使用-c参数时,你可以使用正则表达式条件列表。这样re2c会为每个条件生成扫描块,在每一个生成的扫描器都有自己的先决条件。先决条件是定义YYGETCONDETION ,而且类型必须是YYCONDTYPE。 - YYSETSTATE(s) 用户只需要在指定-f模式时定义这个宏。在此种情况下,生成的代码将会在YYFILL(n)之前调用YYSETSTATE(s),YYSETSTATE的参数是一个有符号整型,被称为唯一的标示特定的YYFILL(n)实例。如果用户希望保存扫描器的状态并用YYFILL(n) 将状态返回给调用 者,他所需要做的是在变量中保存这个唯一的标识。然后,当再次调用扫描器时,它将调用 - YYGETSTATE()并在恢复到之前离开的地方继续执行。即使禁用了 YYFILL(n) ,生成的代码也会包含YYSETSTATE(s)和YYGETSTATE。 ## Scanner With Storable States可存储状态的扫描器[]() 当指定-f标记时,re2c会生成一个存储了它当前状态的扫描器,它能精确的恢复到之前离开的位置,并返回给调用者。 re2c的默认行为是拉模式,无论何时需要,它都可以要求额外的输入,然而,这种操作模式是基于扫描器可以控制解析循环这一前提的,而这个前提并不一定会存在。通常情况下,如果有一个预处理过程或其它相关的源程序数据在扫描器之前先执行,则扫描器无法再要求更多的数据,除非他们都在独立的线程之中。 -f标记刚好可以解决这个问题:它让用户设计的扫描器以拉模式工作,即数据一块一块的输入到扫描器中。当扫描器运行数据时,它仅存储它的状态,并返回给调用者。当更多的输入数据输入到扫描器时,它能很精确的恢复到之前离开的位置。 当re2c使用-f选项时,它不能接收标准输入,因为它必须做两次完整的全局扫描,而两次扫描就需要读取两次。这就意味着,如果不能打开输入两次或第一次输入影响第二次输入,re2c会执行失败。 相对于拉模式,可存储的扫描器有以下不同: 1. 用户必须提供YYSETSTATE() 宏和YYGETSTATE(state)宏 1. -f参数禁止了yych和yyaccept的声明。因此用户必须声明这些,并且必须能够保存和恢复他们。在example/push.re文件的示例中,这些都被声明为C++类的字段,因此他们不再需要明确的保存或恢复。对于C语言来说,我们可以通过宏,以参数传递的方式从结构体中获取这些字段。或者,可以将他们声明为局部变量,当它决定返回并将之作为函数的一个项保存在 YYFILL(n)中。此外, 使用YYFILL(n)保存的效率更高,因为可以无条件的调用YYSETSTATE(state)。然而,YYFILL(n) 并不能将state作为参数,因此,我们必须通过YYSETSTATE(state)将state保存到局部变量中。 1. 如果需要更多的输入,需要修改YYFILL(n) ,使之可以从调用它的函数处返回。 1. 修改调用者的逻辑,使其在需要更多的输入时做出相应的应答。 1. 生成的代码中将包含一个选择逻辑块,这个选择逻辑会被用来通过跳转到相应的YYFILL(n)调用处,以恢复最后的状态。这个代码块会在第一个 "/*!re2c */"块收尾的地方自动生成。通过放置 "/*!getstate:re2c */"注释,可能会触发YYGETSTATE() 的生成操作。这对于被包含在循环中的扫描器非常有用。 请查看 examples/push.re文件中的推模式示例扫描器。它生成的代码可以通过"state:abort"和"state:nextlabel"调整。Scanner With Condition Support可判断条件的扫描 当使用-c参数时,你可以在正则表达式之前优先一系统的条件名。在这种情况下,re2c会针对每个条件生成扫描代码块。这些代码块都有它自己的前置条件,这此前置条件都是通过接口定义YYGETCONDITON实现,并且必须为YYCONDTYPE类型。 其中有两个特别的类型,一个是‘*’,它表示满足所有条件;另一个是空条件,它提供一个没有扫描内容的代码块,这意味着不需要任何正则表达式。这个特殊的块始终有一个固定的枚举值0。这些特殊的规则可以被用来初始化一个扫描器。这些特殊的规则并不是必须的,但是有时可以用它来声明一些没有初始化的状态。 非空规则允许指定新的条件,这些条件将导致规则的变化。它会生成定义的YYSETCONDTITION,除此之外再无其它。 还有另一种特殊的规则,它允许在所有的有规则和没有规则代码前添加代码。例如,它可以用来保存扫描的字符串的长度。这个特殊的规则以感叹号开始,后面可以接条件 或星号。当re2c为这个规则生成代码时,如果这个规则的状态没有起始规则或已经在在一个星号规则,那么这个代码将作为起始代码。 ## Scanner Specifications 扫描器规则[]() 每个扫描器规格都由 规则集、命名定义和配置构成。 规则由正则以及紧跟其后面的C/C++代码构成,当正则匹配时,其后的C/C++代码会被执行。你可以以大括号或:=开始代码。当用大括号开始代码时,re2c会根据大括号判断其尝试并自动结束代码的查找。如果不使用大括号开始代码,则re2c会在第一行不为空时停止查找。 regular-expression { C/C++ code } regular-expression := C/C++ code 如果指定-c参数,则每个正则前面都会有一系列的由逗号分隔的条件名称。除了正常命名的规则以外,有两种特殊的情况。一个规则可能包含一个单独的条件名称'*'和没有条件名称。对于没有条件名称的情况,其后面不能接正则表达式。非空规则可能会进一步指定新的条件。在这种情况下,re2c可能会自动生成必要的代码来改变条件。如上所示代码,其以大括号和':='开始代码。更进一步,更多的规则可以使用':=>'快捷方式来自动生成代码,它不仅仅可以设置新的状态,还可以继续执行新的状态。一个快捷规则不应该在循环中使用,这些循环代码在循环开始和re2c块之间,除非用 re2c:cond:goto使之 'continue;'如果一段代码必须放在所有的规则之前,你可以使用 regular-expression { C/C++ code } regular-expression := C/C++ code regular-expression => condition { C/C++ code } regular-expression => condition := C/C++ code regular-expression :=> condition <*> regular-expression { C/C++ code } <*> regular-expression := C/C++ code <*> regular-expression => condition { C/C++ code } <*> regular-expression => condition := C/C++ code <*> regular-expression :=> condition <> { C/C++ code } <> := C/C++ code <> => condition { C/C++ code } <> => condition := C/C++ code <> :=> condition { C/C++ code } := C/C++ code { C/C++ code } := C/C++ code 命名定义格式如下: name = regular-expression; 如果使用了-F 模式,可以使用如下命名定义方法: name regular-expression 以"re2c"开始的命名定义配置如下所示: re2c:name = value; re2c:name = "value"; ## Summary Of Re2c Regular-expressionsre2c正则表达式小结[]() - "foo" 字符串foo。可以使用ANSI-C转义序列。 - [xyz] 字符集;此种情况匹配字符x,y或z - [abj-oZ] 包含区间的字符集,此种情况匹配a,b,j到o之间的任一字符,或z - [^class] 字符集否定匹配,匹配没有在方括号中定义的字符。 - r\s 匹配非s的正则,r和s都必须是可以表示为字符集的正则表达式 - r* 零次或多次匹配,r是任一正则表达式 - r+ 一次或多次匹配(至少一次) - r? 零次或一次匹配 - name 这里name就是在前面的定义段给出的名字 - (r) 匹配规则表达式r,圆括号可以提高其优先级。 - rs 匹配规则表达式r,其后紧跟着表达式s。这称为联接(concatenation)。 - r|s 或者匹配规则表达式r,或者匹配表达式s。 - r/s 匹配模式r,但是要求其后紧跟着模式s。s并不会参与文本的匹配。这种正则表达式的匹配称之为“尾部上下文” - r{n} n次匹配 - r{n,} 至少n次匹配 - r{n,m} 至少n次,至多m次匹配;匹配除换行符外的任意字符 - def 当没有使用-F参数时,匹配的命名定义通过def定义。当-F参数指定时,def语名和双引号包含的效果一样,直接匹配def字符串。字符集和字符串可能包含有八进制或十六进制或如下的转义字符 (\n, \t, \v, \b, \r, \f, \a, \)。一个八进制字符由一个反斜杠和紧跟着它的三个八进制数字组成,一个十六进制字符由一个反斜杠,一个小写的x,以及两个十六进制数字组成,或由一个反斜杠,一个大写的X,以及四个十六进制数字组成。 re2c进一步会支持更多的C/C++的unicode符号。这些unicode符号由一个反斜杠+u+四个十六进制数字或一个反斜杠+U+八个十六进制的数字组成。然后,仅当-u模式下才能处理这些uincode字符。 在非unicode模式下,大于\X00FF的字符是无法直接匹配的,除非使用”万金油“类型的 (.|"\n")和[^]正则表达式匹配所有的字符时,包含它们。 如上所示的正则表达式列表按优先级分组,从最上面的最高优先级到最下面的最低优先级。这些组合之间的优先级相同。 ## Inplace Configuration现场配置[]() 它可能在re2c块中配置并生成代码,如下所示为可用的配置项: - re2c:condprefix = yyc_ ;允许指定条件标签的前缀。它将在生成的输出文件中的所有条件标签前添加指定的前缀。 - re2c:condenumprefix = yyc ;允许指定条件值的前缀。它将在生成的输出文件中的所有条件枚举值前添加指定的前缀。 - re2c:cond:divider = "/* *********************************** */" ;允许为条件块自定义分隔符。你可以使用'@@'输出条件的名字或使用 - re2c:cond:divider@cond = @@ ;指定即将被 re2c:cond:divider中的条件名替换的占位符。 - re2c:cond:goto = "goto @@;" ;允许使用 ':=>' 规则自定义条件跳转语句。你可以使用'@@'输出条件的名字或使用re2c:cond:divider@cond自定义占位符,同时你也可以使用此语句继续下一个循环周期,这个循环周期包括循环开始到re2c块之间的任何代码。 - re2c:cond:goto@cond = @@ ;指定即将在 re2c:cond:goto语句中被替换的条件标签占位符 - re2c:indent:top = 0 ;指定最小的缩进,大于或等于0 - re2c:indent:string = "\t" ;指定缩进用的字符串。除非你想使用外部工具,否则就需要只包含空白字符串。最简单的方法就是用单引号或双引号包含它们。如果你不需要任何缩进,直接使用""即可。 - re2c:yych:conversion = 0 ;当此设置非零时,re2c会在读取yych时自动生成转换代码。此时的类型必须使用re2c:define:YYCTYPE定义。 - re2c:yych:emit = 1 ;设置为0可以禁止yych的生成。 - re2c:yybm:hex = 0 ;如果设置为0,则生成一个十进制表格,否则将生成一个十六进制表格 - re2c:yyfill:enable = 1 ;将此设置为0可以禁止YYFILL(n)的生成。当使用它时请确认生成的扫描器在输入之后不再读取。允许此行为将给你的程序引入服务安全问题。 - re2c:yyfill:check = 1 ;当YYLIMIT + max(YYFILL)一直可用时,把此设置为0可以禁止使用YYCURSOR和YYLIMIT的先决条件的输出。 - re2c:yyfill:parameter = 1 ;允许禁止YYFILL调用的参数传递。如果设置为0,将没有任何参数传递到YYFILL。然而,define:YYFILL@LEN允许指定一个字符串替换实际字符中的长度。如果设置为非0,除非设置re2c:define:YYFILL:naked,否则YYFILL将使用紧跟其后的大括号内的所要求的字符数。其它请参照:re2c:define:YYFILL:naked和re2c:define:YYFILL@LEN. - re2c:startlabel = 0 ;如果设置为0的整数,即使没有扫描器本身,下一个扫描块的开始标签也会被生成。否则仅在需要的时候生成常规的yy0开始标签。如果设置为一个文本值,不管常规的开始标签生成是否,包含当前文本的标签都会被生成。在开始标签生成后,当前设置会被重置为0。 - re2c:labelprefix = yy ;允许修改数字标签的前缀,默认为yy,任何有效的标签都是可以的。 - re2c:state:abort = 0 ;当设置为非零,并且开启-f模式时,YYGETSTATE 块会包含一个默认的情况,初始化时设置为-1 - re2c:state:nextlabel = 0 ;当开启-f模式时,使用此设置可以控制是否在YYGETSTATE块后面接yyNext标签行。通常,你可以用startlabel配置强制指定开始标签或用默认的yy0作为开始标签,而不是用yyNext。通常我们通过放置"/*!getstate:re2c */" 注释来分隔实际扫描器的YYGETSTATE 代码,而不是专用的标签。 - re2c:cgoto:threshold = 9 ;当启用-g模式时,这个值指定生成的跳转表的复杂度阈值,而不是使用嵌套的if语句和决策位字段。 - re2c:yych:conversion = 0 ;当输入使用有符号字符时,并且开启-s和-b械时,re2c会自动将其转化为无符号类型。当设置为0时会禁用空字符串转化。设置为非零时,转化将在YYCTYPE处进行。如果这个值通过现场配置,则使用该值。否则,将会变成(YYCTYPE),并且不能再修改成配置。当设置为一个字符串时,必须用括号括起来。现在,假设你的输入为char*并且使用上述的设置,你可以设置YYCTYPE为unsigned char,并且当前值设置为1或者"(unsigned char)" - re2c:define:define:YYCONDTYPE = YYCONDTYPE ;枚举用于支持-c模式的条件 - re2c:define:YYCTXMARKER = YYCTXMARKER ;允许覆盖定义的YYCTXMARKER ,从而避免将其设置为实际所需的代码。 - re2c:define:YYCTYPE = YYCTYPE ;允许覆盖定义的YYCTYPE ,从而避免将其设置为实际所需的代码。 - re2c:define:YYCURSOR = YYCURSOR ;允许覆盖定义的YYCURSOR ,从而避免将其设置为实际所需的代码。 - re2c:define:YYDEBUG = YYDEBUG ;允许覆盖定义的YYDEBUG ,从而避免将其设置为实际所需的代码。 - re2c:define:YYFILL = YYFILL ;允许覆盖定义的YYFILL ,从而避免将其设置为实际所需的代码。 - re2c:define:YYFILL:naked = 0 ;当设置为1时,括号、参数、分号都会被发出。 - re2c:define:YYFILL@len = @@ ;当使用 re2c:define:YYFILL 时,并且re2c:yyfill:parameter 为0时,YYFILL 中的任何文本将会被新的实际的长度值替换。 - re2c:define:YYGETCONDITION = YYGETCONDITION ;允许覆盖定义的YYGETCONDITION - re2c:define:YYGETCONDITION:naked = ;当设置为1时,括号、参数、分号都会被发出。 - re2c:define:YYGETSTATE = YYGETSTATE ;允许覆盖定义的YYGETSTATE ,从而避免将其设置为实际所需的代码。 - re2c:define:YYGETSTATE:naked = 0 ;当设置为1时,括号、参数、分号都会被发出。 - re2c:define:YYLIMIT = YYLIMIT ;允许覆盖定义的YYLIMIT ,从而避免将其设置为实际所需的代码。 - re2c:define:YYMARKER = YYMARKER ;允许覆盖定义YYMARKER,从而避免将其设置为实际所需的代码。 - re2c:define:YYSETCONDITION = YYSETCONDITION ;允许覆盖定义的YYSETCONDITION - re2c:define:YYSETCONDITION@cond = @@ ;当使用 re2c:define:YYSETCONDITION时,YYSETCONDITION中的任何文本将会被新的实际的条件值替换。 - re2c:define:YYSETSTATE = YYSETSTATE ;允许覆盖定义的YYSETSTATE,从而避免将其设置为实际所需的代码。 - re2c:define:YYSETSTATE:naked = 0 ;当设置为1时,括号、参数、分号都会被发出。 - re2c:define:YYSETSTATE@state = @@ ;当使用re2c:define:YYSETSTATE时,YYSETCONDITION中的任何文本将会被新的实际的状态值替换 - re2c:label:yyFillLabel = yyFillLabel ;允许覆盖标签yyFillLabel,即可以自定义生成的yyFillLabel 变量名。 - re2c:label:yyNext = yyNext ;允许覆盖标签yyNext ,即可以自定义生成的yyNext变量名。 - re2c:variable:yyaccept = yyaccept ;允许覆盖变量yyaccept,即可以自定义生成的yyaccept变量名。 - re2c:variable:yybm = yybm ;允许覆盖变量yybm,即可以自定义生成的yybm变量名。 - re2c:variable:yych = yych ;允许覆盖变量yych,即可以自定义生成的yych变量名。 - re2c:variable:yyctable = yyctable ;当指定-c参数和-g参数时,re2c会使用此变量为YYGETCONDITION生成静态跳转表。 - re2c:variable:yystable = yystable ;当指定-f参数和-g参数时,re2c会使用此变量为YYGETSTATE生成静态跳转表。 - re2c:variable:yytarget = yytarget ;允许覆盖变量yytarget,即可以自定义生成的yytarget变量名。 ## Understanding Re2c 理解re2c[]() re2c的子目录中包含各种例子教你一步一步的如何开启re2c的世界,所有的例子都是可编译运行的。 ## Features特点[]() re2c不提供默认的动作:生成的代码假定输入包含一系列token。通常,可以通过添加一条规则实现,例如上面示例中的异常字符 因为re2c不提供结束表达式,所以用户必须安排一个输入结束符并让一个规则匹配并捕获它。如果来源是一个以空字符串结尾的字符串,则匹配一个空字符串就可以了。如果来源是一个文件,你可以在文件后添加一个换行(或其它不会出现的标记);通过识别这个字符,以检查这是否为一个标记点并执行相应的操作。同样,你也可以使用YYFILL(n)判断是否没有足够的字符可用时结束扫描。 BugsDifference only works for character sets.The re2c internal algorithms need documentation.See Alsoflex(1), lex(1).More information on re2c can be found here:http://re2c.org/Authors Peter Bumbulis [peter@csg.uwaterloo.ca](#)Brian Young [bayoung@acm.org](#)Dan Nuffer [nuffer@users.sourceforge.net](#)Marcus Boerger [helly@users.sourceforge.net](#)Hartmut Kaiser [hkaiser@users.sourceforge.net](#)Emmanuel Mogenet [mgix@mgix.com](#) added storable state 英文原地址:http://re2c.org/manual.html译者:胖子(http://www.phppan.com/)友情协助:吴帅(http://www.imsiren.com/)校验:reeze(http://www.reeze.cn)
';

附录F PHP5.4新功能升级解析

最后更新于:2022-04-01 20:39:54

本篇主要从两个角度对PHP5.4的一些更新进行说明,并同时尽可能的解释做出这些变更的具体原因。也就解释What & Why。本片并不会介绍所有的变更,只针对比较大或者对我们的开发有影响的一些特性及变更进行说明。 ### 新特性[]()
';

附录E phpt测试文件说明

最后更新于:2022-04-01 20:39:52

phpt文件用于PHP的自动化测试,这是PHP用自己来测试自己的测试数据用例文件。测试脚本通过执行PHP源码根目录下的run-tests.php,读取phpt文件执行测试。 phpt文件包含 TEST,FILE,EXPECT 等多个段落的文件。在各个段落中,TEST、FILE、EXPECT是基本的段落,每个测试脚本都必须至少包括这三个段落。其中: - TEST段可以用来填写测试用例的名字。 - FILE段是一个 PHP 脚本实现的测试用例。 - EXPECT段则是测试用例的期待值。 在这三个基本段落之外,还有多个段落,如作为用例输入的GET、POST、COOKIE等,此类字段最终会赋值给$env变量。比如,cookie存放在$env['HTTP_COOKIE'],$env变量将作为用例中脚本的执行环境。一些主要段落说明如下表所示: PHP测试脚本中的段落说明 | :段落名 | 填充内容 | 备注 | |-----|-----|-----| | TEST | 测试脚本语句 | 必填段落 | | FILE | 测试脚本语句 | 必填段落。用PHP语言书写的脚本语句。其执行的结果将与 EXPECT* 段的期待结果做对比。 | | ARGS | FILE 段的输入参数 | 选填段落 | | SKIPIF | 跳过这个测试的条件 | 选填段落 | | POST | 传入测试脚本的 POST 变量 | 选填段落。如果使用POST段,建议配合使用SKIPIF段 | | GET | 传入测试脚本的 GET 变量 | 选填段落。如果使用GET段,建议配合使用SKIPIF段。 | | POST_RAW | 传入测试脚本的POST内容的原生值 | 选填段落。比如在做文件上传测试时就需要使用此字段来模拟HTTP的POST请求。 | | COOKIE | 传入测试脚本的COOKIE的值 | 选填段落。最常见的是将PHPSESSID的值传入。 | | INI | 应用于测试脚本的 ini 设置 | 选填段落。例如 foo=bar 。其值可通过函数 ini_get(string name_entry) 获得。 | | ENV | 应用于测试脚本的环境设置 | 选填段落。例如做gzip测试,则需要设置环境HTTP_ACCEPT_ENCODING=gzip。 | | EXPECT | 测试脚本的预期结果 相当于测试文件的结果 | 必填段落 | | EXPECTF | 测试脚本的预期结果 | 选填段落。可用函数 sscanf() 中的格式表达预期结果 EXPECT 段的变体 | | EXPECTREGEX | 测试脚本的正则预期结果 | 选填段落。以正则的方式包含多个预期结果,是预期结果EXPECT段的一种变体。 | | EXPECTHEADERS | 测试脚本的预期头部内容 | 选填段落.测试脚本期待HTTP头部返回,是预期结果EXPECT段的另一种格式。验证过程中会按头部的字段一一比对测试,比如zlib扩展中,如果开启zlib.output_compression, 则在EXPECTHEADERS中包含Content-Encoding: gzip作为预期结果。 | phpt文件只是用例文件,它还需要一个控制器来调用这些文件,以实现整个测试过程。PHP的测试控制器文件是源码根目录下的run-tests.php文件。此文件的作用是根据传入的参数,分析用例相关数据,执行测试过程。其大概过程如下: 1. 分析输入的命令行,根据参数配置相关参数,初始化各种信息。 1. 分析用例输入参数,获取需要执行的用例文件列表。PHP支持指定单文件用例执行,支持多文件用例执行,支持* .phpt多用例执行,支持* .phpt简化版本_多用例执行(相当于_.phpt)。 1. 遍历用例文件列表,执行每一个用例。对于每个用例,PHP会具体解析测试脚本中各个段落的含义,清除所有上次测试的记录与设置将准备此次的测试环境,并把各种中间文件和日志文件准备好,然后用环境变量 TEST_PHP_EXECUTABLE 指定的 PHP 可执行对象运行实际的测试语句。最后将运行后的结果和测试脚本中的预期结果(EXPECT*段)进行比较,如果比较结果一致,则测试通过;如果不一致,则测试失败,最后将结果信息一一记录到用户设置的日志文件中。 1. 生成测试结果。 这仅仅是执行的过程,除此之外,还有若干准备和清理工作,如,对上次测试遗留下的环境的清理,本次测试所必须的环境变量的读取与设置,对测试参数的解析,测试脚本名的解析,各种输出文件的准备等等 以测试脚本/tests/basic/001.phpt为例: --TEST-- Trivial "Hello World" test --FILE-- --EXPECT-- Hello World 这个用例脚本只包含必填的三项。测试控制器会执行--FILE--下面的PHP文件,如果最终的输出是--EXPECT--所期望的结果则表示这个测试通过,如果不一致,则测试不通过,最终这个用例的测试结果会汇总会所有的测试结果集中。
';

附录D 怎样为PHP贡献

最后更新于:2022-04-01 20:39:50

既然你在阅读本书,那说明你也是对PHP很感兴趣的读者,在窥探到PHP内部实现之后或许也蠢蠢欲动想要共享自己的力量。下面进行一些简单的说明。 很多人以为为PHP做贡献(contribute)只是简单的为PHP提交补丁,其实在广义上来说,为PHP做贡献有很多种方式,这包含但不限于: - 宣传和参与PHP的讨论 - 发现和报告或者提到补丁修复PHP的bug - 编写和翻译PHP手册 - 编写PHP相关的书籍 - 写PHP相关技术的博客 - 为PHP增加新功能 - 编写和贡献PHP扩展或者库 所以很可能大部分的读者目前其实已经是在为PHP做贡献了。只不过如果你是本书的读者,可能更想为PHP-Runtime做贡献,比如:修复PHP代码的bug,提交功能改进。 我们可能根据自己的特长来为PHP做贡献,如果你英语好,那么翻译手册将会是你的强项。如果你的C比较好,那么可以为PHP修改bug,如果你对PHP语言的语法或者功能有改进想法,你可以提交改进方法,当然如果你能将该功能实现出来那更好不过了。 下面介绍一下,为PHP做贡献的方方面面。 ## 沟通方式[]() ## 邮件组[]() 在很多开源项目中,邮件组都是作为最主要的沟通方式,邮件组虽然古老,但是却很有效,每个人都会有一个邮箱,可以快捷的使用邮件客户端来沟通,目前的邮件客户端都很好用,可以根据主题进行汇总。Gmail和QQmail就做的不错。 PHP官方的邮件组都列在这里了: [http://php.net/mailing-lists.php](http://php.net/mailing-lists.php) ## IRC[]() 国内IRC使用的不太多,PHP核心开发者都会在 http://www.efnet.org/ 的#php.pecl 频道。如果你想直接找某个开发者,在#php.pecl频道应该可以找的到 :) ## 报告和修复Bug[]() PHP的bug可以在[http://bugs.php.net](http://bugs.php.net)上提交。在这里你可以提交和php相关的各种bug,虽然是bug管理,其实这里还可以提出你的需求,比如你觉得PHP缺失某个功能,你可以在这里提交。在提交的同时,如果你能提供实现补丁那再好不过了。没有补丁也没有关系,如果这的确是个bug,根据紧急和难易程度可能会有同学帮你修复,如果是一个功能改进,同时对PHP的改动比较大,那么这个需要提交到php-internals邮件组进行讨论,如果已经有实现了,讨论充分后就可以进行投票了。如果通过投票,那么恭喜你。 > 这里的bug通指bug和feature,也就是非预期行为以及功能需求。 ## 原则[]() 这里的的Bug指的是PHP语言本身的bug,而不是应用程序的bug,比如某个函数的行为和预期不一致,或者运行某段程序后PHP崩溃了,或者性能低下,你都可以提交报告。 这有一些基本的原则: 如果是bug: 1. 你需要确认这的确是PHP的bug,而不是应用程序的bug 1. 确认你使用正确,也就是是否和PHP手册文档使用一致 1. 尽量用最少的代码来重现问题。这将有利于问题的追查 ## 修复[]() 如果你发现了PHP的一个bug,而同时你想到了解决方案,可以在[Github][http://github.com/php/php-src]上提交一个Pull Request,或者也可以直接把修改的patch上传到你所提交的bug页 ## 测试[]() 在你发现了一个bug或者实现一个功能时,你需要为你的bug或者功能编写测试,测试用例的编写可以参考[附录E phpt测试文件说明](#) 测试时可以使用make test TESTS=/path/to/your/bugXXXX.phpt 来进行测试。通常如果是个bug,那么会将测试的名称命名为bugXXXX.phpt XXXX为bug的ID。 ## 贡献功能[]() ## RFC (Request For Comments)[]() 比如你觉得PHP不支持重载很不习惯,你想PHP支持这个特性,对于这个特性来说,这是一个非常大的变动,这样的话你最好编写一个RFC说明一下你为什么需要这样一个特性。因为PHP的用户量是非常大的,任何一个变动都会影响到非常多的人,所以你必须说服绝大多数人赞同你的想法。 你的RFC可以放在任何地方,比如直接发送到邮件组讨论,或者放在github上,不过通常,你可以把RFC放在PHP官方的wiki上方便讨论。 1. 在[https://wiki.php.net/start?do=register](https://wiki.php.net/start?do=register)上申请一个账号。 1. 发送邮件到: php-webmaster@lists.php.net 申请RFC的编写权限(Request for RFC karma),同时你得提供你的wiki用户名,最好同时说明你要创建什么样的一个RFC。 好了后你就可以编写RFC了,具体流程见官方的说明吧:[https://wiki.php.net/rfc/howto](https://wiki.php.net/rfc/howto) ## 邮件组讨论[]() 编写好了后,你可以把你的RFC发送到internals@lists.php.net邮件组。当然为了防止你的RFC白写了,你可以直接把你的需求发送到邮件组看看大家的反应,看看大家是否对这个特性或者变动感兴趣。 > 为了保证你收到大家的邮件,最好在[http://php.net/mailing-lists.php](http://php.net/mailing-lists.php)订阅一下 邮件组的邮件,同时也推荐大家订阅这个邮件组,可以知道PHP发展的最新动态。 邮件发出来以后,大家可能会质疑你的想法,这时你就需要对家的疑问进行解答。尽可能的把自己的想法表达清楚。 经过一番讨论后,如果感觉进展还可以,那么你就可以发起投票了。投票是在wiki上进行的,可以参考[https://wiki.php.net/rfc/trailing-comma-function-args](https://wiki.php.net/rfc/trailing-comma-function-args) 修改wiki后需要再给邮件组发送邮件,通知到大家来进行投票。 提议被接受的比如为:50%+1 ## 代码实现[]() 如果你已经实现了你想要的功能,记得在RFC中体现,很多时候有一些功能可能大家都希望有,但是由于没有人来实现或者现有的实现不够好。如果有一个实现可能会大大提高你的RFC被接受的可能性。 如果你不太熟悉PHP内核也没有关系,如果的确是个不错的主意,肯定会有人来帮你实现的。 > 所以如果你有不错的想法欢迎反馈 ## 贡献PECL扩展[]() 如果你写了一个扩展,想分享给大家可以通过pecl来发布你的扩展。 官方有明确的说明:[http://pecl.php.net/account-request.php](http://pecl.php.net/account-request.php) 不过在发起之前请留意:1. 首先你要确保你的扩展的许可:推荐使用PHP3.0.1、BSD或者类Apache许可。2. 你的扩展是否已经有个类似的实现了?如果有人也实现了,社区可能不太会认同。 ## Composer[]() 从目前来看,目前很少有人维护pear的库了,和ruby社区的gem比起来太过冷清了。目前一个新起之秀: [http://getcomposer.org/](http://getcomposer.org/) Composer表现不俗,目前绝大多数的PHP开源项目都是用composer来进行包管理,所以你如果有开源库的话,也推荐使用composer。 ## 改进和增加文档[]() 目前PHP的文档还算比较全,不过随着版本的升级可能会有些文档没有跟上,或者有的地方会有错误。PHP目前提供了一个方便的平台来协同维护文档。登陆:[http://edit.php.net](http://edit.php.net)即可对文档进行修改。修改后会有人对修改进行review,如果合适的话会把修改合并进去。 同时手册的中文版本也需要维护,所以如果你感兴趣也可以对文档进行翻译。 如果你有想法,可以参考:[http://marc.info/?l=phpdoc&m=136370213519136&w=2](http://marc.info/?l=phpdoc&m=136370213519136&w=2)
';

附录C VLD扩展使用指南

最后更新于:2022-04-01 20:39:47

[VLD(Vulcan Logic Dumper)](http://pecl.php.net/package/vld/)是一个挂钩在Zend引擎下,并且输出PHP脚本生成的中间代码(执行单元)的扩展。它可以在一定程序上查看Zend引擎内部的一些实现原理,是我们学习PHP源码的必备良器。它的作者是[Derick Rethans](http://derickrethans.nl/projects.html),除了VLD扩展,我们常用的[XDebug扩展](http://xdebug.org/)的也有该牛人的身影。 VLD扩展是一个开源的项目,在[这里](http://pecl.php.net/package/vld/)可以下载到最新的版本,虽然最新版本的更新也是一年前的事了。作者没有提供编译好的扩展,Win下使用VC6.0编译生成dll文件。 *nix系统下直接configue,make,make install生成。如果遇到问题,请自行Google之。 看一个简单的例子,假如存在t.php文件,其内容如下: $a = 10; echo $a; 在命令行下使用VLD扩展显示信息。 php -dvld.active=1 t.php -dvld.active=1表示激活VLD扩展,使用VLD扩展输出中间代码,此命令在CMD中输出信息为: Branch analysis from position: 0 Return found filename: D:\work\xampp\xampp\php\t.php function name: (null) number of ops: 5 compiled vars: !0 = $a line # * op fetch ext return operands --------------------------------------------------------------------------------- 2 0 > EXT_STMT 1 ASSIGN !0, 10 3 2 EXT_STMT 3 ECHO !0 4 4 > RETURN 1   branch: # 0; line: 2- 4; sop: 0; eop: 4 path #1: 0, 10 如上为VLD输出的PHP代码生成的中间代码的信息,说明如下: - Branch analysis from position 这条信息多在分析数组时使用。 - Return found 是否返回,这个基本上有都有。 - filename 分析的文件名 - function name 函数名,针对每个函数VLD都会生成一段如上的独立的信息,这里显示当前函数的名称 - number of ops 生成的操作数 - compiled vars 编译期间的变量,这些变量是在PHP5后添加的,它是一个缓存优化。这样的变量在PHP源码中以IS_CV标记。 - op list 生成的中间代码的变量列表 使用-dvld.active参数输出的是VLD默认设置,如果想看更加详细的内容。可以使用-dvld.verbosity参数。 php -dvld.active=1 -dvld.verbosity=3 t.php -dvld.verbosity=3或更大的值的效果都是一样的,它们是VLD在当前版本可以显示的最详细的信息了,包括各个中间代码的操作数等。显示结果如下: Finding entry points Branch analysis from position: 0 Add 0 Add 1 Add 2 Add 3 Add 4 Return found filename: D:\work\xampp\xampp\php\t.php function name: (null) number of ops: 5 compiled vars: !0 = $a line # * op fetch ext return operands -------------------------------------------------------------------------------- - 2 0 > EXT_STMT RES[ IS_UNUSED ] OP1[ IS_UNUSED ] OP2[ IS_UNUSED ] 1 ASSIGN OP1[IS_CV !0 ] OP2[ , IS_CONST (0) 10 ] 3 2 EXT_STMT RES[ IS_UNUSED ] OP1[ IS_UNUSED ] OP2[ IS_UNUSED ] 3 ECHO OP1[IS_CV !0 ] 4 > RETURN OP1[IS_CONST (0) 1 ]   branch: # 0; line: 2- 3; sop: 0; eop: 4 path #1: 0, 10 以上的信息与没有加-dvld.verbosity=3的输出相比,多了Add 字段,还有中间代码的操作数的类型,如IS_CV,IS_CONST等。PHP代码中的$a = 10; 其中10的类型为IS_CONST,$a作为一个编译期间的一个缓存变量存在,其类型为IS_CV。 如果我们只是想要看输出的中间代码,并不想执行这段PHP代码,可以使用-dvld.execute=0来禁用代码的执行。 php -dvld.active=1 -dvld.execute=0 t.php 运行这个命令,你会发现这与最开始的输出有一点点不同,它没有输出10。除了直接在屏幕上输出以外,VLD扩展还支持输出.dot文件,如下的命令: php -dvld.active=1 -dvld.save_dir='D:\tmp' -dvld.save_paths=1 -dvld.dump_paths=1 t.php 以上的命令的意思是将生成的中间代码的一些信息输出在D:/tmp/paths.dot文件中。-dvld.save_dir指定文件输出的路径,-dvld.save_paths控制是否输出文件,-dvld.dump_paths控制输出的内容,现在只有0和1两种情况。输出的文件名已经在程序中硬编码为paths.dot。这三个参数是相互依赖的关系,一般都会同时出现。 总结一下,VLD扩展的参数列表: - -dvld.active 是否在执行PHP时激活VLD挂钩,默认为0,表示禁用。可以使用-dvld.active=1启用。 - -dvld.skip_prepend 是否跳过php.ini配置文件中[auto_prepend_file](http://php.net/auto-prepend-file)指定的文件,默认为0,即不跳过包含的文件,显示这些包含的文件中的代码所生成的中间代码。此参数生效有一个前提条件:-dvld.execute=0 - -dvld.skip_append 是否跳过php.ini配置文件中[auto_append_file](http://php.net/auto-append-file)指定的文件,默认为0,即不跳过包含的文件,显示这些包含的文件中的代码所生成的中间代码。此参数生效有一个前提条件:-dvld.execute=0 - -dvld.execute 是否执行这段PHP脚本,默认值为1,表示执行。可以使用-dvld.execute=0,表示只显示中间代码,不执行生成的中间代码。 - -dvld.format 是否以自定义的格式显示,默认为0,表示否。可以使用-dvld.format=1,表示以自己定义的格式显示。这里自定义的格式输出是以-dvld.col_sep指定的参数间隔 - -dvld.col_sep 在-dvld.format参数启用时此函数才会有效,默认为 "\t"。 - -dvld.verbosity 是否显示更详细的信息,默认为1,其值可以为0,1,2,3 其实比0小的也可以,只是效果和0一样,比如0.1之类,但是负数除外,负数和效果和3的效果一样比3大的值也是可以的,只是效果和3一样。 - -dvld.save_dir 指定文件输出的路径,默认路径为/tmp。 - -dvld.save_paths 控制是否输出文件,默认为0,表示不输出文件 - -dvld.dump_paths 控制输出的内容,现在只有0和1两种情况,默认为1,输出内容
';

附录B PHP的历史

最后更新于:2022-04-01 20:39:45

';

附录A PHP及Zend API

最后更新于:2022-04-01 20:39:43

';

附录

最后更新于:2022-04-01 20:39:40

[附录A PHP及Zend API](files--php-zend-api.md) [附录B PHP的历史](files--php-versions-and-history.md) [附录C VLD扩展使用指南](files--php-vld.md) [附录D 怎样为PHP贡献](files--how-to-contribute-to-php.md) [附录E phpt测试文件说明](files--phpt-file.md) [附录F PHP5.4新功能升级解析](files--upgrade-to-php-5-4-explain.md) [附录G:re2c中文手册](files--re2c-mannual.md)
';

第二十章 怎么样系列(how to)

最后更新于:2022-04-01 20:39:38

这一章用于编写哪些PHP中的一些小点内容,比如:遇到PHP的bug该怎么样定位及修复,和本书相似,how to系列主要偏重于PHP实现方面,当然也尽可能的结合具体应用来解释。 如果有什么有意思或疑惑的点需要了解或探讨可以留言,我们将根据情况进行相应的实现。
';

foreach的实现

最后更新于:2022-04-01 20:39:36

foreach是PHP的关键字,用来实现基于数据的循环。基于数据循环语句的循环是由数据结构中的元素的数目来控制的。一般来说,基于数据的循环语句会使用一种称之为迭代器的函数来实现元素的遍历。 除了foreach,PHP还提供了预定义的一些函数来实现对数组的迭代访问操作,如current, next, reset等等。然而我们使用得最多的还是foreach语句,foreach可以直接迭代访问数组,如果用户自己定义的对象需要使用此语句进行迭代访问,必须实现SPL的迭代器。 这一小节,我们具体介绍PHP中foreach的实现过程。foreach 语法结构提供了遍历数组的简单方式。foreach 仅能够应用于数组和对象,如果尝试应用于其他数据类型的变量,或者未初始化的变量,将导致错误。foreach每次循环时,当前单元的值被赋给 $value 并且数组内部的指针向前移一步(因此下一次循环中将会得到下一个单元)。 ### 循环过程的实现[]() foreach语句在语法解析时对应三个操作: 1. zend_do_foreach_begin: 循环开始操作,生成FE_RESET中间代码,数组会在循环开始时执行RESET操作,即我们使用foreach遍历时不用每次重新手动RESET,同时此操作也会生成获取变量的FE_FETCH中间代码。 1. zend_do_foreach_cont:根据需要获取变量的状态判断是否引用,此处的引用会影响FE_RESET的初始化操作和FE_FETCH中间代码的获取变量操作。 1. zend_do_foreach_end:设置ZEND_JMP中间代码,设置下一条OP,以跳出循环,结束循环,清理工作。 这三个操作都是语法解析时对应的函数名,在编译过程中会直接调用。他们形成的中间代码在PHP内核执行时,形成的循环遍历效果是:在foreach遍历之前, PHP内核首先会有个FE_RESET操作来重置数组的内部指针,也就是pInternalPointer, 然后通过每次FE_FETCH将pInternalPointer指向数组的下一个元素,从而实现顺序遍历。并且每次FE_FETCH的结果都会被一个全局的中间变量存储,以给下一次的获取元素使用。 如下面这段代码: $arr = [array](http://www.php.net/array)(1, 2, 3, 4, 5);   foreach ($arr as $key => $row) { [echo](http://www.php.net/echo) $key , $row; } 这是一个标准的foreach循环使用示例。在VLD扩展中我们可以看到如下的中间代码: number of ops: 16 compiled vars: !0 = $arr, !1 = $key, !2 = $row line # * op fetch ext return operands --------------------------------------------------------------------------------- 2 0 > INIT_ARRAY ~0 1 1 ADD_ARRAY_ELEMENT ~0 2 2 ADD_ARRAY_ELEMENT ~0 3 3 ADD_ARRAY_ELEMENT ~0 4 4 ADD_ARRAY_ELEMENT ~0 5 5 ASSIGN !0, ~0 4 6 > FE_RESET $2 !0, ->14 7 > > FE_FETCH $3 $2, ->14 8 > ZEND_OP_DATA ~5 9 ASSIGN !2, $3 10 ASSIGN !1, ~5 5 11 ECHO !1 12 ECHO !2 6 13 > JMP ->7 14 > SWITCH_FREE $2 7 15 > RETURN 1 当我们通过RESET初始化数组后,FETCH会获取变量,并将数组的内部指针指向一个元素。在前面我们讲过,常规情况下OPCODE的执行是一条一条依次执行的,则在FE_FETCH获取完变量后,PHP内核会依次执行后续的OPCODE,当执行到JMP时,会重新跳到->7,即再一次获取变量,如此构成一个循环。当FE_FETCH执行失败时,会跳转到->14,即SWITCH_FREE,从而结束整个循环。 ### 指针的意外行为[]() 在PHP手册中有这样一个NOTE: > Note: 当 foreach 开始执行时,数组内部的指针会自动指向第一个单元。这意味着不需要在 foreach 循环之前调用 reset()。 由于 foreach 依赖内部数组指针,在循环中修改其值将可能导致意外的行为。 比如这样一段代码: $arr = array(1,2,3,4,5);   foreach($arr as $key => $row) { echo key($arr), '=>', current($arr), "\r\n"; } 如果在$row加上引用呢?如果在遍历前添加 **$g = $arr;** 呢?结果是上面示例的代码只会输出数组的第二个元素。修改建议的代码会依次输出变量,但是第一个元素并没有在输出结果中出现。 这个异常引申出三个问题: 1. 为什么foreach循环体中执行key或current会显示第二个元素(非引用情况)?以key函数为例,我们执行函数调用时,会执行中间代码SEND_REF,此中间代码会将没有设置引用的变量复制一份并设置为引用。当进入循环体时,PHP内核已经经过了一次fetch操作,相当于执行了一次next操作,当前元素指向第二个元素。因此我们在foreach的循环体中执行key函数时,key中调用的数组变量为PHP执行了一次fetch操作的数组拷贝,此时foreach的内部指针指向第二个元素。 1. 为什么在foreach中执行end等操作,其循环过程不变?在遍历的代码中通过end,next等操作数组的指针,数组的指针不会变化,这是因为在PHP内核进行FETCH操作时,会通过中间变量存储当前操作数组的内部指针,每遍历一个元素,会先获取之前存储的指针位置,获取下一个元素后,再恢复指针位置,关键在于FETCH OPCODE执行过程中的中间变量。 1. 为什么$row的引用和非引用情况下输出结果不同?如果是引用,PHP内核在reset数组时,会直接分裂数组,生成一个数组的拷贝,并将其设置为引用。如果是非引用,PHP内核在reset数组时,当数组的引用计数大于1,并且不存在引用时,会拷贝数组供foreach使用,其它情况使用原数组,将其引用计数加1。因为引用的不同,在循环体中给函数传递参数时其结果不同,导致看到的foreach数组内部指针变化的不同。对于非引用且引用计数大于1的情况,其本身就是两个不同的数组,在RESET时就不同了。
';

第一节 循环语句

最后更新于:2022-04-01 20:39:34

PHP是一种动态脚本语言,属于命令式程序设计语言的一种。命令式程序式程序设计语言的实质是赋值语句占主导地位。赋值语句的目的是改变变量的值,因此在所有的命令式程序设计语言中,其相同之处就是不停的变幻变量的值,最后达到我们的目的。然而我们的程序并不是仅仅是由赋值语句组成的。至少还需要两种额外的语言机制: 1. 控制路径选择的方法。 1. 控制某些语句重复执行的方法。 这些方法或语言机制我们称之为控制语句。大量的控制语句可以提高程序语言的可写性,但是同时又会降低程序语言的可读性,为此,经常需要在这二者之间进行权衡。 这一章我们会说明PHP中循环和迭代的实现过程。
';

第十六章 PHP语言特性的实现

最后更新于:2022-04-01 20:39:31

这一章主要涉及一些PHP特性的实现。 > 编写中
';

第十章 输出缓冲

最后更新于:2022-04-01 20:39:29

PHP脚本在执行的过程中会不停的输出内容到用户浏览器(或者标准输出),这个行为看起来没什么问题,不过有的时候我们会有一些比较特殊的需求,比如不想输出内容,或者将输出的内容缓存起来等等。 PHP提供了一套基本的机制来实现内容输出的缓冲。对于用户而言PHP提供了一些列[操作输出缓冲的函数](http://cn2.php.net/manual/zh/ref.outcontrol.php)。
';

第九章 错误和异常处理

最后更新于:2022-04-01 20:39:27

编程在大部分时候都需要处理异常情况,程序会依赖很多的外部因素,而外部因素很多都是不可控的,[墨菲定律](http://zh.wikipedia.org/zh-cn/%E6%91%A9%E8%8F%B2%E5%AE%9A%E7%90%86)告诉我们:_凡是可能出错的事必定会出错_,不可避免的为了健壮性和可用性,我们必须要处理各种可能的异常情况。 从输入输出来看,每个接受输入的单元对外部的输入检查越严格从可靠性上来看都是更好的,不过从系统的分层来看,可靠性和开发维护的成本来看两者是矛盾的。比如对于一个不对外暴漏的函数来说过分的检查参数也会带来不必要的成本。 本章涉及的内容注重在PHP语言再错误及异常处理的实现机制以及相应的风格推荐分析。
';

第三节 PHP中的线程安全

最后更新于:2022-04-01 20:39:25

## 缘起TSRM[]() 在多线程系统中,进程保留着资源所有权的属性,而多个并发执行流是执行在进程中运行的线程。如Apache2 中的woker,主控制进程生成多个子进程,每个子进程中包含固定的线程数,各个线程独立地处理请求。同样,为了不在请求到来时再生成线程,MinSpareThreads和MaxSpareThreads设置了最少和最多的空闲线程数;而MaxClients设置了所有子进程中的线程总数。如果现有子进程中的线程总数不能满足负载,控制进程将派生新的子进程。 当PHP运行在如上类似的多线程服务器时,此时的PHP处在多线程的生命周期中。在一定的时间内,一个进程空间中会存在多个线程,同一进程中的多个线程公用模块初始化后的全局变量,如果和PHP在CLI模式下一样运行脚本,则多个线程会试图读写一些存储在进程内存空间的公共资源(如在多个线程公用的模块初始化后的函数外会存在较多的全局变量), 此时这些线程访问的内存地址空间相同,当一个线程修改时,会影响其它线程,这种共享会提高一些操作的速度,但是多个线程间就产生了较大的耦合,并且当多个线程并发时,就会产生常见的数据一致性问题或资源竞争等并发常见问题,比如多次运行结果和单线程运行的结果不一样。如果每个线程中对全局变量、静态变量只有读操作,而无写操作,则这些个全局变量就是线程安全的,只是这种情况不太现实。 为解决线程的并发问题,PHP引入了TSRM: 线程安全资源管理器(Thread Safe Resource Manager)。TRSM 的实现代码在 PHP 源码的 /TSRM 目录下,调用随处可见,通常,我们称之为 TSRM 层。一般来说,TSRM 层只会在被指明需要的时候才会在编译时启用(比如,Apache2+worker MPM,一个基于线程的MPM),因为Win32下的Apache来说,是基于多线程的,所以这个层在Win32下总是被开启的。 ## TSRM的实现[]() 进程保留着资源所有权的属性,线程做并发访问,PHP中引入的TSRM层关注的是对共享资源的访问,这里的共享资源是线程之间共享的存在于进程的内存空间的全局变量。当PHP在单进程模式下时,一个变量被声明在任何函数之外时,就成为一个全局变量。 PHP解决并发的思路非常简单,既然存在资源竞争,那么直接规避掉此问题,将多个资源直接复制多份,多个线程竞争的全局变量在进程空间中各自都有一份,各做各的,完全隔离。以标准的数组扩展为例,首先会声明当前扩展的全局变量,然后在模块初始化时会调用全局变量初始化宏初始化array的,比如分配内存空间操作。 这里的声明和初始化操作都是区分ZTS和非ZTS,对于非ZTS的情况,直接就是声明变量,初始化变量。对于ZTS情况,PHP内核会添加TSRM,对应到这里的代码就是声明时不再是声明全局变量,而是用ts_rsrc_id代码,初始化是不再是初始化变量,而是调用ts_allocate_id函数在多线程环境中给当前这个模块申请一个全局变量并返回资源ID。 资源ID变量名由模块名和global_id组成。它是一个自增的整数,整个进程会共享这个变量,在进程SAPI初始调用,初始化TSRM环境时,id_count作为一个静态变量将被初始化为0。这是一个非常简单的实现,自增。确保了资源不会冲突,每个线程的独立。 ### 资源id的分配[]() 当通过ts_allocate_id函数分配全局资源ID时,PHP内核会锁一下,确保生成的资源ID的唯一,这里锁的作用是在时间维度将并发的内容变成串行,因为并发的根本问题就是时间的问题。 当加锁以后,id_count自增,生成一个资源ID,生成资源ID后,就会给当前资源ID分配存储的位置,每一个资源都会存储在 resource_types_table 中,当一个新的资源被分配时,就会创建一个tsrm_resource_type。每次所有tsrm_resource_type以数组的方式组成tsrm_resource_table,其下标就是这个资源的ID。其实我们可以将tsrm_resource_table看做一个HASH表,key是资源ID,value是tsrm_resource_type结构。只是,任何一个数组都可以看作一个HASH表,如果数组的key值有意义的话。 resource_types_table的定义如下: typedef struct { size_t size;//资源的大小 ts_allocate_ctor ctor;//构造方法指针 ts_allocate_dtor dtor;//析构方法指针 int done; } tsrm_resource_type; 在分配了资源ID后,PHP内核会接着遍历**所有线程**为每一个线程的tsrm_tls_entry分配这个线程全局变量需要的内存空间。这里每个线程全局变量的大小在各自的调用处指定。 每一次的ts_allocate_id调用,PHP内核都会遍历所有线程并为每一个线程分配相应资源,如果这个操作是在PHP生命周期的请求处理阶段进行,岂不是会重复调用? PHP考虑了这种情况,ts_allocate_id的调用在模块初始化时就调用了。 在模块初始化阶段,通过SAPI调用tsrm_startup启动TSRM,tsrm_startup函数会传入两个非常重要的参数,一个是expected_threads,表示预期的线程数,一个是expected_resources,表示预期的资源数。不同的SAPI有不同的初始化值,比如mod_php5,cgi这些都是一个线程一个资源。 TSRM启动后,在模块初始化过程中会遍历每个扩展的模块初始化方法,扩展的全局变量在扩展的实现代码开头声明,在MINIT方法中初始化。其在初始化时会知会TSRM申请的全局变量以及大小,这里所谓的知会操作其实就是前面所说的ts_allocate_id函数。TSRM在内存池中分配并注册,然后将资源ID返回给扩展。后续每个线程通过资源ID定位全局变量,比如我们前面提到的数组扩展,如果要调用当前扩展的全局变量,则使用:ARRAYG(v),这个宏的定义: #ifdef ZTS #define ARRAYG(v) TSRMG(array_globals_id, zend_array_globals *, v) #else #define ARRAYG(v) (array_globals.v) #endif 如果是非ZTS则直接调用全局变量的属性字段,如果是ZTS,则需要通过TSRMG获取变量。 TSRMG的定义: #define TSRMG(id, type, element) (((type) (*((void ***) tsrm_ls))[TSRM_UNSHUFFLE_RSRC_ID(id)])->element) 去掉这一堆括号,TSRMG宏的意思就是从tsrm_ls中按资源ID获取全局变量,并返回对应变量的属性字段。 那么现在的问题是这个tsrm_ls从哪里来的? 其实这在我们写扩展的时候会经常用到: #define TSRMLS_D void ***tsrm_ls #define TSRMLS_DC , TSRMLS_D #define TSRMLS_C tsrm_ls #define TSRMLS_CC , TSRMLS_C 以上为ZTS模式下的定义,非ZTS模式下其定义全部为空。 最后个问题,tsrm_ls是从什么时候开始出现的,从哪里来?要到哪里去? 答案就在php_module_startup函数中,在PHP内核的模块初始化时,如果是ZTS模式,则会定义一个局部变量tsrm_ls,这就是我们线程安全开始的地方。从这里开始,在每个需要的地方通过在函数参数中以宏的形式带上这个参数,实现线程的安全。 ## 参考资料[]() - [究竟什么是TSRMLS_CC?- 54chen](http://www.54chen.com/php-tech/what-is-tsrmls_cc.html) - [深入研究PHP及Zend Engine的线程安全模型](http://blog.codinglabs.org/articles/zend-thread-safety.html)
';

第二节 线程,进程和并发

最后更新于:2022-04-01 20:39:22

## 进程[]() 进程是什么?进程是正在执行的程序;进程是正在计算机上执行的程序实例;进程是能分配给处理器并由处理器执行的实体。进程一般会包括指令集和系统资源集,这里的指令集是指程序代码,这里的系统资源集是指I/O、CPU、内存等。综合起来,我们也可以理解进程是具有一定独立功能的程序在关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位。 在进程执行时,进程都可以被唯一的表示,由以下一些元素组成: - 进程描述符:进程的唯一标识符,用来和其它进程区分。在Linux中叫进程ID,在系统调用fork期间生成,只是我们通过getpid返回的不是其pid字段,而是其线程组号tgid。 - 进程状态:我们常说的挂起、运行等状态,其表示的是当前的状态。 - 优先级:进程间的执行调度相关,相对于其它进程而言。 - 程序计数器:程序中即将被执行的下一条指令的地址,该地址是内核术中或用户内存空间中的内存地址。 - 内存指针:包括程序代码和进程相关数据的指针,还有和其它进程共享内存块的指针。 - 上下文数据:进程执行时处理器的寄存器的数据。 - I/O状态信息:包括显式的I/O请求、分配给进程的I/O设备等 - 记账信息:可能包括处理器时间总和、使用的时钟数总和、时间限制等 以上的这些元素都会放在一个叫做进程控制块的数据结构中。进程控制块是操作系统能够支持多进程和提供多处理的结构。当操作系统做进程切换时,它会执行两步操作,一是中断当前处理器中的进程,二是执行下一个进程。不管是中断还是执行,进程控制块中的程序计数器、上下文数据和进程状态都会发生变化。当进程中断时,操作系统会把程序计数器和处理器寄存器(对应进程控制块中的上下文数据)保存到进程控制块中的相应位置,进程状态也会有所变化,可能进入阻塞状态,也有可能进入就绪态。当执行下一个进程时,操作系统按规则将下一个进程设置为运行态,并加载即将要执行进程的程序上下文数据和程序计数器等。 ## 线程[]() 进程有两个特性部分:资源所有权和调度执行。资源所有权是指进程包括了进程运行所需要的内存空间、I/O等资源。调度执行是指进程执行过程中间的执行路径,或者说程序的指令执行流。这两个特性部分是可以分开的,分开后,拥有资料所有权的通常称为进程,拥有执行代码的可分派部分的被称之为线程或轻量级进程。 线程有“执行的线索”的意思在里面,而进程在多线程环境中被定义为资源所有者,其还是会存储进程的进程控制块。线程的结构与进程不同,每个线程包括: - 线程状态: 线程当前的状态。 - 一个执行栈 - 私有的数据区: 用于每个线程局部变量的静态存储空间 - 寄存器集: 存储处理器的一些状态 每个进程都有一个进程控制块和用户地址空间,每个线程都有一个独立的栈和独立的控制块,都有自己一个独立执行上下文。其结构如图8.1所示。 ![图8.1 进程模型图](http://box.kancloud.cn/2015-07-06_559a632f7956c.jpg) 图8.1 进程模型图 线程在执行过程中与进程有一些不同。每个独立的线程有一个程序运行的入口、顺序执行序列和程序的出口。但是线程不能够独立执行,必须依存在于进程之中,由进程提供多个线程执行控制。从逻辑角度来看,多线程的意义在于一个进程中,有多个执行部分可以同时执行。此时,进程本身不是基本运行单位,而是线程的容器。 线程较之进程,其优势在于一个快,不管是创建新的线程还是终止一个线程;不管是线程间的切换还是线程间共享数据或通信,其速度与进程相比都有较大的优势。 ## 并发及并行[]() 并发又称共行,是指能处理多个同时性活动的能力,并发事件之间不一定要同一时刻发生。比如,现代计算机系统可在同一段时间内以进程的形式将多个程序加载到存储器中,并借由处理器的时分复用,以在一个处理器上表现出同时运行的感觉。 并行是指同时发生的两个并发事件,具有并发的含义,而并发则不一定并行。 并发和并行的区别就是一个处理器同时处理多个任务和多个处理器或者是多核的处理器同时处理多个不同的任务。前者是逻辑上的同时发生(simultaneous),而后者是物理上的同时发生。 ### PHP的各种并发模型[]() ## 参考资料[]() 《操作系统精髓与设计原理》
';

第八章 线程安全

最后更新于:2022-04-01 20:39:20

线程安全这一样主要介绍PHP中的线程话题。 > 本章目前仍然在编写中。
';

第五节 小结

最后更新于:2022-04-01 20:39:18

Zend引擎作为PHP的核心,它的作用尤为重要,Zend引擎实现了PHP的语法以及语言扩展机制,前面的章节已经陆陆续续介绍了Zend引擎的一些内容,本章则详细介绍了Zend虚拟机的实现机制。 本章完整的介绍了词法分析和语法分析的细节内容,同时完整的实现了一个PHP语法结构,用以在运行时获取到变量的名称。随后介绍了PHP代码的分发和安全,以此引出PHP代码的加密和解密,并实现了一个简单的扩展来实现代码的加密。
';

第四节 PHP代码的加密解密

最后更新于:2022-04-01 20:39:15

PHP语言作为脚本语言的一种,由于不需要进行编译,所以通常PHP程序的分发都是直接发布源代码。对于一些开源软件来说,这并没有什么问题,因为它本来就希望有更多的人阅读代码,希望有更多的人参与进来,而对于商业代码来说,这却是一个不太好的消息,不管是从商业秘密,还是从对公司产权的保护来说却是一个问题,基于此,从而引出了对PHP代码的加密和解密的议题。例如国内的Discuz论坛程序在开源之前要运行是必须安装Zend Optimizer的,Zend官方的代码加密软件是[Zend Guard](http://www.zend.com/en/products/guard/),可以用来加密和混淆PHP代码,这样分发出去的代码就可以避免直接分发源代码,不过加密后的代码是无法直接运行的,在运行时还需要一个解密的模块来运行加密后的程序,要运行Zend Guard加密后的代码需要安装Zend Optimizer(PHP5.2之前的版本),或者安装Zend Guard Loader(PHP5.3版本)扩展才能运行。 ## 加密的本质[]() 本质上程序在运行时都是在执行机器码,而基于虚拟机的语言的加密通常也是加密到这个级别,也就是说PHP加密后的程序在执行之前都会解密成opcode来执行。 PHP在执行之前有一个编译的环节,编译的结果是opcode,然后由Zend虚拟机执行,从这里看如果只要将源代码加密,然后在执行之前将代码解密即可。 从这里看,只要代码能被解密为opcode,那么总有可能反编译出来源代码,其他的语言中也是类似,比如objdump程序能将二进制程序反汇编出来,.NET、Java的程序也是一样,都有一些反编译的程序,不过通常这些厂商同时还会附带代码混淆的工具,经过混淆的代码可读性极差,很多人都留意过Gmail等网站经过混淆的JS代码吧,他们阅读起来非常困难,经过混淆的代码即使反编译出来,读者也很难通过代码分析出代码中的逻辑,这样也就极大的增加了应用的安全性。 ## 简单的代码加密解密实战[]() 根据前文的介绍,作为实例,本文将编写一个简单的代码加密扩展用于对PHP代码的加密,我们只需要能把源码加密,简单通过浏览源代码的方法无法获取到源代码那我们的目标就达到了,为了能正确执行加密后的代码,我们还需要另一个模块:解密模块。 简单的思路是把所有的PHP文件代码进行加密,同时另存为同名的PHP文件,这是一种很简单的做法,只是为了防止源代码赤裸裸的暴露在代码中。 加密也有很多种做法,第一种简单的方法可以简单的把源码本身进行一些可逆加密,这样我们可以在运行之前把真实的源码反解出来执行,不过这种方式存在一种问题,只要知道了加密算法我们就可以把代码给解出来,采用这种方式唯一能做的就是尽量增加加密的复杂度,既然正式的代码在运行之前会被转化成PHP源代码,通过hack的方式是可以完完整整的获得PHP源码的,保密的效果就很有限了。 因为Zend引擎最终执行的是opcode,那么我们只要保证能解密出opcode则能满足需求,我们只要简单的将opcode进行简单的序列化或者像Zend Guard那样进行混淆,在运行之前将opcode还原,那么源代码的信息就不存在了,这样我们就能保证源代码的安全,而不至于泄露。 ### 加密[]() 前面提到加密的目的就是为了防止轻易获取程序源码的一种手段,对于PHP来说,将源码编译为opcode已经能达到目的了,因为PHP引擎最终都是需要执行opcode的。虽然可以将加密进一步,但是如果需要修改Zend引擎,那么成本就有点大了,因为需要修改Zend引擎了,而这是无法通过简单的扩展机制来实现了,所以解密的成本也会变的太大,也就没有实际意义了。 在本例中为了方便,代码的加密和解密实现均实现在同一个模块中。 熟悉PHP的同学可能会发现,这种加密方式和opcode缓存本质上没有太大差别,opcode缓存的工作是将源码编译为opcode然后缓存起来,在执行的时候绕过编译直接执行opcode,的确是没错的。这里唯一的区别是:opcode缓存是动态透明的,而加密后我们要做的是分发加密后的代码。这么说我们是不是可以直接将APC之类的缓存扩展进行改造就可以了,其实理论上是可以的。不过这两者的定位还是有差别的:加密的目的是为了减少源码被分析破解的可能,而缓存只是为了提高程序运行的速度。 ### 解密[]() 本例中的代码其实并没有进行加密,相对源代码来说,opcode编译本身也可以算做一种加密了,因为毕竟通过阅读opcode来理解程序的逻辑还是比较困难的。 ### 实现[]() TODO
';

第三节 中间代码的执行

最后更新于:2022-04-01 20:39:13

在[<< 第二章第三小节 PHP脚本的执行 -- opcode >>](#)中, 我们对opcode进行了一个简略的说明。这一小节我们讲这些中间代码在Zend虚拟机中是如何被执行的。 假如我们现在使用的是CLI模式,直接在SAPI/cli/php_cli.c文件中找到main函数,默认情况下PHP的CLI模式的行为模式为PHP_MODE_STANDARD。此行为模式中PHP内核会调用php_execute_script(&file_handle TSRMLS_CC);来执行PHP文件。顺着这条执行的线路,可以看到一个PHP文件在经过词法分析,语法分析,编译后生成中间代码的过程: EG(active_op_array) = zend_compile_file(file_handle, type TSRMLS_CC); 在销毁了文件所在的handler后,如果存在中间代码,则PHP虚拟机将通过以下代码执行中间代码: zend_execute(EG(active_op_array) TSRMLS_CC); 如果你是使用VS查看源码的话,将光标移到zend_execute并直接按F12,你会发现zend_execute的定义跳转到了一个指针函数的声明(Zend/zend_execute_API.c)。 ZEND_API void (*zend_execute)(zend_op_array *op_array TSRMLS_DC); 这是一个全局的函数指针,它的作用就是执行PHP代码文件解析完的转成的zend_op_array。和zend_execute相同的还有一个zedn_execute_internal函数,它用来执行内部函数。在PHP内核启动时(zend_startup)时,这个全局函数指针将会指向execute函数。注意函数指针前面的修饰符ZEND_API,这是ZendAPI的一部分。在zend_execute函数指针赋值时,还有PHP的中间代码编译函数zend_compile_file(文件形式)和zend_compile_string(字符串形式)。 zend_compile_file = compile_file; zend_compile_string = compile_string; zend_execute = execute; zend_execute_internal = NULL; zend_throw_exception_hook = NULL; 这几个全局的函数指针均只调用了系统默认实现的几个函数,比如compile_file和compile_string函数,他们都是以全局函数指针存在,这种实现方式在PHP内核中比比皆是,其优势在于更低的耦合度,甚至可以定制这些函数。比如在APC等opcode优化扩展中就是通过替换系统默认的zend_compile_file函数指针为自己的函数指针my_compile_file,并且在my_compile_file中增加缓存等功能。 到这里我们找到了中间代码执行的最终函数:execute(Zend/zend_vm_execure.h)。在这个函数中所有的中间代码的执行最终都会调用handler。这个handler是什么呢? if ((ret = EX(opline)->handler(execute_data TSRMLS_CC)) > 0) { } 这里的handler是一个函数指针,它指向执行该opcode时调用的处理函数。此时我们需要看看handler函数指针是如何被设置的。在前面我们有提到和execute一起设置的全局指针函数:zend_compile_string。它的作用是编译字符串为中间代码。在Zend/zend_language_scanner.c文件中有compile_string函数的实现。在此函数中,当解析完中间代码后,一般情况下,它会执行pass_two(Zend/zend_opcode.c)函数。pass_two这个函数,从其命名上真有点看不出其意义是什么。但是我们关注的是在函数内部,它遍历整个中间代码集合,调用ZEND_VM_SET_OPCODE_HANDLER(opline);为每个中间代码设置处理函数。ZEND_VM_SET_OPCODE_HANDLER是zend_vm_set_opcode_handler函数的接口宏,zend_vm_set_opcode_handler函数定义在Zend/zend_vm_execute.h文件。其代码如下: static opcode_handler_t zend_vm_get_opcode_handler(zend_uchar opcode, zend_op* op) { static const int zend_vm_decode[] = { _UNUSED_CODE, /* 0 */ _CONST_CODE, /* 1 = IS_CONST */ _TMP_CODE, /* 2 = IS_TMP_VAR */ _UNUSED_CODE, /* 3 */ _VAR_CODE, /* 4 = IS_VAR */ _UNUSED_CODE, /* 5 */ _UNUSED_CODE, /* 6 */ _UNUSED_CODE, /* 7 */ _UNUSED_CODE, /* 8 = IS_UNUSED */ _UNUSED_CODE, /* 9 */ _UNUSED_CODE, /* 10 */ _UNUSED_CODE, /* 11 */ _UNUSED_CODE, /* 12 */ _UNUSED_CODE, /* 13 */ _UNUSED_CODE, /* 14 */ _UNUSED_CODE, /* 15 */ _CV_CODE /* 16 = IS_CV */ }; return zend_opcode_handlers[opcode * 25 + zend_vm_decode[op->op1.op_type] * 5 + zend_vm_decode[op->op2.op_type]]; }   ZEND_API void zend_vm_set_opcode_handler(zend_op* op) { op->handler = zend_vm_get_opcode_handler(zend_user_opcodes[op->opcode], op); } 在前面章节[<< 第二章第三小节 -- opcode处理函数查找 >>](#)中介绍了四种查找opcode处理函数的方法,而根据其本质实现查找也在其中,只是这种方法对于计算机来说比较容易识别,而对于自然人来说却不太友好。比如一个简单的A + B的加法运算,如果你想用这种方法查找其中间代码的实现位置的话,首先你需要知道中间代码的代表的值,然后知道第一个表达式和第二个表达式结果的类型所代表的值,然后计算得到一个数值的结果,然后从数组zend_opcode_handlers找这个位置,位置所在的函数就是中间代码的函数。这对阅读代码的速度没有好处,但是在开始阅读代码的时候根据代码的逻辑走这样一个流程却是大有好处。 回到正题。handler所指向的方法基本都存在于Zend/zend_vm_execute.h文件文件。知道了handler的由来,我们就知道每个opcode调用handler指针函数时最终调用的位置。 在opcode的处理函数执行完它的本职工作后,常规的opcode都会在函数的最后面添加一句:ZEND_VM_NEXT_OPCODE();。这是一个宏,它的作用是将当前的opcode指针指向下一条opcode,并且返回0。如下代码: #define ZEND_VM_NEXT_OPCODE() \ CHECK_SYMBOL_TABLES() \ EX(opline)++; \ ZEND_VM_CONTINUE()   #define ZEND_VM_CONTINUE() return 0 在execute函数中,处理函数的执行是在一个while(1)循环作用范围中。如下:   while (1) { int ret; #ifdef ZEND_WIN32 if (EG(timed_out)) { zend_timeout(0); } #endif   if ((ret = EX(opline)->handler(execute_data TSRMLS_CC)) > 0) { switch (ret) { case 1: EG(in_execution) = original_in_execution; return; case 2: op_array = EG(active_op_array); goto zend_vm_enter; case 3: execute_data = EG(current_execute_data); default: break; } }   } 前面说到每个中间代码在执行完后都会将中间代码的指针指向下一条指令,并且返回0。当返回0时,while 循环中的if语句都不满足条件,从而使得中间代码可以继续执行下去。正是这个while(1)的循环使得PHP内核中的opcode可以从第一条执行到最后一条,当然这中间也有一些函数的跳转或类方法的执行等。 以上是一条中间代码的执行,那么对于函数的递归调用,PHP内核是如何处理的呢?看如下一段PHP代码: function t($c) { [echo](http://www.php.net/echo) $c, "\n"; if ($c > 2) { return ; } t($c + 1); } t(1); 这是一个简单的递归调用函数实现,它递归调用了两次,这个递归调用是如何进行的呢?我们知道函数的调用所在的中间代码最终是调用zend_do_fcall_common_helper_SPEC(Zend/zend_vm_execute.h)。在此函数中有如下一段: if (zend_execute == execute && !EG(exception)) { EX(call_opline) = opline; ZEND_VM_ENTER(); } else { zend_execute(EG(active_op_array) TSRMLS_CC); } 前面提到zend_execute API可能会被覆盖,这里就进行了简单的判断,如果扩展覆盖了opcode执行函数,则进行特殊的逻辑处理。 上一段代码中的ZEND_VM_ENTER()定义在Zend/zend_vm_execute.h的开头,如下: #define ZEND_VM_CONTINUE() return 0 #define ZEND_VM_RETURN() return 1 #define ZEND_VM_ENTER() return 2 #define ZEND_VM_LEAVE() return 3 这些在中间代码的执行函数中都有用到,这里的ZEND_VM_ENTER()表示return 2。在前面的内容中我们有说到在调用了EX(opline)->handler(execute_data TSRMLS_CC))后会将返回值赋值给ret。然后根据ret判断下一步操作,这里的递归函数是返回2,于是下一步操作是: op_array = EG(active_op_array); goto zend_vm_enter; 这里将EG(active_op_array)的值赋给op_array后,直接跳转到execute函数的定义的zend_vm_enter标签,此时的EG(active_op_array)的值已经在zend_do_fcall_common_helper_SPEC中被换成了当前函数的中间代码集合,其实现代码为: if (EX(function_state).function->type == ZEND_USER_FUNCTION) { // 用户自定义的函数 EX(original_return_value) = EG(return_value_ptr_ptr); EG(active_symbol_table) = NULL; EG(active_op_array) = &EX(function_state).function->op_array; // 将当前活动的中间代码指针指向用户自定义函数的中间代码数组 EG(return_value_ptr_ptr) = NULL; 当内核执行完用户自定义的函数后,怎么返回之前的中间代码代码主干路径呢?这是由于在execute函数中初始化数据时已经将当前的路径记录在EX(op_array)中了(EX(op_array) = op_array;)当用户函数返回时程序会将之前保存的路径重新恢复到EG(active_op_array)中(EG(active_op_array) = EX(op_array);)。可能此时你会问如果函数没有返回呢?这种情况在用户自定义的函数中不会发生的,就算是你没有写return语句,PHP内核也会自动给加上一个return语句,这在第四章 [<< 第四章 函数的实现 第二节 函数的定义,传参及返回值 函数的返回值 >>](#)已经有说明过。 整个调用路径如下图所示: ![图7.2 Zend中间代码调用路径图](http://box.kancloud.cn/2015-07-06_559a632f5d82f.png) 图7.2 Zend中间代码调用路径图 以上是opcode的执行过程,与过程相比,过程中的数据会更加重要,那么在执行过程中的核心数据结构有哪些呢?在Zend/zend_vm_execute.h文件中的execute函数实现中,zend_execute_data类型的execute_data变量贯穿整个中间代码的执行过程,其在调用时并没有直接使用execute_data,而是使用EX宏代替,其定义在Zend/zend_compile.h文件中,如下: #define EX(element) execute_data.element 因此我们在execute函数或在opcode的实现函数中会看到EX(fbc),EX(object)等宏调用,它们是调用函数局部变量execute_data的元素:execute_data.fbc和execute_data.object。execute_data不仅仅只有fbc、object等元素,它包含了执行过程中的中间代码,上一次执行的函数,函数执行的当前作用域,类等信息。其结构如下: typedef struct _zend_execute_data zend_execute_data;   struct _zend_execute_data { struct _zend_op *opline; zend_function_state function_state; zend_function *fbc; /* Function Being Called */ zend_class_entry *called_scope; zend_op_array *op_array; /* 当前执行的中间代码 */ zval *object; union _temp_variable *Ts; zval ***CVs; HashTable *symbol_table; /* 符号表 */ struct _zend_execute_data *prev_execute_data; /* 前一条中间代码执行的环境*/ zval *old_error_reporting; zend_bool nested; zval **original_return_value; /* */ zend_class_entry *current_scope; zend_class_entry *current_called_scope; zval *current_this; zval *current_object; struct _zend_op *call_opline; }; 在前面的中间代码执行过程中有介绍:中间代码的执行最终是通过EX(opline)->handler(execute_data TSRMLS_CC)来调用最终的中间代码程序。在这里会将主管中间代码执行的execute函数中初始化好的execture_data传递给执行程序。 zend_execute_data结构体部分字段说明如下: - opline字段:struct _zend_op类型,当前执行的中间代码 - op_array字段: zend_op_array类型,当前执行的中间代码队列 - fbc字段:zend_function类型,已调用的函数 - called_scope字段:zend_class_entry类型,当前调用对象作用域,常用操作是EX(called_scope) = Z_OBJCE_P(EX(object)),即将刚刚调用的对象赋值给它。 - symbol_table字段: 符号表,存放局部变量,这在前面的[<< 第六节 变量的生命周期 变量的作用域 >>](#)有过说明。在execute_data初始时,EX(symbol_table) = EG(active_symbol_table); - prev_execute_data字段:前一条中间代码执行的中间数据,用于函数调用等操作的运行环境恢复。 在execute函数中初始化时,会调用zend_vm_stack_alloc函数分配内存。这是一个栈的分配操作,对于一段PHP代码的上下文环境,它存在于这样一个分配的空间作放置中间数据用,并作为栈顶元素。当有其它上下文环境的切换(如函数调用),此时会有一个新的元素生成,上一个上下文环境会被新的元素压下去,新的上下文环境所在的元素作为栈顶元素存在。 在zend_vm_stack_alloc函数中我们可以看到一些PHP内核中的优化。比如在分配时,这里会存在一个最小分配单元,在zend_vm_stack_extend函数中,分配的最小单位是ZEND_VM_STACK_PAGE_SIZE((64 * 1024) - 64),这样可以在一定范围内控制内存碎片的大小。又比如判断栈元素是否为空,在PHP5.3.1之前版本(如5.3.0)是通过第四个元素elelments与top的位置比较来实现,而从PHP5.3.1版本开始,struct _zend_vm_stack结构就没有第四个元素,直接通过在当前地址上增加整个结构体的长度与top的地址比较实现。两个版本结构代码及比较代码如下: // PHP5.3.0 struct _zend_vm_stack { void **top; void **end; zend_vm_stack prev; void *elements[1]; };   if (UNEXPECTED(EG(argument_stack)->top == EG(argument_stack)->elements)) { }   // PHP5.3.1 struct _zend_vm_stack { void **top; void **end; zend_vm_stack prev; };   if (UNEXPECTED(EG(argument_stack)->top == ZEND_VM_STACK_ELEMETS(EG(argument_stack)))) { }   #define ZEND_VM_STACK_ELEMETS(stack) \ ((void**)(((char*)(stack)) + ZEND_MM_ALIGNED_SIZE(sizeof(struct _zend_vm_stack)))) 当一个上下文环境结束其生命周期后,如果回收这段内存呢?还是以函数为例,我们在前面的函数章节中<< [函数的返回](#) >>中我们知道每个函数都会有一个函数返回,即使没有在函数的实现中定义,也会默认返回一个NULL。以ZEND_RETURN_SPEC_CONST_HANDLER实现为例,在函数的返回最后都会调用一个函数**zend_leave_helper_SPEC**。 在zend_leave_helper_SPEC函数中,对于执行过程中的函数处理有几个关键点: - 上下文环境的切换:这里的关键代码是:EG(current_execute_data) = EX(prev_execute_data);。EX(prev_execute_data)用于保留当前函数调用前的上下文环境,从而达到恢复和切换的目的。 - 当前上下文环境所占用内存空间的释放:这里的关键代码是:zend_vm_stack_free(execute_data TSRMLS_CC);。zend_vm_stack_free函数的实现存在于Zend/zend_execute.h文件,它的作用就是释放栈元素所占用的内存。 - 返回到之前的中间代码执行路径中:这里的关键代码是:ZEND_VM_LEAVE();。我们从zend_vm_execute.h文件的开始部分就知道ZEND_VM_LEAVE宏的效果是返回3。在执行中间代码的while循环当中,当ret=3时,这个执行过程就会恢复之前上下文环境,继续执行。
';