对文本文件的解析,有两种方式比较常见。第一种,文件信息有固定的格式但没有太过复杂的结构,比如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”头文件中的枚举类型。定义中各枚举项的含义已在其相应的注释中标明。