作者归档:admin

PHP的语法分析器-Bison入门

Bison是一种通用目的的分析器生成器。它将LALR(1)上下文无关文法的描述转化成分析该文法的C程序。 使用它可以生成解释器,编译器,协议实现等多种程序。 Bison向上兼容Yacc,所有书写正确的Yacc语法都应该可以不加修改地在Bison下工作。 它不但与Yacc兼容还具有许多Yacc不具备的特性。

Bison分析器文件是定义了名为yyparse并且实现了某个语法的函数的C代码。 这个函数并不是一个可以完成所有的语法分析任务的C程序。 除此这外我们还必须提供额外的一些函数: 如词法分析器、分析器报告错误时调用的错误报告函数等等。 我们知道一个完整的C程序必须以名为main的函数开头,如果我们要生成一个可执行文件,并且要运行语法解析器, 那么我们就需要有main函数,并且在某个地方直接或间接调用yyparse,否则语法分析器永远都不会运行。

先看下bison的示例:逆波兰记号计算器

%{
#define YYSTYPE double
#include <stdio.h>
#include <math.h>
#include <ctype.h>
int yylex (void);
void yyerror (char const *);
%}

%token NUM

%%
input:    /* empty */
     | input line
    ;

line:     '\n'
    | exp '\n'      { printf ("\t%.10g\n", $1); }
;

exp:      NUM           { $$ = $1;           }
   | exp exp '+'   { $$ = $1 + $2;      }
    | exp exp '-'   { $$ = $1 - $2;      }
    | exp exp '*'   { $$ = $1 * $2;      }
    | exp exp '/'   { $$ = $1 / $2;      }
     /* Exponentiation */
    | exp exp '^'   { $$ = pow($1, $2); }
    /* Unary minus    */
    | exp 'n'       { $$ = -$1;          }
;
%%

#include <ctype.h>

int yylex (void) {
       int c;

/* Skip white space.  */
       while ((c = getchar ()) == ' ' || c == '\t') ;

/* Process numbers.  */
       if (c == '.' || isdigit (c)) {
       ungetc (c, stdin);
       scanf ("%lf", &yylval);
       return NUM;
     }

       /* Return end-of-input.  */
       if (c == EOF) return 0;

       /* Return a single char.  */
       return c;
}

void yyerror (char const *s) {
    fprintf (stderr, "%s\n", s);
}

int main (void) {
    return yyparse ();
}

我们先看下运行的效果:

bison demo.y
gcc -o test -lm test.tab.c
chmod +x test
./test

NOTE gcc命令需要添加-lm参数。因为头文件仅对接口进行描述,但头文件不是负责进行符号解析的实体。此时需要告诉编译器应该使用哪个函数库来完成对符号的解析。  GCC的命令参数中,-l参数就是用来指定程序要链接的库,-l参数紧接着就是库名,这里我们在-l后面接的是m,即数学库,他的库名是m,他的库文件名是libm.so。

这是一个逆波兰记号计算器的示例,在命令行中输入 3 7 + 回车,输出10

一般来说,使用Bison设计语言的流程,从语法描述到编写一个编译器或者解释器,有三个步骤:

  • 以Bison可识别的格式正式地描述语法。对每一个语法规则,描述当这个规则被识别时相应的执行动作,动作由C语句序列。即我们在示例中看到的%%和%%这间的内容。
  • 描述编写一个词法分析器处理输入并将记号传递给语法分析器(即yylex函数一定要存在)。词法分析器既可是手工编写的C代码, 也可以由lex产生,后面我们会讨论如何将re2c与bison结合使用。上面的示例中是直接手工编写C代码实现一个命令行读取内容的词法分析器。
  • 编写一个调用Bison产生的分析器的控制函数,在示例中是main函数直接调用。编写错误报告函数(即yyerror函数)。

将这些源代码转换成可执行程序,需要按以下步骤进行:

  • 按语法运行Bison产生分析器。对应示例中的命令,bison demo.y
  • 同其它源代码一样编译Bison输出的代码,链接目标文件以产生最终的产品。即对应示例中的命令 gcc -o test -lm test.tab.c

前端控制器

前端控制器

表现层的请求处理机制需要支持每个用户多个请求,我们可以以集中式或分散式的方式管理这些请求。

如果以分散的方式进行管理可能会导致如下的一些问题:

  • 每个请求都有一个共同的操作,分散处理可能会导致代码的重复。
  • 可能会导致视图导航和视图内容的耦合。
  • 分散处理可能会带来更高的维护成本。

如果我们采用集中的方式进行管理,则可以对安全认证、国际化等操作统一处理,同时也可以在一个集中点处理站点的某些操作(如日志记录,站点全局访问控制等),
并且可以在一个地方处理逻辑在多个视图中重复显示。如此我们有了选择前端控制器的理由。

前端控制器建议集中处理所有请求的处理,然而它并没有限制系统中请求处理器的个数,对于不同的服务,完全可以提供不同的处理器。
这与集中式的管理并不矛盾,其实集中只是一种相对的集中,从而达到解决分散式所产生的问题的目的,
任何一种模式只是为解决一些应用场景的特定问题。

运行机制
一个前端控制器其本体包括两部分:一个分发中心(或叫调度处理程序)和一个command(或动作)层次结构。
当一个请求到达服务器,前端控制器接收此请求,从其请求信息中获取足够的内容并决定下一步操作,然后委托给某个command,执行操作。

分发中心可以是一个类或几个类,它没有页面输出,它的作用就是决定最终运行哪一个command。
这里简单点,可以直接根据参数约定,动态识别并执行。
这种简单方法做到了开闭原则,可以在不修改分发中心的前提下添加新的command。

例子
前端控制器在PHP的框架中基本上都会出现,在实现方式上,前端控制器大多采用Apache的url_rewrite模块,
以在.htaccess中重写规则,将所有请求都转发到index.php文件处理。

如PHP的YII框架,Application即YII framework的前端处理器,它是整个请求过程的运行环境。
Application 接收用户的请求并把它分发到合适的控制器作进一步处理。
其一个访问到动作被执行,简单过程如下:

  • 用户访问 Web 服务器,假设其访问地址为index.php?r=post/show,则其入口脚本 index.php 会处理该请求。
  • index.php建立一个应用实例并运行(run方法,在运行前有若干组件加载,初始化操作)。
  • 在应用从一个叫 HTTPRequest 的应用组件获取此次请求的详情。
  • 在urlManager 的组件的帮助下,根据前面获取的请求详情确定用户要请求的控制器和动作,分别对应CController和CInlineAction。
  • 应用建立一个被请求的控制器实例来进一步处理用户请求,控制器确定由它的actionShow 方法来处理 show 动作。
  • 然后它创建并运行和该动作相关的过滤器(CController->runActionWithFilters( )),如果过滤器允许的话,动作被执行,即CController->runAction() ==> CInlineAction->run()。

对于前端控制器,Java体系中的Struts框架以XML配置方式体现,在strut.xml配置动作,在web.xml中配置过滤器。

  • 前端页面提交以“.do”结尾的请求。
  • FilterDispatcher接收请求并调用Action处理该请求。
  • Action处理完毕返回一个逻辑视图。
  • FilterDispatcher根据Action返回逻辑视图创建物理视图
  • 将物理视图返回给页面。

当然我们也可以在一个PHP文件中实现整个前端控制,直接约定命名规范,根据传递进来的参数动态加载处理器,处理方法,视图等。

PHP的词法解析器:re2c

re2c是一个扫描器制作工具,可以创建非常快速灵活的扫描器。它可以产生高效代码,基于C语言,可以支持C/C++代码。 与其它类似的扫描器不同,它偏重于为正则表达式产生高效代码(和他的名字一样)。因此,这比传统的词法分析器有更广泛的应用范围。 你可以在sourceforge.net获取源码。

PHP在最开始的词法解析器是使用的是flex,后来PHP的改为使用re2c。 在源码目录下的Zend/zend_language_scanner.l 文件是re2c的规则文件, 如果需要修改该规则文件需要安装re2c才能重新编译。

re2c调用方式:

re2c [-bdefFghisuvVw1] [-o output] [-c [-t header]] file

我们通过一个简单的例子来看下re2c。如下是一个简单的扫描器,它的作用是判断所给的字符串是数字/小写字母/大小字母。 当然,这里没有做一些输入错误判断等异常操作处理。示例如下:

#include <stdio.h>

char *scan(char *p){
#define YYCTYPE char
#define YYCURSOR p
#define YYLIMIT p
#define YYMARKER q
#define YYFILL(n)
    /*!re2c
      [0-9]+ {return "number";}
      [a-z]+ {return "lower";}
      [A-Z]+ {return "upper";}
      [^] {return "unkown";}
     */
}

int main(int argc, char* argv[])
{
    printf("%s\n", scan(argv[1]));

    return 0;
}

如果你是在ubuntu环境下,可以执行下面的命令生成可执行文件。

re2c -o a.c a.l
gcc a.c -o a
chmod +x a
./a 1000

此时程序会输出number。

我们解释一下我们用到的几个re2c约定的宏。

  • YYCTYPE 用于保存输入符号的类型,通常为char型和unsigned char型
  • YYCURSOR 指向当前输入标记, -当开始时,它指向当前标记的第一个字符,当结束时,它指向下一个标记的第一个字符
  • YYFILL(n) 当生成的代码需要重新加载缓存的标记时,则会调用YYFILL(n)。
  • YYLIMIT 缓存的最后一个字符,生成的代码会反复比较YYCURSOR和YYLIMIT,以确定是否需要重新填充缓冲区。

参照如上几个标识的说明,可以较清楚的理解生成的a.c文件,当然,re2c不会仅仅只有上面代码所显示的标记, 这只是一个简单示例,更多的标识说明和帮助信息请移步 re2c帮助文档http://re2c.org/manual.html

更多编译器相关算法: Compiler Algorithms