我的新浪微博: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 |
檢查所有元素拋出的異常是真實的異常 |