Lex 和 Yacc 是 UNIX 两个非常重要的、功能强大的工具。如果能够熟练的熟练掌握 Lex 和 Yacc ,可以轻易的写出c编译器。本文将详细的讨论了这两种工具,包括常规表达式、声明、匹配模式、变量、Yacc 语法和解析器代码。最后,他把 Lex 和 Yacc 结合起来,完成一个示例。


1.lex

lex是一种词法分析器,可以识别文本中的词汇模式,模式可以用正则表达式表示。通过lex编译l文件(词法文件)就可以生产对应的c代码,然后编译连接c代码就可以生成词法分析器了。

一个l文件分为三个部分,每部分通过双百分号(%%)分割。如下所示:

... definitions ...
%%
... rules ...
%%
... subroutines ...

definitions 部分用于定义模式、C语言变量、以及包含c头文件等。 rules 部分用户定义模式对应的动作。 subroutines 部分用于定义c函数等。

一个简单的l文件例子,test.l。

%{
    int yylineno;
%}
%%
^(.*)\n    printf("%4d\t%s", ++yylineno, yytext);
%%
int main(int argc, char *argv[]) {
    yyin = fopen(argv[1], "r");
    yylex();
    fclose(yyin);
}

int yywrap(void) {
    return 1;
}
首先这个l文件在definitions 部分定义了一个int型变量yylineno用于记录行号。然后,在rules定义了一个模式动作:当遇到一行的结尾时,输出当前行号,并输出当前行内容。最后在subroutines部分定义了一个c语言main函数,读取文件,并实现yywrap并返回1表示停止解析。 ---- ###2.yacc Yacc( Yet Another Compiler Compiler)。 Yacc 的 GNU 版叫做 Bison。它是一种工具,将一种编程语言的语法翻译成针对此种语言的 Yacc 语法解析器。从上文中我们可以知道lex可以参数一系列标记,如果我们想当某个标记序列出现时执行某一动作,该怎么实现呢?Yacc该出场了。通过yacc编译y文件(语法文件)就可以产生对应的c程序了;生成的c代码通过编译链接就可以生产语法分析器了。但是,语法分析的前提是词法分析,因此我们需要lex出现输入文件并生成标记。在讲解y文件(语法文件)之前我们假设以及存在如下l文件(词法文件):
%{
#include <stdio.h>
#include "y.tab.h"
%}
%%
[0-9]+                  return NUMBER;
heat                    return TOKHEAT;
on|off                  return STATE;
target                  return TOKTARGET;
temperature             return TOKTEMPERATURE;
\n                      /* ignore end of line */;
[ \t]+                  /* ignore whitespace */;
%%
这个l文件主要是参数y文件定义的各种token,大家可以看到它的subroutines部分为空,因为该词法分析器的结果直接输出到语法分析器,因此不需要额外的函数。**下面的y文件都依赖于该l文件**。 一个y文件(语法文件)同样包含definitions、rules、subroutines三个部分,每部分同样通过双百分号(%%)分割。各个部分的作用l文件的对应部分也基本一致。 一个简单的y文件例子,test.y。
%{
#include <stdio.h>
#include <string.h>
void yyerror(const char *str);
%}
%token NUMBER TOKHEAT STATE TOKTARGET TOKTEMPERATURE
%% 
commands: /* empty */
        | commands command
        ;

command:
        heat_switch
        |
        target_set
        ;

heat_switch:
        TOKHEAT STATE
        {
                printf("\tHeat turned on or off\n");
        }
        ;

target_set:
        TOKTARGET TOKTEMPERATURE NUMBER
        {
                printf("\tTemperature set\n");
        }
        ;
%%
void yyerror(const char *str)
{
        fprintf(stderr,"error: %s\n",str);
}
int yywrap()
{
        return 1;
}  
main()
{
        yyparse();
} 
该y文件的definitions部分声明了一个函数,并定义了一系列标记(TOKEN)。然后在rules部分定义了四个模式序列对应(语句)的动作,其中commands是一个递归定义。最后在subroutines部分定义了一个c语言main函数,读取文件,并实现yywrap并返回1表示停止解析。这个y文件实现了以下功能 ``` 输入:heat on 输出:Heat turned on or off 输入:target temperature 22 输出:New temperature set! ``` ###3.lex与yacc结合 也许你已经注意到了,l文件的definitions部分往往要包含#include "y.tab.h"。而y.tab.h是yacc对y文件编译后产生的c源文见。因此y文件必须限于l文件进行编译成c源文件,然后将l文件产生的c文件和y文件产生的c文件编译连接生产语法解析器。具体步骤见图:

更多lex和yacc的资料如下:

http://epaperpress.com/lexandyacc/index.html
http://tldp.org/HOWTO/Lex-YACC-HOWTO-1.html

SAPI: Server Application Programming Interface 服务器端应用编程端口。先看一张php模块图。

从图中可以看出,各种应用都是通过对应的SAPI与php进行交互的,SAPI相当于一个接口,使得php的核心实现不用关心各个应用交互的细节。虽然通过Web服务器和命令行程序执行脚本看起来很不一样,实际上它们的工作流程是一样的。


在php的源代码sapi目录下有多种sapi的具体实现,比如cgi、cli、apache、fpm等。SAPI中最重要的一个数据结构就是_sapi_module_struct,定义在/main/SAPI.h中。

struct _sapi_module_struct {
    char *name;       //应用层的名称,比如cgi,apache等
    char *pretty_name;     //应用层更易读的名字
    int (*startup)(struct _sapi_module_struct *sapi_module);    //startup 函数指针, 当一个应用要调用PHP的时候,这个函数会被调用
    int (*shutdown)(struct _sapi_module_struct *sapi_module);     // shutdown 函数指针,
    int (*activate)(TSRMLS_D);    //active 函数指针,PHP会在每个request的时候,处理一些初始化,资源分配的事务。这部分就是activate字段要定义的
    int (*deactivate)(TSRMLS_D);    //deactivate函数指针,这个是对应与activate的函数,顾名思义,它会提供一个handler, 用来处理收尾工作
    int (*ub_write)(const char *str, unsigned int str_length TSRMLS_DC);    //这个hanlder告诉了php如何输出数据,比如cgi和fpm模式下输出数据方式肯定不一样
    void (*flush)(void *server_context);    //这个是提供给php的刷新缓存的函数指针
    struct stat *(*get_stat)(TSRMLS_D);    //这部分用来让php可以验证一个要执行脚本文件的state,从而判断文件是否据有执行权限等等
    char *(*getenv)(char *name, size_t name_len TSRMLS_DC);     //为Zend提供了一个根据name来查找环境变量的接口
    void (*sapi_error)(int type, const char *error_msg, ...);    //错误处理函数指针
    int (*header_handler)(sapi_header_struct *sapi_header, sapi_header_op_enum op, sapi_headers_struct *sapi_headers TSRMLS_DC);    // 这个函数会在我们调用PHP的header()函数的时候被调用
    int (*send_headers)(sapi_headers_struct *sapi_headers TSRMLS_DC);  //这个函数会在要真正发送header的时候被调用,一般来说,就是当有任何的输出要发送之前
    void (*send_header)(sapi_header_struct *sapi_header, void *server_context TSRMLS_DC);  //单独发送每一个header的函数指针
    int (*read_post)(char *buffer, uint count_bytes TSRMLS_DC);   //这个句函数指针明了如何获取POST的数据
    char *(*read_cookies)(TSRMLS_D);  //这个句函数指针明了如何获取COOKIE的数据
    void (*register_server_variables)(zval *track_vars_array TSRMLS_DC);    //这个函数给了一个接口,用以给$_SERVER变量中添加变量 
    void (*log_message)(char *message TSRMLS_DC);    //用来输出错误信息的函数指针
    double (*get_request_time)(TSRMLS_D);   //获得请求时间的函数指针
    void (*terminate_process)(TSRMLS_D); 
    char *php_ini_path_override;
    void (*block_interruptions)(void);
    void (*unblock_interruptions)(void);
    void (*default_post_reader)(TSRMLS_D);
    void (*treat_data)(int arg, char *str, zval *destArray TSRMLS_DC);
    char *executable_location;
    int php_ini_ignore;
    int php_ini_ignore_cwd; /* don't look for php.ini in the current directory */
    int (*get_fd)(int *fd TSRMLS_DC);
    int (*force_http_10)(TSRMLS_D);
    int (*get_target_uid)(uid_t * TSRMLS_DC);
    int (*get_target_gid)(gid_t * TSRMLS_DC);
    unsigned int (*input_filter)(int arg, char *var, char **val, unsigned int val_len, unsigned int *new_val_len TSRMLS_DC);
   void (*ini_defaults)(HashTable *configuration_hash);
   int phpinfo_as_text;
   char *ini_entries;
   const zend_function_entry *additional_functions;
   unsigned int (*input_filter_init)(TSRMLS_D);
};

不同的SAPI就是用不同的参数实例化_sapi_module_struct来实习的,下面我们分别简单分析一些cgi SAPI和cli SAPI的源代码,力求对SAPI有些更深入的理解。


1.cgi模式

cgi模式下,_sapi_module_struct的实例定义在cgi_main.c中。

static sapi_module_struct cgi_sapi_module = {
    "cgi-fcgi",                     /* name */
    "CGI/FastCGI",                  /* pretty name */
    php_cgi_startup,                /* startup */
    php_module_shutdown_wrapper,    /* shutdown */
    sapi_cgi_activate,              /* activate */
    sapi_cgi_deactivate,            /* deactivate */
    sapi_cgi_ub_write,              /* unbuffered write */
    sapi_cgi_flush,                 /* flush */
    NULL,                           /* get uid */
    sapi_cgi_getenv,                /* getenv */
    php_error,                      /* error handler */
    NULL,                           /* header handler */
    sapi_cgi_send_headers,          /* send headers handler */
    NULL,                           /* send header handler */
    sapi_cgi_read_post,             /* read POST data */
    sapi_cgi_read_cookies,          /* read Cookies */
    sapi_cgi_register_variables,    /* register server variables */
    sapi_cgi_log_message,           /* Log message */
    NULL,                           /* Get request time */
    NULL,                           /* Child terminate */
    STANDARD_SAPI_MODULE_PROPERTIES
};

下面分析 char *(*read_cookies)(TSRMLS_D) 在cgi模式下的实现:sapi_cgi_read_cookies。其中sapi_cgi_read_cookies的源码片段如下:

static char *sapi_cgi_read_cookies(TSRMLS_D)
{
    return getenv("HTTP_COOKIE");
}

可以看到,cgi模式下的char *(*read_cookies)(TSRMLS_D)最终为从环境变量中读取HTTP_COOKIE。

2.cli 模式

cgi模式下,_sapi_module_struct的实例定义在php_cli.c

static sapi_module_struct cli_sapi_module = {
    "cli",                          /* name */
    "Command Line Interface",       /* pretty name */
    php_cli_startup,                /* startup */
    php_module_shutdown_wrapper,    /* shutdown */
    NULL,                           /* activate */
    sapi_cli_deactivate,            /* deactivate */
    sapi_cli_ub_write,              /* unbuffered write */
    sapi_cli_flush,                 /* flush */
    NULL,                           /* get uid */
    NULL,                           /* getenv */
    php_error,                      /* error handler */
    sapi_cli_header_handler,        /* header handler */
    sapi_cli_send_headers,          /* send headers handler */
    sapi_cli_send_header,           /* send header handler */
    NULL,                           /* read POST data */
    sapi_cli_read_cookies,          /* read Cookies */
    sapi_cli_register_variables,    /* register server variables */
    sapi_cli_log_message,           /* Log message */
    NULL,                           /* Get request time */
    NULL,                           /* Child terminate */
    STANDARD_SAPI_MODULE_PROPERTIES
};

下面分析 char *(*read_cookies)(TSRMLS_D) 在cli模式下的实现:sapi_cli_read_cookies。其中sapi_cli_read_cookies的源码片段如下:

static char* sapi_cli_read_cookies(TSRMLS_D) 
{
    return NULL;
}

可以看到,cgi模式下的char *(*read_cookies)(TSRMLS_D)最终为直接返回NULL,因为cli模式下不存在用户cookies信息。


通过上面的cgi和cli模式下read_cookies的不同实现,可以看出sapi确实对下层php屏蔽了交互细节,当下层php核心要读取用户cookies时,只需要通过sapi_module_struct->read_cookies,而不需要关注上层应用的交互细节。

that right! 这就是SAPI的作用。