标签归档:深入理解PHP内核

PHP的$this变量

手册上的一个有意思的小示例。

http://www.php.net/manual/zh/language.variables.basics.php

	$this = 'text'; // error
	$name = 'this';
	$$name = 'text'; // sets $this to 'text'
	echo $$name;

在PHP的词法分析时,$this变量是符合其规则的,在语法解析生成中间代码时,PHP内核会根据变量类型在生成赋值的中间代码时判断是否为$this变量,如果是则报错。这里为什么要报错呢?因为this作为一个特殊的变量,在对象的成员方法等调用初始化时会将this变量添加到活动符号表。

在类的成员方法里面,可以用 ->(对象运算符):$this->property(其中 property 是该属性名)这种方式来访问非静态属性。

当一个方法在类定义内部被调用时,有一个可用的伪变量 $this。$this 是一个到主叫对象的引用(通常是该方法所从属的对象,但如果是从第二个对象静态调用时也可能是另一个对象)。

在词法分析、语法分析并生成中间代码时,$this作为一个特殊的变量存在,特别是在生成中间代码时,代码中充斥着对于this的特殊处理。这些都是为后面的运行做准备,如识别标记出某处使用this变量,在存储opcode的zend_op_array结构体中专门有一个变量this_var标识是否有this变量。一个函数或一个类方法都会生成一个新的zend_op_array,在生成中间代码时,判断当前变量是否为this变量。

this变量在执行过程中会有两种存在状态,一种是全局传递的状态,存储在EG(This),一种是当前作用域状态,以this变量存储在EG(active_symbol_table)(当前执行环境的活动符号表)。
在我们执行一个 op_array 时,比如一个对象的方法,PHP内核会给这个 op_array 生成一个 zendexecutedata ,在生成初始化时,EG(This) 会添加到EG(active_symbol_table) 。
在方法调用过程中,如果有用到this变量,则会直接取EG(active_symbol_table)的值。

那么一个对象中的EG(This)在哪里初始化呢?
就EG(This)变量本身来说,在我们初始化PHP的执行环境时,它和其它全局变量(如EG(scope)等)一样都会被初始化为NULL。
对于一个对象来说,当我们创建了一个对象,调用时,PHP内核会将当前获得的对象直接赋值给EG(This),而这个当前获得的对象是在通过new操作生成对象时创建的对象本身。

如下这个简单示例:

	class Foo {
	     public $var = 10;
 
	     function t() {
	          echo $this->var;     
	     }
 
	     function t2() {
	     	echo 33;
		}
	}
 
	$foo = new Foo();
	$foo->t();

其主程序流程生成的中间代码如下:

        function name:  (null)
	number of ops:  8
	compiled vars:  !0 = $foo
	line     # *  op                           fetch          ext  return  operands
	---------------------------------------------------------------------------------
	   2     0  >   NOP                                                      
	  15     1      ZEND_FETCH_CLASS                              4  :1      'Foo'
	         2      NEW                                              $2      :1
	         3      DO_FCALL_BY_NAME                              0          
	         4      ASSIGN                                                   !0, $2
	  16     5      ZEND_INIT_METHOD_CALL                                    !0, 't'
	         6      DO_FCALL_BY_NAME                              0          
	         7    > RETURN                                                   1

this变量原始的对象值出生在 opcode NEW,经过了赋值(ASSIGN)后,在方法初始化时,将变量本身传递给执行环境的调用者,调用者又在执行调用(DO_FCALL_BY_NAME)时将变量传递给EG(This),当执行这个方法的op_array时,初始化当前作用域的环境(zend_execute_data)时,会将EG(This)作为$this变量添加到活动符号表,后续方法中的$this变量的使用就会直接取符号表的变量。

re2c中文手册

re2c中文手册

在PHP的实现过程中,包括PHP语言本身的词法分析,一共有多达8处的地方使用了re2c,如果我们常用的时间函数、pdo扩展等。对re2c的了解更能促进我们进PHP内核实现的认知。

本手册是re2c官网的manual.html文件翻译稿,仅适用于对re2c的初步了解,更多的资料见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)宏
  2. -f参数禁止了yych和yyaccept的声明。因此用户必须声明这些,并且必须能够保存和恢复他们。在example/push.re文件的示例中,这些都被声明为C++类的字段,因此他们不再需要明确的保存或恢复。对于C语言来说,我们可以通过宏,以参数传递的方式从结构体中获取这些字段。或者,可以将他们声明为局部变量,当它决定返回并将之作为函数的一个项保存在 YYFILL(n)中。此外, 使用YYFILL(n)保存的效率更高,因为可以无条件的调用YYSETSTATE(state)。然而,YYFILL(n) 并不能将state作为参数,因此,我们必须通过YYSETSTATE(state)将state保存到局部变量中。
  3. 如果需要更多的输入,需要修改YYFILL(n) ,使之可以从调用它的函数处返回。
  4. 修改调用者的逻辑,使其在需要更多的输入时做出相应的应答。
  5. 生成的代码中将包含一个选择逻辑块,这个选择逻辑会被用来通过跳转到相应的YYFILL(n)调用处,以恢复最后的状态。这个代码块会在第一个 “/*!re2c */”块收尾的地方自动生成。通过放置 “/*!getstate:re2c */”注释,可能会触发YYGETSTATE() 的生成操作。这对于被包含在循环中的扫描器非常有用。

请查看 examples/push.re文件中的推模式示例扫描器。它生成的代码可以通过”state:abort”和”state:nextlabel”调整。

Scanner With Condition Support 可判断条件的扫描

当使用-c参数时,你可以在正则表达式之前优先一系统的条件名。在这种情况下,re2c会针对每个条件生成扫描代码块。这些代码块都有它自己的前置条件,这此前置条件都是通过接口定义YYGETCONDITON实现,并且必须为YYCONDTYPE类型。

其中有两个特别的类型,一个是‘*’,它表示满足所有条件;另一个是空条件,它提供一个没有扫描内容的代码块,这意味着不需要任何正则表达式。这个特殊的块始终有一个固定的枚举值0。这些特殊的规则可以被用来初始化一个扫描器。这些特殊的规则并不是必须的,但是有时可以用它来声明一些没有初始化的状态。

非空规则允许指定新的条件,这些条件将导致规则的变化。它会生成定义的YYSETCONDTITION,除此之外再无其它。

还有另一种特殊的规则,它允许在所有的有规则和没有规则代码前添加代码。例如,它可以用来保存扫描的字符串的长度。这个特殊的规则以感叹号开始,后面可以接条件 <! condition, … > 或星号<!*>。当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;’如果一段代码必须放在所有的规则之前,你可以使用<! 伪规则。

    <condition-list> regular-expression { C/C++ code }
    <condition-list> regular-expression := C/C++ code
    <condition-list> regular-expression => condition { C/C++ code }
    <condition-list> regular-expression => condition := C/C++ code
    <condition-list> 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
    <!condition-list> { C/C++ code }
    <!condition-list> := C/C++ code
    <!*> { C/C++ code }
    <!*> := C/C++ code

命名定义格式如下:

name = regular-expression;

如果使用了-F 模式,可以使用如下命名定义方法:

name regular-expression&lt; /pre&gt;"re2c"开始的命名定义配置如下所示:
<pre lang="c">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)
特别鸣谢:老婆大人的亲自指点和崽崽的听话。

PHP的ticks机制

PHP的ticks机制

要过年了,在年前完成这篇文章,如果有缘可以看到,祝福看到的朋友新年快乐,在新的一年里,万事顺意!

按今年的计划每个月至少有两篇文章,而一月份因为各种理由而只有一篇2012的总结,无论什么原因,总归是不对的。这篇算是补上的,也作为今年的开始。

回正题,今天要研究的是PHP的ticks机制。

PHP提供declare关键字和ticks关键字来声明ticks机制。如:declare(ticks = N); 这表示:在当前scope内,每执行N句internal statements(opcodes),就会中断当前的业务语句,去执行通过register_tick_function注册的函数(如果存在的话),然后再继续之前的代码。需要注意的是这里的N是指的PHP的一些OPCODE,而OPCODE与我们见到的PHP语句却不是一一对应的。

最开始我以为PHP内核是在编译时记录是否有ticks机制,在真正执行中间代码时插入判断代码,实现此机制。但是事实上却不是这样滴。

看PHP代码示例1:

    $name = "phppan";
    echo $name;
    class Tipi {
        public function test() {
            echo "test";
        }
    }
    function f_tipi() {
    }

如上代码包括了我们常见的几种语句,赋值,输出,定义类,定义函数。通常我们用VLD查看PHP生成的中间代码,上面的代码通过 php -dvld.active=1 t.php 我们会看到 ECHO、ASSIGN、NOP等中间代码。

现在我们在示例1的代码上添加上ticks机制。如PHP代码示例2:

    declare(ticks=1);
    $name = "phppan";
    echo $name;
    class Tipi {
        public function test() {
            echo "test";
        }
    }
    function f_tipi() {
    }

示例2与示例1相比也就是多了第一条语句: declare(ticks=1); 如果我们此时再次通过VLD查看中间代码,会发现在每个中间代码的后面都多了一句中间代码:TICKS

是否因为ticks=1的原因而在每个中间代码的后面添加了TICKS?将declare(ticks=1);换成declare(ticks=100);,再次VLD,结果没有变化。从以上的结果可以看出,PHP内核在语法分析过程中实现了ticks机制。

从实现过程来说定义ticks机制分为两个过程:一个是定义是否需要执行ticks或者说声明ticks机制,另一个实现在声明了ticks机制的情况下控制语句的执行。

声明ticks机制过程

声明的过程就是调用declare(ticks = N); 在语法分析时根据declare关键字和参数中的ticks关键字来声明ticks机制。通过zend_compile.c文件中的zend_do_declare_begin、declare_statement、zend_do_declare_end三个函数来编译声明ticks机制。在declare_statement函数中我们可以看到:declare除了可以声明ticks之外,还可以声明encoding,这在代码里面就是一个if else的判断。

ticks机制的声明仅在编译过程有用,它为后面的声明控制语句服务。其编译过程中的全局变量为:CG(declarables)。这是一个结构体,它仅有一个成员:ticks。当然后面应该还会有更多的成员出现。

声明控制语句

示例1和示例2已经充分说明在每条语句的语法分析时,会根据是否声明了ticks机制来添加TICKS中间代码,其实现在于每条语句在语法解析时都会添加一条函数调用:zend_do_ticks。从zend_language_parser.y文件中可以看出:zend_do_ticks函数添加在类定义语句,函数定义语句和常规语句的后面。 zend_compile.c文件中的zend_do_ticks函数会根据前面提到的 CG(declarables).ticks 来判断是否生成 ZEND_TICKS 中间代码(在VLD中看到的中间代码都是没有ZEND开头)。

除了声明ticks机制,还有执行。执行过程中关键的变量是在声明时的ticks=N。其实这里的N可以换个角度去理解:ticks指定的数字是指执行了多少次TICKS语句。在TICKS中间代码的执行函数ZEND_TICKS_SPEC_CONST_HANDLER中,会统计执行当前函数的次数,存储变量为EG(ticks_count)。当达到当初声明的界限,就会调用一次所有通过register_tick_function注册的函数,并计数清零。

与当初自己设想的实现相比,PHP内核对ticks机制的实现满足了功能单一原则和松耦合原则。将ticks机制作为一个中间代码添加到整个中间代码的执行体系中,包括状态的转移,函数的切换这些都是直接使用原有的机制。

ticks机制的应用场景

手册上说:Ticks 很适合用来做调试,以及实现简单的多任务,后台 I/O 和很多其它任务。

在调试过程中,对于定位一段特定代码中速度慢的语句比较有用,我们可以每执行两条低级语句就记录一次时间。虽然这个过程也可以用其它方法完成,但用 tick 更方便也更容易实现。

PCNTL也使用ticks机制来作为信号处理机制(signal handle callback mechanism),可以最小程度地降低处理异步事件时的负载。这里的关键在于PCNTL扩展的模块初始化函数(PHP_MINIT_FUNCTION(pcntl))。在此模块做模块初始化时,它会调用: php_add_tick_function(pcntl_signal_dispatch);将pcntl的分发执行函数添加到ticks机制的调用函数中去,从而当ticks触发时就会调用PCNTL扩展函数中指定的所有方法。