分享

Thrift之代码生成器Compiler原理及源码详细解析1

本帖最后由 xuanxufeng 于 2015-12-21 20:16 编辑
问题导读
1.类关系图Compiler功能模块主要分为哪两个部分?
2.Main.cc文件的作用是什么?
3.t_generator类和t_generator_registry类的功能是什么?







本篇博客内容主要是前一段时间研究的Thrift的代码生成器的源码详细分析,没有具体分析语法解析,因为是工具字段生成的代码,人是没有办法阅读的----到处都是跳转表!由于Thrift支持N多种语言,但是生成代码原理都差不多,我主要分析了C++相关代码生成。关于Thrift的使用及原理、代码网上基本上都有,代码的注释很好,基本上都是英文注释。下面就是我之前分析写的文档,希望对学习使用代码生成代码的爱好者有一定帮助。




这个功能是一个单独的工具程序,它会独立的生成一个可执行文件。
第一节 类关系图

1.png

本节主要展示了这个部分的整体类图,然后对这个类图做了简要的说明,有了这个类图让我在阅读这个部分源代码时不会找不到方向,让我更加清楚这个部分中的类是怎样协同工作的,类关系图如下所示:

注意:实线代表继承关系;而虚线代表依赖关系。
由类关系图可以看出Compiler功能模块主要分为两个部分,一个是图的右边展示了各种语言代码生成的类。它们的共同基类都是t_generator类,但是面向对象的语言并不是直接从它继承,而是又把面向对象的共同特性提取出来用一个类来实现,这个类就是t_oop_generator,其他面向对象语言的生成类都是从这个类继承。总基类的实现是依赖于左边的t_program类,这个类表示一个程序代码需要的所有特征和要素。左边部分就是解决一个程序需要拥有的数据类型和函数,根据接口定义语言(IDL)解析和生成相应的数据和函数等。左边部分就显示thrift定义的中间语言(IDL)能够支持的数据类型,t_type类是所有数据类型类的基类。


第二节 程序流程图

这个部分整体的流程图如下图所示:


2.png


以上流程图简要的说明了Compiler运行的一个过程,通过这个流程图可以让我们知道了根据中间定义语言生成各种程序代码的整体思路和流程。下一节将根据源代码详细分析整个过程的原理及实现方案,这个里面涉及到一些编译原理的知识,不深入分析这一部分,它里面的词法分析程序是用linux上的工具flex自动生成c语言程序,解析中间定义语言的时候直接调用yyparse函数即可。另一部分重点内容就是生成各种程序语言代码的功能,每一种语言用一个类来生成,后面不会详细分析每一种语言的生成实现的代码,主要分析三种主流语言(C++、java、python)。


第三节 源代码详细分析

1  Main.cc文件

这个文件是这个部分的入口文件,因为它定义main函数。除了main函数以外还定义了其它很多的全局函数和变量,其中比较重要的函数有:parse解析函数、generate生成代码的函数;比较重要的全局变量主要是:g_program程序类的变量和各种数据类型的类的变量。
(1)main函数
首先定义一个用于存放源代码输出路径的字符串变量:std::string out_path;然后生成一个基于时间的字符串保存到一个全局变量中,如下代码实现:
[mw_shl_code=bash,true]time_t now = time(NULL);
  g_time_str = ctime(&now);[/mw_shl_code]
然后检查参数个数是否满足最低要求,不满足就调用使用说明函数,这个函数就是简单打印这个工具的使用说明,然后退出程序。代码如下:
[mw_shl_code=bash,true]if (argc < 2) {
    usage();
}[/mw_shl_code]
下面定义一个用于保存需要生成语言的代表字符串的向量数组:vector<string> generator_strings;后面就根据参数的个数解析参数,然后根据参数的内容执行相应的功能。解析参数的时候用到了一个函数strtok,它需要两个参数,第一个是需要分割的字符串,不能是指向常量区的,第二个是分割字符串的分隔符字符串,首先返回第一个被分割后的字符串,下一次调用第一个参数用NULL就继续下一个被分割下来的字符串,如果没有了就返回NULL。上面说了根据参数内容执行相应的功能:主要包括查看版本信息、是否打印详细的执行过程信息、警告级别等,最主要的还是解析需要生产哪些语言的参数,然后将能够代表需要生产某种语言的字符串保存到generator_strings字符串数组当中。
下面的代码开始根据参数得到中间语言定义的文件,然后根据文件名生成一个t_program的对象来代表整个程序的解析树,接着根据文件名找到文件所在的目录并设置包含文件的目录,最后初始化一些全局变量(为这些变量分别内存资源),中间还设置了生成代码输出的路径。
[mw_shl_code=bash,true]if (saferealpath(argv, rp) == NULL) {
    failure("Could not open input file with realpath: %s", argv);
}
string input_file(rp);
t_program* program = new t_program(input_file);
if (out_path.size()) {
    program->set_out_path(out_path);
}
string input_filename = argv;
string include_prefix;
string::size_type last_slash = string::npos;
if ((last_slash = input_filename.rfind("/")) != string::npos) {
    include_prefix = input_filename.substr(0, last_slash);
}
program->set_include_prefix(include_prefix);
g_type_void   = new t_base_type("void",   t_base_type::TYPE_VOID);
g_type_string = new t_base_type("string", t_base_type::TYPE_STRING);
g_type_binary = new t_base_type("string", t_base_type::TYPE_STRING);
((t_base_type*)g_type_binary)->set_binary(true);
g_type_slist  = new t_base_type("string", t_base_type::TYPE_STRING);
  ((t_base_type*)g_type_slist)->set_string_list(true);
g_type_bool   = new t_base_type("bool",   t_base_type::TYPE_BOOL);
g_type_byte   = new t_base_type("byte",   t_base_type::TYPE_BYTE);
g_type_i16    = new t_base_type("i16",    t_base_type::TYPE_I16);
g_type_i32    = new t_base_type("i32",    t_base_type::TYPE_I32);
g_type_i64    = new t_base_type("i64",    t_base_type::TYPE_I64);
g_type_double = new t_base_type("double", t_base_type::TYPE_DOUBLE);[/mw_shl_code]
后然调用解析函数和代码生成函数如下:
[mw_shl_code=bash,true]parse(program, NULL);
generate(program, generator_strings);[/mw_shl_code]
最后删除申请到资源。
(2)parse函数
这个函数的主要功能就是调用词法分析程序来进行词法分析,后面会根据词法分析的结果来生产程序代码。下面将详细分析这个函数的功能。
首先从program得到中间文件的全路径并初始化当前文件路径的全局变量,然后根据文件的全路径得到目录来初始化当前目录全局变量,实现代码如下:
[mw_shl_code=bash,true]string path = program->get_path();
g_curdir = directory_name(path);
g_curpath = path;[/mw_shl_code]
接着根据上面得到的文件路径打开这个文件作为词法分析程序的分析对象:yyin = fopen(path.c_str(), "r");
下面开始第一次进行词法分析,这次词法分析的主要目的提取里面内嵌的IDL文件,所以设置解析的模式为INCLUDES,解析完成以后关闭文件。词法分析的结果都存放到program中。以便后面使用到的地方直接从program就可以得到。词法分析的时候可能发生异常,所以需要处理异常。实现代码如下:
[mw_shl_code=bash,true]g_parse_mode = INCLUDES;
g_program = program;
g_scope = program->scope();
try {
    yylineno = 1;
    if (yyparse() != 0) {
      failure("Parser error during include pass.");
    }
} catch (string x) {
    failure(x.c_str());
}
fclose(yyin);[/mw_shl_code]
分析出内嵌的IDL文件以后就对这些IDL文件进行递归调用parse函数来对每一个IDL文件进行词法分析,因为每一个IDL文件里面可能包括多个IDL文件,所以需要用一个for循环对没有一个IDL都进行递归词法分析,具体实现如下:
[mw_shl_code=bash,true]vector<t_program*>& includes = program->get_includes();
vector<t_program*>::iterator iter;
for (iter = includes.begin(); iter != includes.end(); ++iter) {
    parse(*iter, program);
}[/mw_shl_code]
最后一部分是重点,将进行第二次词法分析,这次分析就是真正的对IDL文件中定义的数据类型和服务(函数)进行词法分析和语法分析,所以首先需要设置词法分析的模式为PROGRAM。还需要初始化一些全局变量,和第一次词法分析一样需要打开IDL文件为词法分析程序提供分析源、异常处理和最后关闭文件,实现的主要代码如下:
[mw_shl_code=bash,true]g_parse_mode = PROGRAM;
g_program = program;
g_scope = program->scope();
g_parent_scope = (parent_program != NULL) ? parent_program->scope() : NULL;
g_parent_prefix = program->get_name() + ".";
g_curpath = path;
yyin = fopen(path.c_str(), "r");
yylineno = 1;
try {
    if (yyparse() != 0) {
      failure("Parser error during types pass.");
    }
} catch (string x) {
    failure(x.c_str());
}
fclose(yyin);[/mw_shl_code]
到此这个函数完成了所有自己的功能,所有词法分析的结果都保存到program中了,同时g_program也保存同样的一份内容,以便后面的生成代码函数使用。
(3)generate函数
本函数主要功能就是根据parse函数生成的各种数据类型和服务生成各种代码文件。首先递归调用每一个内嵌的t_program来生成代码,实现代码如下:
[mw_shl_code=bash,true]const vector<t_program*>& includes = program->get_includes();
    for (size_t i = 0; i < includes.size(); ++i) {
      includes->set_out_path(program->get_out_path());
      generate(includes, generator_strings);
}[/mw_shl_code]
后面部分就是真正生成代码文件的功能了。首先为每一个结构体、枚举和异常生成一个在thrift中全球唯一的识别指纹(其实就是字符串,这个字符串是根据具体类型信息的字符串经过MD5处理后的字符串,如枚举就是根据”enum”生成的)。然后根据需要决定是否打印所有的调试信息。接着根据需要生成的语言循环生成每一种语言的代码,这个是根据在main函数中存放代表语言的字符串(generator_strings)来决定,根据t_program和代表语言的字符串得到一个代码生成器的对象(每一种语言都有一个独立的生成语言的类),然最后就调用这个代码生成器对象的代码生成函数生成具体的代码文件,代码如下:
[mw_shl_code=bash,true]generate_all_fingerprints(program);
if (dump_docs) {
    dump_docstrings(program);
}
vector<string>::const_iterator iter;
for (iter = generator_strings.begin(); iter != generator_strings.end(); ++iter) {
   t_generator* generator = t_generator_registry::get_generator(program, *iter);
   if (generator == NULL) {
      pwarning(1, "Unable to get a generator for \"%s\".\n", iter->c_str());
   } else {
      pverbose("Generating \"%s\"\n", iter->c_str());
      generator->generate_program();
      delete generator;
   }
}[/mw_shl_code]
本函数实现的功能就是以上这些功能,至于具体生成语言代码的功能在各种语言生成的类中实现,后面会详细分析java、C++和python的生成实现。
(4)其它函数
这个主程序文件中还有其他许多函数,下面简单介绍每一个函数的功能,就不分析详细的实现了,具体的实现可以查看源代码。
函数名称
函数功能
saferealpath
根据文件的相对路径得到文件真实而安全的文件绝对路径
yyerror
词法分析程序的错误信息输出程序
pdebug
解析器打印调试信息
pverbose
打印一个详细的输出模式的消息
pwarning
打印警告消息
failure
打印失败的消息并且退出程序
program_name
转换一个字符串的文件名为thrift的program名称
directory_name
根据一个文件的路径得到目录路径
include_file
从给定的文件名查找相应的文件路径
clear_doctext
清除任何以前存储的文档的文本字符串。
clean_up_doctext
清理文本通常类似doxygen的注释
dump_docstrings
输出程序文档字符串到stdout
generate_all_fingerprints
为program的每一个结构体和枚举生成唯一的“指纹”
version
打印thrift的版本信息
usage
打印使用信息并且退出程序
validate_const_rec
验证常量类型是否有效
validate_const_type
检查常量类型的声明类型
validate_field_value
检查分配给一个字段默认值的类型。
validate_throws
检查所有元素抛出的异常是真实的异常



###########################################################



  t_generator类和t_generator_registry类这个两个类的主要功能就是为生成所有语言的代码提供基础信息和提供具体代码生成器对象,上面就是调用这个两个类的方法来生成具体语言的代码生成器对象和执行生成代码的功能函数。下面主要分析两个函数的功能,一个是t_generator_registry类的get_generator函数,这个是一个静态的函数可以直接通过类调用;另一个是t_generator类的generate_program函数。
(1)t_generator_registry类的get_generator函数
这个函数有两个参数,一个是表示程序的对象program,另一个是语言字符串参数(包括代表语言的简短字符串和可选项的组合,有的没有)。函数首先解析语言字符串参数,参数字符串中是这样组织的:在冒号(:)之前是代表语言的字符串,冒号之后是可选项的参数,每一个可选项参数用逗号(,)分割,每一个可选项参数都是键值对并且键和值是用等号(=)分割。按照上面的字符串格式解析各个参数部分就可以了,可选项参数用map来保存键值对,代码实现如下:
[mw_shl_code=bash,true]string::size_type colon = options.find(':');
  string language = options.substr(0, colon);
  map<string, string> parsed_options;
  if (colon != string::npos) {
    string::size_type pos = colon+1;
    while (pos != string::npos && pos < options.size()) {
      string::size_type next_pos = options.find(',', pos);
      string option = options.substr(pos, next_pos-pos);
      pos = ((next_pos == string::npos) ? next_pos : next_pos+1);
      string::size_type separator = option.find('=');
      string key, value;
      if (separator == string::npos) {
        key = option;
       value = "";
      } else {
        key = option.substr(0, separator);
        value = option.substr(separator+1);
     }
      parsed_options[key] = value;
    }
  }[/mw_shl_code]
然后调用get_generator_map函数得到一个代表语言字符串和产生这种语言生成器对象的工厂对象的map对象:gen_map_t& the_map = get_generator_map(); gen_map_t的定义如下:
[mw_shl_code=bash,true]typedef std::map<std::string, t_generator_factory*> gen_map_t;[/mw_shl_code]
get_generator_map函数只有两句代码,一个是定义一个静态局部变量并初始化(因为静态局部变量必须并初始化并且只有第一次会执行初始化,因为不初始化链接程序的时候会报错),第二句就是返回这个静态局部变量给调用者,代码如下:
[mw_shl_code=bash,true]static gen_map_t* the_map = new gen_map_t();
  return *the_map;[/mw_shl_code]
然后在这个map对象中找到对应语言的工厂对象,然后用这个工厂对象生产一个这种语言的代码生成器对象并返回给调用者,代码如下所示:
[mw_shl_code=bash,true]   gen_map_t::iterator iter = the_map.find(language);
  return iter->second->get_generator(program, parsed_options, options);[/mw_shl_code]
本函数的功能已经分析完毕,但是还存在着两个问题(或是困难)。一个是最后一条返回一句是根据具体的语言来使用具体语言生产器的工厂对象生产代码生成器,具体又是怎么生成的了?第二个就是从main函数执行到现在还没有发现在哪儿为get_generator_map函数里定义的静态局部变量添加过任何键值对,那么我们查找具体语言必定会失败,那么会返回一个NULL给调用者,那么程序就会执行不下去了,但是程序确实能够完完整整的执行下去,这个问题困扰了我好一会儿。下面就这两个问题继续分析相关代码并且解决问题。
第一个应该不算是问题,但是必须要解决第二个问题以后才能够解释,因为没有解决第二个问题,那么根本就不会执行到最后一条返回语句这儿来,所以我先解决第二个问题。
第二个问题分析和解决思路如下:
我们通常认为main函数是程序的入口函数,那么所以程序的执行都是从main函数开始的,所以我也选择从main函数开始分析这部分的代码,根据程序的执行流程阅读和分析代码是我一贯的思路。但是这种情况在C++里面有例外,记得我在学习MFC的时候,分析MFC执行过程就发现一个问题,那就是全局变量的初始化是在main函数开始之前的,也就是说全局类对象的构造函数也是在main执行之前执行的。由于我反复从main开始一直详细的阅读每一行代码,所以可以确定确实没有在执行的过程中初始化the_map静态局部变量,所以唯一的可能就是在main函数开始之前已经初始化好了。根据这一点思路自己开始着手查找初始化the_map的代码,发现t_generator_registry类的register_generator函数为the_map添加键值对了,这个函数定义如下:
[mw_shl_code=bash,true]void t_generator_registry::register_generator(t_generator_factory* factory) {
  gen_map_t& the_map = get_generator_map();
  if (the_map.find(factory->get_short_name()) != the_map.end()) {
    failure("Duplicate generators for language \"%s\"!\n", factory->get_short_name().c_str());
  }
  the_map[factory->get_short_name()] = factory;
}[/mw_shl_code]
这个函数也首先调用get_generator_map函数得到那个静态局部变量,然后查找要注册的工程是否已经在the_map中存在,如果存在就提示失败信息,否则就把工厂的名字和工厂对象作为键值对添加到the_map中。
虽然找到了为the_map添加键值对的地方,但是还没有找到调用这个注册工厂函数的地方,所以继续在代码中搜索调用这个函数的地方。整个代码就只有一处调用了这个函数,而且是在一个类的构造函数中,代码如下:
[mw_shl_code=bash,true]t_generator_factory::t_generator_factory(const std::string& short_name, const std::string& long_name,
    const std::string& documentation) : short_name_(short_name)
  , long_name_(long_name) , documentation_(documentation)
{
  t_generator_registry::register_generator(this);
}[/mw_shl_code]
t_generator_factory类是所有生产代码生产器对象工厂的基类,每一种具体的语言都有自己的代码生成器类和生产这种类的工厂类,上面的代码是它的构造函数,功能就是把自己注册到the_map中。看到这里是否有一种逐渐清晰的感觉,但是总是感觉还有少点什么,就是这个构造函数被调用也必须有这个类的对象被定义或其子类的对象被定义。于是我又开始搜索哪些类是从这个类继承的,发现两处很重要的代码,一处如下:
[mw_shl_code=bash,true]template <typename generator>
class t_generator_factory_impl : public t_generator_factory {
public:
  t_generator_factory_impl(const std::string& short_name, const std::string& long_name,
         const std::string& documentation) : t_generator_factory(short_name, long_name, documentation)
  {}
virtual t_generator* get_generator(t_program* program,
const std::map<std::string, std::string>& parsed_options, const std::string& option_string) {
    return new generator(program, parsed_options, option_string);
}
……//此处省略了一些代码
};[/mw_shl_code]
t_generator_factory_impl类继承了t_generator_factory类,而且在构造函数的时候也调用了父类的构造函数,因为是带参数的构造函数所以必须手动调用父类的构造函数。这个类是一个模板类,模板参数就是一个代码生成器类,所以函数get_generator就能够根据这个模板参数生成new一个对应语言的代码生成器对象了。这里就把上面提到的第一个问题也解决了,每一个工厂类把自己注册到the_map,然后使用者通过代表语言的键(key)在the_map找到对应的工厂对象,然后调用get_generator函数就生成具体的代码生成器对象了,这就是第一个问题提到的最后一句返回语句的代码执行情况。
但是还是没有看到定义具体的工厂对象呀,那么还需要看下面一处的代码:
[mw_shl_code=bash,true]#define THRIFT_REGISTER_GENERATOR(language, long_name, doc)        \
class t_##language##_generator_factory_impl                      \
    : public t_generator_factory_impl<t_##language##_generator>    \
  {                                                                \
   public:                                                         \
    t_##language##_generator_factory_impl()                        \
      : t_generator_factory_impl<t_##language##_generator>(        \
          #language, long_name, doc)                               \
    {}                                                             \
};       [/mw_shl_code]                                                        \
  static t_##language##_generator_factory_impl _registerer;
这是一个宏定义,它根据参数language定义一个生产具体语言的代码生成器的工厂类,并从t_generator_factory_impl类继承,传递的模板参数也是对应语言的代码生成器类,构造函数同样调用了父类的构造函数;最后还定义了一个对应的静态的类全局变量(千呼万唤始出来,终于找到定义类的全局变量了)。但是还是存在同样的问题就是定义了宏函数还是需要调用才执行吧,所以就在代码中搜索调用了这个宏函数的代码,最终发现这个每一个具体的语言代码生成器的文件都调用了一次,如下面是C++的文件t_cpp_generator.cc中调用的代码:
[mw_shl_code=bash,true]THRIFT_REGISTER_GENERATOR(cpp, "C++",
"    pure_enums:      Generate pure enums instead of wrapper classes.\n"
"    dense:           Generate type specifications for the dense protocol.\n"
"    include_prefix:  Use full include paths in generated files.\n"
)[/mw_shl_code]
其他语言的代码生成器类的定义文件中都有类似的调用,这样每一个语言生成器对象的生产工厂就被注册到the_map中了,由此问题得到解决。
(2)t_generator类的generate_program函数
这个函数是生成具体语言代码的顶层函数,它会调用子类定义的各个子函数来做具体代码的生成过程,后面会详细解析C++、java和python代码生成的过程。
首先调用代码生成器的初始化函数来初始化代码生成器,然后依次调用各种基本数据类型和服务的生成函数来生成相应的代码,最后关闭代码生成器。代码实现如下:
  [mw_shl_code=bash,true]init_generator();
  vector<t_enum*> enums = program_->get_enums();
  vector<t_enum*>::iterator en_iter;
  for (en_iter = enums.begin(); en_iter != enums.end(); ++en_iter) {
    generate_enum(*en_iter);
  }
  vector<t_typedef*> typedefs = program_->get_typedefs();
  vector<t_typedef*>::iterator td_iter;
  for (td_iter = typedefs.begin(); td_iter != typedefs.end(); ++td_iter) {
    generate_typedef(*td_iter);
  }
  vector<t_const*> consts = program_->get_consts();
  generate_consts(consts);
  vector<t_struct*> objects = program_->get_objects();
  vector<t_struct*>::iterator o_iter;
  for (o_iter = objects.begin(); o_iter != objects.end(); ++o_iter) {
    if ((*o_iter)->is_xception()) {
      generate_xception(*o_iter);
    } else {
      generate_struct(*o_iter);
    }
  }
  vector<t_service*> services = program_->get_services();
  vector<t_service*>::iterator sv_iter;
  for (sv_iter = services.begin(); sv_iter != services.end(); ++sv_iter) {
    service_name_ = get_service_name(*sv_iter);
    generate_service(*sv_iter);
  }
  close_generator();[/mw_shl_code]此函数使用的是词法和语法分析结果的一些符号,这些符号都保持在t_program对象的对于数据结构里面,所以上面的函数依次从t_program对象中取得各种数据类型的符号和服务的符号,并依次生成。
(3)t_generator类的其它功能简介
这个类是所有具体语言代码生成器的共同基类,所以定义了很多各种语言代码生成需要的共同功能,例如生成代码的格式控制、命名空间的有效性检查、驼峰标识符和下划线标识符的相互转换等等。这些功能比较简单,需要可以直接查看源代码。




下一篇:Thrift之代码生成器Compiler原理及源码详细解析2
http://www.aboutyun.com/thread-16685-1-1.html







没找到任何评论,期待你打破沉寂

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

关闭

推荐上一条 /2 下一条