yzwalkman

——用数绘画

导航

Ogre源代码浅析——脚本及其解析(二)

      对文本文件的解析,有两种方式比较常见。第一种,文件信息有固定的格式但没有太过复杂的结构,比如Ogre中的.cfg格式文件。对这种文件,一般可以逐行读取并直接按行解析。整个过程相对比较简单。第二种,文件本身有比较复杂的结构,而且文件信息的组织要符合一定的语法规范。比如各种计算机语言的源文件以及各种脚本语言(JavaScript、Python、Lua等等)的源文件。对这种文件的解析一般要经历几个较复杂的阶段,并根据最终的解析结果得到相应的指令顺列。Ogre的脚本文件的组织和解析,其方法和复杂呈度实际上介于这两者之间。

      以.material、.program、.particle和.compositor为例,其脚本包含各种待处理的输入信息且有相同的格式规范,这与第一种文件类似;但同时这些输入信息,一般是由较简单的数据单元按照某种方式嵌套构造而成,各单元之间可以形成一种树形结构,这又明显比第一种文件要复杂。另外,以上Ogre脚本与各种脚本语言的源文件(乃至各种计算机语言源文件)的最大不同是,Ogre的脚本没有控制结构,也没有对各种数据类型的定义。实际上,Ogre的各项脚本文件更象是HML或XML等格式化文本文件。研究Ogre对其脚本文件的解析机制,对了解这类文件的解析机制,或定义自已的外部文本格式并编写相应的文件解析程序是有帮助的。

     Ogre的各种脚本都遵循共同的格式或者说共同的数据组织规则。Ogre对脚本的解析方法分为:词法分析,解析或称语义分析和编译三个过程。这与计算机语言源文件的编译过程很象,但研究其源代码后可知道,其中的“词法分析”和“语义分析过程”要比实际的计算机语言源文件编译过程简单很多(但作用有类似之处),而其第三阶段的所谓的compile(位于ScriptCompile::compile()函数中,以下代码的第6行)的作用则与脚本程序语言源文件的最后阶段的完全不同。来看Script::compiler()函数:

1     bool ScriptCompiler::compile(const String &str, const String &source, const String &group)
2     {
3         ScriptLexer lexer;
4         ScriptParser parser;
5         ConcreteNodeListPtr nodes = parser.parse(lexer.tokenize(str, source));
6         return compile(nodes, group);
7     }

       ScriptLexer是Ogre自定义的词法分析器,ScriptParser则是语法解析器,compile()函数的第一个参数,str指向的是从外部读入的整个脚本文件的文本。待解析的脚本文件首先由词法分析器的tokenize()函数进行相关处理,看一下相应源代码:

  1     ScriptTokenListPtr ScriptLexer::tokenize(const String &str, const String &source)
  2     {
  3         // State enums
  4         enum{ READY = 0, COMMENT, MULTICOMMENT, WORD, QUOTE, VAR, POSSIBLECOMMENT };
  5 
  6         // Set up some constant characters of interest
  7 #if OGRE_WCHAR_T_STRINGS
  8         const wchar_t varopener = L'$', quote = L'\"', slash = L'/', backslash = L'\\', openbrace = L'{', closebrace = L'}', colon = L':', star = L'*', cr = L'\r', lf = L'\n';
  9         wchar_t c = 0, lastc = 0;
 10 #else
 11         const wchar_t varopener = '$', quote = '\"', slash = '/', backslash = '\\', openbrace = '{', closebrace = '}', colon = ':', star = '*', cr = '\r', lf = '\n';
 12         char c = 0, lastc = 0;
 13 #endif
 14 
 15         String lexeme;
 16         uint32 line = 1, state = READY, lastQuote = 0;
 17         ScriptTokenListPtr tokens(OGRE_NEW_T(ScriptTokenList, MEMCATEGORY_GENERAL)(), SPFM_DELETE_T);
 18 
 19         // Iterate over the input
 20         String::const_iterator i = str.begin(), end = str.end();
 21         while(i != end)
 22         {
 23             lastc = c;
 24             c = *i;
 25 
 26             if(c == quote)
 27                 lastQuote = line;
 28 
 29             switch(state)
 30             {
 31             case READY:
 32                 if(c == slash && lastc == slash)
 33                 {
 34                     // Comment start, clear out the lexeme
 35                     lexeme = "";
 36                     state = COMMENT;
 37                 }
 38                 else if(c == star && lastc == slash)
 39                 {
 40                     lexeme = "";
 41                     state = MULTICOMMENT;
 42                 }
 43                 else if(c == quote)
 44                 {
 45                     // Clear out the lexeme ready to be filled with quotes!
 46                     lexeme = c;
 47                     state = QUOTE;
 48                 }
 49                 else if(c == varopener)
 50                 {
 51                     // Set up to read in a variable
 52                     lexeme = c;
 53                     state = VAR;
 54                 }
 55                 else if(isNewline(c))
 56                 {
 57                     lexeme = c;
 58                     setToken(lexeme, line, source, tokens.get());
 59                 }
 60                 else if(!isWhitespace(c))
 61                 {
 62                     lexeme = c;
 63                     if(c == slash)
 64                         state = POSSIBLECOMMENT;
 65                     else
 66                         state = WORD;
 67                 }
 68                 break;
 69             case COMMENT:
 70                 // This newline happens to be ignored automatically
 71                 if(isNewline(c))
 72                     state = READY;
 73                 break;
 74             case MULTICOMMENT:
 75                 if(c == slash && lastc == star)
 76                     state = READY;
 77                 break;
 78             case POSSIBLECOMMENT:
 79                 if(c == slash && lastc == slash)
 80                 {
 81                     lexeme = "";
 82                     state = COMMENT;
 83                     break;    
 84                 }
 85                 else if(c == star && lastc == slash)
 86                 {
 87                     lexeme = "";
 88                     state = MULTICOMMENT;
 89                     break;
 90                 }
 91                 else
 92                 {
 93                     state = WORD;
 94                 }
 95             case WORD:
 96                 if(isNewline(c))
 97                 {
 98                     setToken(lexeme, line, source, tokens.get());
 99                     lexeme = c;
100                     setToken(lexeme, line, source, tokens.get());
101                     state = READY;
102                 }
103                 else if(isWhitespace(c))
104                 {
105                     setToken(lexeme, line, source, tokens.get());
106                     state = READY;
107                 }
108                 else if(c == openbrace || c == closebrace || c == colon)
109                 {
110                     setToken(lexeme, line, source, tokens.get());
111                     lexeme = c;
112                     setToken(lexeme, line, source, tokens.get());
113                     state = READY;
114                 }
115                 else
116                 {
117                     lexeme += c;
118                 }
119                 break;
120             case QUOTE:
121                 if(c != backslash)
122                 {
123                     // Allow embedded quotes with escaping
124                     if(c == quote && lastc == backslash)
125                     {
126                         lexeme += c;
127                     }
128                     else if(c == quote)
129                     {
130                         lexeme += c;
131                         setToken(lexeme, line, source, tokens.get());
132                         state = READY;
133                     }
134                     else
135                     {
136                         // Backtrack here and allow a backslash normally within the quote
137                         if(lastc == backslash)
138                             lexeme = lexeme + "\\" + c;
139                         else
140                             lexeme += c;
141                     }
142                 }
143                 break;
144             case VAR:
145                 if(isNewline(c))
146                 {
147                     setToken(lexeme, line, source, tokens.get());
148                     lexeme = c;
149                     setToken(lexeme, line, source, tokens.get());
150                     state = READY;
151                 }
152                 else if(isWhitespace(c))
153                 {
154                     setToken(lexeme, line, source, tokens.get());
155                     state = READY;
156                 }
157                 else if(c == openbrace || c == closebrace || c == colon)
158                 {
159                     setToken(lexeme, line, source, tokens.get());
160                     lexeme = c;
161                     setToken(lexeme, line, source, tokens.get());
162                     state = READY;
163                 }
164                 else
165                 {
166                     lexeme += c;
167                 }
168                 break;
169             }
170 
171             // Separate check for newlines just to track line numbers
172             if(c == cr || (c == lf && lastc != cr))
173                 line++;
174             
175             i++;
176         }
177 
178         // Check for valid exit states
179         if(state == WORD || state == VAR)
180         {
181             if(!lexeme.empty())
182                 setToken(lexeme, line, source, tokens.get());
183         }
184         else
185         {
186             if(state == QUOTE)
187             {
188                 OGRE_EXCEPT(Exception::ERR_INVALID_STATE, 
189                     Ogre::String("no matching \" found for \" at line ") + 
190                         Ogre::StringConverter::toString(lastQuote),
191                     "ScriptLexer::tokenize");
192             }
193         }
194 
195         return tokens;
196     }

      由于返回值是ScriptTokenListPtr,可以先看一下相关定义:

 1     /** This struct represents a token, which is an ID'd lexeme from the
 2         parsing input stream.
 3     */
 4     struct ScriptToken
 5     {
 6         /// This is the lexeme for this token
 7         String lexeme, file;
 8         /// This is the id associated with the lexeme, which comes from a lexeme-token id mapping
 9         uint32 type;
10         /// This holds the line number of the input stream where the token was found.
11         uint32 line;
12     };
13     typedef SharedPtr<ScriptToken> ScriptTokenPtr;
14     typedef vector<ScriptTokenPtr>::type ScriptTokenList;
15     typedef SharedPtr<ScriptTokenList> ScriptTokenListPtr;

      ScriptLexer::tokenize()函数,先定义了一个保存ScriptToken的vector(ScriptTokenList)并得到了它的指针——tokens(17行),然后将词法分析结果以token为单位,逐一保存到tokens中并返回tokens值。整个函数的处理机制是:逐字符分析+状态机。

      变量“i”和“end”分别标识了读入的待解析的脚本文件的开头和结尾(20行),随着解析的进行,变量“i”将逐字符后移(175行)。整个解析过程由状态机的几种状态来表达,它们分别是:准备状态(READY 31-68行)、对注释信息的解析状态(COMMENT, MULTICOMMENT  69-77行)、对单词的解析状态(WORD  95-119)、对双引号中引用信息的解析状态(QUOTE  120-143行), 对变量信息的解析状态(VAR  144-168行)、对可能是注释信息的数据进行解析的状态(POSSIBLECOMMENT 78-94行)。词法分析的主要目的,是将脚本文件中的各个词素(lexeme 比如,一个单词、脚本中大括号的左半边、脚本中大括号的右半边等都被看一个词素)解读出来,并针对每个词素生成一个token对象,将此词素的相关信息保存在token对象中。在每一次循环开始时都要初始化两个变量:“c”和“lastc” (23,24行)。c表示当前正要被处理字符,lastc表示当前字符的前一个字符。之所以要申请这两个变量是因为,Ogre脚本中的“词素(lexeme)”是以空格为分格符的,用这两个变量就可以方便的识别出:当前读取的字符是一个新词素的第一个字符,还是正在解析的词素的最后一个字符,又或者是当前正在解析的词素的多个字符(如果存在的话)中间位置的某个字符。

      生成token对象并保存相应词素信息的过程由ScriptLexer::setToken()函数来实现,来看一下相关代码:

 1     void ScriptLexer::setToken(const Ogre::String &lexeme, Ogre::uint32 line, const String &source, Ogre::ScriptTokenList *tokens)
 2     {
 3 #if OGRE_WCHAR_T_STRINGS
 4         const wchar_t openBracket = L'{', closeBracket = L'}', colon = L':', 
 5             quote = L'\"', var = L'$';
 6 #else
 7         const char openBracket = '{', closeBracket = '}', colon = ':', 
 8             quote = '\"', var = '$';
 9 #endif
10 
11         ScriptTokenPtr token(OGRE_NEW_T(ScriptToken, MEMCATEGORY_GENERAL)(), SPFM_DELETE_T);
12         token->lexeme = lexeme;
13         token->line = line;
14         token->file = source;
15         bool ignore = false;
16 
17         // Check the user token map first
18         if(lexeme.size() == 1 && isNewline(lexeme[0]))
19         {
20             token->type = TID_NEWLINE;
21             if(!tokens->empty() && tokens->back()->type == TID_NEWLINE)
22                 ignore = true;
23         }
24         else if(lexeme.size() == 1 && lexeme[0] == openBracket)
25             token->type = TID_LBRACKET;
26         else if(lexeme.size() == 1 && lexeme[0] == closeBracket)
27             token->type = TID_RBRACKET;
28         else if(lexeme.size() == 1 && lexeme[0] == colon)
29             token->type = TID_COLON;
30         else if(lexeme[0] == var)
31             token->type = TID_VARIABLE;
32         else
33         {
34             // This is either a non-zero length phrase or quoted phrase
35             if(lexeme.size() >= 2 && lexeme[0] == quote && lexeme[lexeme.size() - 1] == quote)
36             {
37                 token->type = TID_QUOTE;
38             }
39             else
40             {
41                 token->type = TID_WORD;
42             }
43         }
44 
45         if(!ignore)
46             tokens->push_back(token);
47     }

       可以看到,本函数生成ScriptToken对象token(11行),赋给token的lexeme成员变量的值(12行),就是ScriptLexer::tokenize()函数分析后得到的词素,它是一个字符串(在tokenize()函数15行定义,并在后续的while()循环中解析得到实际值);赋给token的line成员变量的值(13行),表示此词素所在脚本的行的序数;赋给token的file成员变量的值(14行),表示此词素所在的脚本文件的文件名。token的type表示此词素的属性,其属性定义为:

    enum{
        TID_LBRACKET = 0, // {
        TID_RBRACKET, // }
        TID_COLON, // :
        TID_VARIABLE, // $...
        TID_WORD, // *
        TID_QUOTE, // "*"
        TID_NEWLINE, // \n
        TID_UNKNOWN,
        TID_END
    };

     它是一个定义在“OgreScriptLexer.h”头文件中的枚举类型。定义中各枚举项的含义已在其相应的注释中标明。

 

 

 

 

 

 

posted on 2013-01-02 15:17  yzwalkman  阅读(2131)  评论(0编辑  收藏  举报