Parser是rc文件解析成执行逻辑的核心工具。
内部通过tokenizer分词器对rc文件的字符流进行解析,转换成单词(参数)和对应的token令牌。根据token令牌,派分到不同的解析器实现进行的处理。
通过模板模式,Parser将实际的解析逻辑解耦到解析器模板的实现类中进行。
Parser定义
class Parser {
public:
using LineCallback = std::function<Result<Success>(std::vector<std::string>&&)>;
Parser();
bool ParseConfig(const std::string& path);
void AddSectionParser(const std::string& name, std::unique_ptr<SectionParser> parser);
void AddSingleLineParser(const std::string& prefix, LineCallback callback);
private:
std::map<std::string, std::unique_ptr<SectionParser>> section_parsers_;
std::vector<std::pair<std::string, LineCallback>> line_callbacks_;
};
bool ParseConfig(const std::string& path);
解析指定的rc文件
path
可以是rc文件路径,也可以是一个目录路径。void AddSectionParser(const std::string& name, std::unique_ptr<SectionParser> parser);
添加 SectionParser 解析器
添加段落解析器,name
为一行命令中,首个参数的完全匹配值。void AddSingleLineParser(const std::string& prefix, LineCallback callback);
添加 LineCallback 解析器
添加单行命令解析器。prefix
为一行命令中,首个参数的前序匹配值。
单行命令解析器模板:LineCallback
class Parser {
public:
using LineCallback = std::function<Result<Success>(std::vector<std::string>&&)>;
}
本质是一个函数。
解析过程中,对一行命令的首个参数进行匹配,当匹配成功时,马上调用对应的单行命令解析器(即回调函数)。
从源码上看,只有在ueventd进程解析ueventd.rc时,才有该类型解析器的应用。
段落命令解析器模板:SectionParser
class SectionParser {
public:
virtual ~SectionParser() {}
virtual Result<Success> ParseSection(std::vector<std::string>&& args,
const std::string& filename, int line) = 0;
virtual Result<Success> ParseLineSection(std::vector<std::string>&&, int) { return Success(); };
virtual Result<Success> EndSection() { return Success(); };
virtual void EndFile(){};
};
解析过程中,对一行命令的首个参数进行匹配,当匹配成功时,即认为该行时一个命令段落的开始,并使用匹配到的解析器解析段落中的命令内容。
主要定义了几个函数:
-
ParseSection()
处理段落首行命令
行命令处理时,匹配到段落解析器的关键字,则视为一个段落的首行,段落的后续命令交由该段落解析器处理,并调用该解析器的ParseSection()
函数。
ParseLineSection()
处理段落内容命令
对匹配不到关键字的行命令,视为段落的内容,假如存在解析中的段落解析器,则调用该解析器的ParseLineSection()
函数。EndSection()
处理段落结束
段落结束时,假如存在解析中的段落解析器,则调用该解析器的EndSection()
函数。EndFile()
处理文件结束
文件解析结束时,调用所有已添加解析器中的EndFile()
函数。
Parser实现
-
AddSectionParser
/AddSingleLineParser
void Parser::AddSectionParser(const std::string& name, std::unique_ptr<SectionParser> parser) { section_parsers_[name] = std::move(parser); } void Parser::AddSingleLineParser(const std::string& prefix, LineCallback callback) { line_callbacks_.emplace_back(prefix, callback); }
把关键字和解析器添加到对应的容器中。
-
ParseConfig
Parser::ParseConfig(const std::string& path) { size_t parse_errors; return ParseConfig(path, &parse_errors); } bool Parser::ParseConfig(const std::string& path, size_t* parse_errors) { *parse_errors = 0; if (is_dir(path.c_str())) { return ParseConfigDir(path, parse_errors); } return ParseConfigFile(path, parse_errors); }
判断
path
是否文件夹,选择直接解析文件,还是解析目录下的所有rc文件。
ParseConfigDir()
的内部遍历出的文件,最终同样调用ParseConfigFile()
进行解析。 -
ParseConfigFile
bool Parser::ParseConfigFile(const std::string& path, size_t* parse_errors) { auto config_contents = ReadFile(path); if (!config_contents) { return false; } config_contents->push_back('\n'); ParseData(path, *config_contents, parse_errors); for (const auto& [section_name, section_parser] : section_parsers_) { section_parser->EndFile(); } return true; }
通过
ReadFile()
函数,将文件数据读取为字符串数据。然后通过ParseData()
函数执行解析和处理。
文件解析完成时,调用所有添加的解析器的EndFile()
函数。
ParseData() 核心解析函数
Parse解析器的核心解析函数。
通过tokenizer分词器解析字符串获取token令牌,根据token令牌的类型,执行处理逻辑,或派分事件到具体解析器实现进行处理。
解析过程相关变量:
void Parser::ParseData(const std::string& filename, const std::string& data, size_t* parse_errors) {
std::vector<char> data_copy(data.begin(), data.end());
data_copy.push_back('\0');
...
}
-
data_copy
存放rc文件字符流的字符容器
把传入数据复制到名为data_copy
的vector<char>容器,并在末尾补充结束字符'\0'(NULL字符)。
struct parse_state
{
char *ptr; // 当前解析中字符的指针,即解析进度
char *text; // 当前检出的单词,即参数
int line; // 当前解析中的行号
int nexttoken; // 令牌缓存,用于性能优化
};
ParseData() {
...
parse_state state;
state.line = 0;
state.ptr = &data_copy[0];
state.nexttoken = 0;
...
}
-
state
解析数据结构体
创建parse_state结构体state
并初始化。用于存放 解析过程的状态 和 产生的临时数据。
ParseData() {
...
SectionParser* section_parser = nullptr;
int section_start_line = -1;
std::vector<std::string> args;
...
}
section_parser
段落目标解析器
当解析到新行时,假如新行首个参数命中解析器关键字 (即段落的首行),则把命中的解析器设置为段落目标解析器 (即当前段落的解析器)。
没有命中解析器关键字时,则尝试使用段落目标解析器对行进行解析(即解析段落非首行数据,即命令数据)。section_start_line
当前段落开始行号
如果当前解析的行,是一个段落的首行,则记录该行行号。args
当前行解析出的参数链表
参数链表,存放一行命令解析出的所有参数。
ParseData() {
...
auto end_section = [&] {
if (section_parser == nullptr) return;
if (auto result = section_parser->EndSection(); !result) {
(*parse_errors)++;
}
section_parser = nullptr;
section_start_line = -1;
};
...
}
-
end_section
Lambda表达式,用于结束段落解析和重置解析相关数据
假如当前段落目标解析器不为空,则调用其EndSection()
函数,通知解析器该段落解析结束,并重置段落目标解析器的指针section_parser
和当前段落开始行号section_start_line
。
解析过程逻辑阶段:
tokenizer分词器的实现分析:安卓启动流程(三) - tokenizer分词器
ParseData() {
...
for (;;) {
switch (next_token(&state)) {
case T_EOF: ...
case T_NEWLINE: ...
case T_TEXT: ...
}
}
...
}
死循环,通过next_token(&state)
函数获取token令牌和检出单词参数。
-
case T_TEXT
case T_TEXT: args.emplace_back(state.text); break;
检出了一个单词
向args
插入单词,state.text
指针指向单词的首字符。 -
case T_EOF
case T_EOF: end_section(); return;
表示rc文本解析结束
调用end_section
表达式,通知当前段落目标解析器该段落解析结束,并退出ParseData
函数。 -
case T_NEWLINE
:case T_NEWLINE: // 当前解析的行号 +1 state.line++; // 检出的参数为空,不执行处理 if (args.empty()) break; // 该行是否匹配到单行命令解析器 // 假如匹配到单行命令解析器,尝试通过end_section函数结束当前段落解析, // 然后调用对应的单行命令解析器 for (const auto& [prefix, callback] : line_callbacks_) { if (android::base::StartsWith(args[0], prefix)) { // 尝试通过end_section函数结束当前段落解析 end_section(); // 调用对应的单行命令解析器 if (auto result = callback(std::move(args)); !result) { (*parse_errors)++; } break; } } // 该行是否匹配到段落解析器 if (section_parsers_.count(args[0])) { // 当匹配到解析器时, 尝试通过end_section函数结束上一个段落解析 end_section(); // 记录当前解析器 section_parser = section_parsers_[args[0]].get(); // 记录段落行号 section_start_line = state.line; // 调用解析器的ParseSection函数 if (auto result = section_parser->ParseSection(std::move(args), filename, state.line); !result) { (*parse_errors)++; section_parser = nullptr; } } else if (section_parser) { // 假如存在段落目标解析器,则调用解析器的ParseLineSection函数 if (auto result = section_parser->ParseLineSection(std::move(args), state.line); !result) { (*parse_errors)++; } } // 清空参数链表 args.clear(); break; }
检测到换行,该行结束,处理行命令
该行参数已解析完成,在解析下一行前,处理该行的命令。
大致流程总结
- 使用
ParseData()
对路径参数path
下的所有rc文件,进行解析。 -
ParseData()
中命令的单位为每一行的文本。一行命令中检出的所有参数都会放入args
链表,在下一行解析开始前,处理命令,并在命令处理完成时清空args
链表。 - 命令处理开始时,先进行解析器匹配。当
args
链表数量大于0时,进入匹配阶段,首先匹配单行命令解析器,然后匹配段落命令解析器。 - 匹配阶段无法匹配到解析器时,则该行命令视为段落命令,进入段落命令处理阶段,尝试使用已设置为处理中的段落解析器进行处理,无法找到解析器,则跳过该行命令的处理。