我的新浪微博:http://weibo.com/freshairbrucewoo。
欢迎大家相互交流,共同提高技术。
又很久没有写博客了,最近忙着研究GlusterFS,本来周末打算写几篇博客的,但是由于调试GlusterFS的一些新增功能就用了整整的一天,还有一天就陪老婆大人逛街去了!今晚浏览完微博发现时间还早就来博客一篇,本篇博客内容主要是前一段时间研究的Thrift的代码生成器的源码详细分析,没有具体分析语法解析,因为是工具字段生成的代码,人是没有办法阅读的----到处都是跳转表!由于Thrift支持N多种语言,但是生成代码原理都差不多,我主要分析了C++相关代码生成。关于Thrift的使用及原理、代码网上基本上都有,代码的注释很好,基本上都是英文注释。下面就是我之前分析写的文档,希望对学习使用代码生成代码的爱好者有一定帮助。
Thrift之代码生成器Compiler原理及源码详细解析1
这个功能是一个单独的工具程序,它会独立的生成一个可执行文件。
第一节 类关系图
本节主要展示了这个部分的整体类图,然后对这个类图做了简要的说明,有了这个类图让我在阅读这个部分源代码时不会找不到方向,让我更加清楚这个部分中的类是怎样协同工作的,类关系图如下所示:
注意:实线代表继承关系;而虚线代表依赖关系。
由类关系图可以看出Compiler功能模块主要分为两个部分,一个是图的右边展示了各种语言代码生成的类。它们的共同基类都是t_generator类,但是面向对象的语言并不是直接从它继承,而是又把面向对象的共同特性提取出来用一个类来实现,这个类就是t_oop_generator,其他面向对象语言的生成类都是从这个类继承。总基类的实现是依赖于左边的t_program类,这个类表示一个程序代码需要的所有特征和要素。左边部分就是解决一个程序需要拥有的数据类型和函数,根据接口定义语言(IDL)解析和生成相应的数据和函数等。左边部分就显示thrift定义的中间语言(IDL)能够支持的数据类型,t_type类是所有数据类型类的基类。
第二节 程序流程图
这个部分整体的流程图如下图所示:
以上流程图简要的说明了Compiler运行的一个过程,通过这个流程图可以让我们知道了根据中间定义语言生成各种程序代码的整体思路和流程。下一节将根据源代码详细分析整个过程的原理及实现方案,这个里面涉及到一些编译原理的知识,不深入分析这一部分,它里面的词法分析程序是用linux上的工具flex自动生成c语言程序,解析中间定义语言的时候直接调用yyparse函数即可。另一部分重点内容就是生成各种程序语言代码的功能,每一种语言用一个类来生成,后面不会详细分析每一种语言的生成实现的代码,主要分析三种主流语言(C++、java、python)。
第三节 源代码详细分析
1 Main.cc文件
这个文件是这个部分的入口文件,因为它定义main函数。除了main函数以外还定义了其它很多的全局函数和变量,其中比较重要的函数有:parse解析函数、generate生成代码的函数;比较重要的全局变量主要是:g_program程序类的变量和各种数据类型的类的变量。
(1)main函数
首先定义一个用于存放源代码输出路径的字符串变量:std::string out_path;然后生成一个基于时间的字符串保存到一个全局变量中,如下代码实现:
1 time_t now = time(NULL); 2 3 g_time_str = ctime(&now);
然后检查参数个数是否满足最低要求,不满足就调用使用说明函数,这个函数就是简单打印这个工具的使用说明,然后退出程序。代码如下:
1 if (argc < 2) { 2 3 usage(); 4 5 }
下面定义一个用于保存需要生成语言的代表字符串的向量数组:vector<string> generator_strings;后面就根据参数的个数解析参数,然后根据参数的内容执行相应的功能。解析参数的时候用到了一个函数strtok,它需要两个参数,第一个是需要分割的字符串,不能是指向常量区的,第二个是分割字符串的分隔符字符串,首先返回第一个被分割后的字符串,下一次调用第一个参数用NULL就继续下一个被分割下来的字符串,如果没有了就返回NULL。上面说了根据参数内容执行相应的功能:主要包括查看版本信息、是否打印详细的执行过程信息、警告级别等,最主要的还是解析需要生产哪些语言的参数,然后将能够代表需要生产某种语言的字符串保存到generator_strings字符串数组当中。
下面的代码开始根据参数得到中间语言定义的文件,然后根据文件名生成一个t_program的对象来代表整个程序的解析树,接着根据文件名找到文件所在的目录并设置包含文件的目录,最后初始化一些全局变量(为这些变量分别内存资源),中间还设置了生成代码输出的路径。
1 if (saferealpath(argv[i], rp) == NULL) { 2 3 failure("Could not open input file with realpath: %s", argv[i]); 4 5 } 6 7 string input_file(rp); 8 9 t_program* program = new t_program(input_file); 10 11 if (out_path.size()) { 12 13 program->set_out_path(out_path); 14 15 } 16 17 string input_filename = argv[i]; 18 19 string include_prefix; 20 21 string::size_type last_slash = string::npos; 22 23 if ((last_slash = input_filename.rfind("/")) != string::npos) { 24 25 include_prefix = input_filename.substr(0, last_slash); 26 27 } 28 29 program->set_include_prefix(include_prefix); 30 31 g_type_void = new t_base_type("void", t_base_type::TYPE_VOID); 32 33 g_type_string = new t_base_type("string", t_base_type::TYPE_STRING); 34 35 g_type_binary = new t_base_type("string", t_base_type::TYPE_STRING); 36 37 ((t_base_type*)g_type_binary)->set_binary(true); 38 39 g_type_slist = new t_base_type("string", t_base_type::TYPE_STRING); 40 41 ((t_base_type*)g_type_slist)->set_string_list(true); 42 43 g_type_bool = new t_base_type("bool", t_base_type::TYPE_BOOL); 44 45 g_type_byte = new t_base_type("byte", t_base_type::TYPE_BYTE); 46 47 g_type_i16 = new t_base_type("i16", t_base_type::TYPE_I16); 48 49 g_type_i32 = new t_base_type("i32", t_base_type::TYPE_I32); 50 51 g_type_i64 = new t_base_type("i64", t_base_type::TYPE_I64); 52 53 g_type_double = new t_base_type("double", t_base_type::TYPE_DOUBLE);
后然调用解析函数和代码生成函数如下:
1 parse(program, NULL); 2 3 generate(program, generator_strings);
最后删除申请到资源。
(2)parse函数
这个函数的主要功能就是调用词法分析程序来进行词法分析,后面会根据词法分析的结果来生产程序代码。下面将详细分析这个函数的功能。
首先从program得到中间文件的全路径并初始化当前文件路径的全局变量,然后根据文件的全路径得到目录来初始化当前目录全局变量,实现代码如下:
1 string path = program->get_path(); 2 3 g_curdir = directory_name(path); 4 5 g_curpath = path;
接着根据上面得到的文件路径打开这个文件作为词法分析程序的分析对象:yyin = fopen(path.c_str(), "r");
下面开始第一次进行词法分析,这次词法分析的主要目的提取里面内嵌的IDL文件,所以设置解析的模式为INCLUDES,解析完成以后关闭文件。词法分析的结果都存放到program中。以便后面使用到的地方直接从program就可以得到。词法分析的时候可能发生异常,所以需要处理异常。实现代码如下:
1 g_parse_mode = INCLUDES; 2 3 g_program = program; 4 5 g_scope = program->scope(); 6 7 try { 8 9 yylineno = 1; 10 11 if (yyparse() != 0) { 12 13 failure("Parser error during include pass."); 14 15 } 16 17 } catch (string x) { 18 19 failure(x.c_str()); 20 21 } 22 23 fclose(yyin);
分析出内嵌的IDL文件以后就对这些IDL文件进行递归调用parse函数来对每一个IDL文件进行词法分析,因为每一个IDL文件里面可能包括多个IDL文件,所以需要用一个for循环对没有一个IDL都进行递归词法分析,具体实现如下:
1 vector<t_program*>& includes = program->get_includes(); 2 3 vector<t_program*>::iterator iter; 4 5 for (iter = includes.begin(); iter != includes.end(); ++iter) { 6 7 parse(*iter, program); 8 9 }
最后一部分是重点,将进行第二次词法分析,这次分析就是真正的对IDL文件中定义的数据类型和服务(函数)进行词法分析和语法分析,所以首先需要设置词法分析的模式为PROGRAM。还需要初始化一些全局变量,和第一次词法分析一样需要打开IDL文件为词法分析程序提供分析源、异常处理和最后关闭文件,实现的主要代码如下:
1 g_parse_mode = PROGRAM; 2 3 g_program = program; 4 5 g_scope = program->scope(); 6 7 g_parent_scope = (parent_program != NULL) ? parent_program->scope() : NULL; 8 9 g_parent_prefix = program->get_name() + "."; 10 11 g_curpath = path; 12 13 yyin = fopen(path.c_str(), "r"); 14 15 yylineno = 1; 16 17 try { 18 19 if (yyparse() != 0) { 20 21 failure("Parser error during types pass."); 22 23 } 24 25 } catch (string x) { 26 27 failure(x.c_str()); 28 29 } 30 31 fclose(yyin);
到此这个函数完成了所有自己的功能,所有词法分析的结果都保存到program中了,同时g_program也保存同样的一份内容,以便后面的生成代码函数使用。
(3)generate函数
本函数主要功能就是根据parse函数生成的各种数据类型和服务生成各种代码文件。首先递归调用每一个内嵌的t_program来生成代码,实现代码如下:
1 const vector<t_program*>& includes = program->get_includes(); 2 3 for (size_t i = 0; i < includes.size(); ++i) { 4 5 includes[i]->set_out_path(program->get_out_path()); 6 7 generate(includes[i], generator_strings); 8 9 }
后面部分就是真正生成代码文件的功能了。首先为每一个结构体、枚举和异常生成一个在thrift中全球唯一的识别指纹(其实就是字符串,这个字符串是根据具体类型信息的字符串经过MD5处理后的字符串,如枚举就是根据”enum”生成的)。然后根据需要决定是否打印所有的调试信息。接着根据需要生成的语言循环生成每一种语言的代码,这个是根据在main函数中存放代表语言的字符串(generator_strings)来决定,根据t_program和代表语言的字符串得到一个代码生成器的对象(每一种语言都有一个独立的生成语言的类),然最后就调用这个代码生成器对象的代码生成函数生成具体的代码文件,代码如下:
1 generate_all_fingerprints(program); 2 3 if (dump_docs) { 4 5 dump_docstrings(program); 6 7 } 8 9 vector<string>::const_iterator iter; 10 11 for (iter = generator_strings.begin(); iter != generator_strings.end(); ++iter) { 12 13 t_generator* generator = t_generator_registry::get_generator(program, *iter); 14 15 if (generator == NULL) { 16 17 pwarning(1, "Unable to get a generator for \"%s\".\n", iter->c_str()); 18 19 } else { 20 21 pverbose("Generating \"%s\"\n", iter->c_str()); 22 23 generator->generate_program(); 24 25 delete generator; 26 27 } 28 29 }
本函数实现的功能就是以上这些功能,至于具体生成语言代码的功能在各种语言生成的类中实现,后面会详细分析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 |
检查所有元素抛出的异常是真实的异常 |