熱點推薦:
您现在的位置: 電腦知識網 >> 編程 >> PHP編程 >> 正文

PHP語法分析器:RE2C && BISON 總結

2013-11-15 12:22:52  來源: PHP編程 
   在這之前我曾經嘗試過一個項目就是將我們的PHP代碼自動生成so擴展

  編譯到PHP中我叫它 phptoc

  但是由於各種原因暫停了此項目

  寫這篇文章一是因為這方面資料太少二是把自己的收獲總結下來以便以後參考如果能明白PHP語法分析

  那對PHP源碼的研究會更上一層樓地 ^^…

  我盡可能寫的通俗易懂些

  這個項目思路源於facebook的開源項目 HipHop

  其實我對這個項目的性能提高%%持懷疑態度從根本來講如果PHP用到APC緩存它的性能是否低

  於HipHop我還沒有做測試不敢斷言

  PHPtoc我只是想把C程序員解放出來希望能達到讓PHPer用PHP代碼就可以寫出接近於PHP擴展性能的一個擴展

  它的流程如下讀取PHP文件解析PHP代碼對其進行語法分析器生成對應的ZendAPI編譯成擴展

  進入正題

  這裡最難的就是語法分析器了大家應該都知道PHP也有自己的語法分析器現在版本用到的是rec 和 Bison

  所以我自然也用到了這個組合

  如果要用PHP的語法分析器就不太現實了因為需要修改zend_language_parsery和 zend_language_scannerl 並重新編譯這難度大不說還可能影響PHP自身

  所以決定重新寫一套自己的語法分析規則這個功能就等於是重寫了PHP的語法分析器當然會捨棄一些不常用的

  rec && yacc/bison通過引用自己的對應文件然後將他們統一編譯成一個*c文件最後再gcc編譯就會生

  成我們自己的程序所以說他們從根本來講不是語法分析程序他們只是將我們的規則生成一個獨立的c文

  件這個c文件才是真正的我們需要的語法分析程序我更願意叫它 語法生成器如下圖

  注圖中ac是 掃描器生成的最終代碼

  rec掃描器假如我們寫的掃描規則文件叫scannerl它會將我們寫的PHP文件內容進行掃描然後根據

  我們寫的規則生成不同的token傳遞給parse

  我們寫的(f)lex語法規則比如我們叫他Parsey

  會通過 yacc/bison編譯成一個parsetabhparsetabc的文件parse根據不同的token進行不同的操作

  比如我們PHP代碼是 echo ″;

  掃描其中有一個規則

  echo {

  return T_ECHO;
    }
   掃描器函數scan會拿到echo ″字符串它對這一段代碼進行循環如果發現有echo字符串那麼它就作為關鍵字返回tokenT_ECHO

  parsey和scannerl會分別生成兩個c文件scannerc和parsetabc用gcc編譯到一起就成了

  下面會具體的說一說

  感興趣的可以去看看我也翻譯了一個中文版本

  還麼有結束稍後我會放上來

  rec提供了一些宏接口方面我們使用我簡單做了翻譯英語水平不好可能有誤需要原文的可以去上面那個地址查看

  接口代碼
   不像其他的掃描器程序rec 不會生成完整的掃描器用戶必須提供一些接口代碼用戶必須定義下面的宏或者是其他相應的配置
   YYCONDTYPE
    用c 模式你可以使用to參數用來生成一個文件使用包含枚舉類型的作為條件每個值都會在規則集合裡面作為條件來使用
   YYCTYPE
    用來維持一個輸入符號通常是 char 或者unsigned char
   YYCTXMARKER
    *YYCTYPE類型的表達式生成的代碼回溯信息的上下文會保存在 YYCTXMARKER如果掃描器規則需要使用上下文中的一個或多個正則表達式則用戶需要定義這個宏
   YYCURSOR
    *YYCTYPE類型的表達式指針指向當前輸入的符號生成的代碼作為符號相匹配在開始的地方YYCURSOR假定指向當前token的第一個字符結束時YYCURSOR將會指向下一個token的第一個字符
   YYDEBUG(statecurrent)
    這個只有指定d標示符的時候才會需要調用用戶定義的函數時可以非常容易的調試生成的代碼
    這個函數應該有以下簽名void YYDEBUG(int statechar current)第一個參數接受 state 默認值為第二個參數接受輸入的當前位置
   YYFILL(n)
    當緩沖器需要填充的時候生成的代碼將會調用YYFILL(n)至少提供n個字符YYFILL(n)將會根據需要調整YYCURSORYYLIMITYYMARKER 和 YYCTXMARKER注意在典型的程序語言當中n等於最長的關鍵詞的長度加一用戶可以在/*!max:rec*/一次定義YYMAXFILL來指定最長長度如果使用了YYMAXFILL將會在/*!rec*/之後調用一次阻塞
   YYGETCONDITION()
    如果使用了c模式這個定義將會在掃描器代碼之前獲取條件集這個值必須初始化為枚舉YYCONDTYPE的類型
   YYGETSTATE()
    如果f模式指定了用戶就需要定義這個宏如果這樣掃描器在開始時為了獲取保存的狀態生成的代碼將會調用YYGETSTATE()YYGETSTATE()必須返回一個帶符號的整數這個值如果是告訴掃描器這是第一次執行否則這個值等於以前YYSETSTATE(s) 保存的狀態否則掃描器將會恢復操作之後立即調用YYFILL(n)
   YYLIMIT
    表達式的類型 *YYCTYPE 標記緩沖器的結尾(YYLIMIT()是緩沖區的最後一個字符)生成的代碼將會不斷的比較YYCORSUR 和 YYLIMIT 以決定 什麼時候填充緩沖區
   YYSETCONDITION(c)
    這個宏用來在轉換規則中設置條件它只會在指定c模式 和 使用轉換規則時有用
   YYSETSTATE(s)
    用戶只需要在指定f模式時定義這個宏如果是這樣生成的代碼將會在YYFILL(n)之前調用YYSETSTATE(s)YYSETSTATE的參數是一個有符號整型被稱為唯一的標示特定的YYFILL(n)實例
   YYMARKER
    類型為*YYCTYPE的表達式生成的代碼保存回溯信息到YYMARKER一些簡單的掃描器可能用不到
   掃描器顧名思義就是對文件掃描找出關鍵代碼來

  掃描器文件結構

  /* #include 文件*/
   /*宏定義*/
   //掃描函數
   int scan(char *p){
   /*掃描器規則區*/
   }
   //執行scan掃描函數返回token到yacc/bison中
   int yylex(){
           int token;
           char *p=YYCURSOR;//YYCURSOR是一個指針指向我們的PHP文本內容
           while(token=scan(p)){//這裡會移動指針p一個一個判斷是不是我們上面定義好的scanner
                   return token;
           }
   }
   int main(int argcchar**argv){
           BEGIN(INITIAL);//
           YYCURSOR=argv[];//YYCURSOR是一個指針指向我們的PHP文本內容
           yyparse();
   }
   BEGIN 是定義的宏

  #define YYCTYPE char   //輸入符號的類型
   #define STATE(name)     yyc##name
   #define BEGIN(n)        YYSETCONDITION(STATE(n))
   #define LANG_SCNG(v)    (sc_globalsv)
   #define SCNG    LANG_SCNG
   #define YYGETCONDITION()        SCNG(yy_state)
   #define YYSETCONDITION(s)       SCNG(yy_state)=s
   yyparse函數是在yacc 中定義的

  裡面有一個關鍵宏 YYLEX

  #define YYLEX yylex()

  它會執行scaner掃描器的yylex

  可能會有點繞重新縷一縷

  在scannerl中通過調用parsey解析器函數yyparse該函數調用scannerl的yylex生成關鍵代碼tokenyylex

  將掃描器返回的

  token返回給parseyparse根據不同的token執行不同的代碼

  舉例

  scannerl
   #include scannerh
   #include parsetabh
   int scan(char *p){
   /*!rec
        <INITIAL><?php([ /t]|{NEWLINE})? {
                   BEGIN(ST_IN_SCRIPTING);
                   return T_OPEN_TAG;
           }
       echo {

  return T_ECHO;
           }
       []+ {
                   return T_LNUMBER;
           }
   */
   }
   int yylex(){
             int c;

  //       return T_STRING;
           int token;
           char *p=YYCURSOR;
           while(token=scan(p)){
                   return token;
           }
   }

  int main (int argcchar ** argv){
           BEGIN(INITIAL);//初始化
           YYCURSOR=argv[];//將用戶輸入的字符串放到YYCURSOR
           yyparse();//yyparse() 》yylex()》yyparse()
           return ;
   }
   這樣一個簡單的掃描器就做成了那解析器呢?

  解析器我用的是flex和bison

  關於flex的文件結構

  %{
   /*
     C代碼段將逐字拷貝到lex編譯後產生的C源文件中
     可以定義一些全局變量數組函數例程等
   */
   #include
   #include scannerh
   extern int yylex();//它在scannerl中定義的
   void yyerror(char *);
   # define YYPARSE_PARAM tsrm_ls
   # define YYLEX_PARAM tsrm_ls
   %}
   {定義段也就是token定義的地方}
   //這就是關鍵  token程序是根據這是做switch的
   %token T_OPEN_TAG
   %token T_ECHO
   %token T_LNUMBER
   %%
   {規則段}
   start:
            T_OPEN_TAG{printf(start/n); }
           |start statement
   ;
   statement:
   T_ECHO expr {printf(echo :%s/n$)}
   ;
   expr:
           T_LNUMBER {$$=$;}
   %%
   {用戶代碼段}
   void yyerror(char *msg){
           printf(error:%s/nmsg);
   }
   在規則段中start是開始的地方如果 scan識別到PHP開始標簽就會返回T_OPEN_TAG然後執行括號的代碼輸出start

  在scannerl中調用scan的是個while循環所以它會檢查到php代碼的末尾

  yyparse會根據scan返回的標記做switch然後goto到相應的代碼比如 yyparsey發現當前的token是T_OPEN_TAG

  它會通過宏 #line 映射到 parsey所對應 T_OPEN_TAG的位置然後執行

  那TOKEN返回給yyparse之後做了什麼呢?

  為了能直觀一些我用gdb跟蹤

  

  這個時候yychar是是什麼?

  

  是bison自動生成的枚舉類型數據

  繼續

  YYTRANSLATE宏接受yychar然後返回所對應的值

  #define YYTRANSLATE(YYX)                                                /
     ((unsigned int) (YYX) <= YYMAXUTOK ? yytranslate[YYX] : YYUNDEFTOK)

  /* YYTRANSLATE[YYLEX] Bison symbol number corresponding to YYLEX  */
   static const yytype_uint yytranslate[] =
   {
                                                      
                                                      
                                                      
                                                     
                                                   
                                                     
                                                     
                                                      
                                                      
                                                      
                                                      
                                                      
                                                    
                                                      
                                                      
                                                      
                                                      
                                                      
                                                      
                                                      
                                                      
                                                      
                                                      
                                                      
                                                      
                                                      
                                                 
                            
   };
   yyparse拿到這個值不斷地translate

  bison會生成很多用來映射的數組將最終的translate保存到yyn

  這樣bison就能找到token所對應的代碼

  switch (yyn)
       {
           case :

  /* Line of yaccc  */
   #line parsey
       {printf(start/n); ;}
       break;
   這樣不斷循環生成token逐條執行然後解析成所對應的zend 函數等生成對應的op保存在哈希表中這些不是本文的重點


From:http://tw.wingwit.com/Article/program/PHP/201311/20842.html
    Copyright © 2005-2013 電腦知識網 Computer Knowledge   All rights reserved.