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

3 生成C++语言代码的代码详解

这个功能是由t_cpp_generator类实现(在文件t_cpp_generator.cc定义和实现),直接继承至t_oop_generator类(这个类是所有面向对象语言生成器类的直接基类,封装了面向对象语言生成器共有的特征与行为),而t_oop_generator又从t_generator继承(上面已经介绍),下面详细分析这个类是怎样生成C++语言的代码文件的。这个还有从上面介绍的generate_program函数开始说起,因为这个函数才是控制整个代码生成的总枢纽。

首先执行的是构造函数,这个构造函数做了一些最基本的初始化,一个是传递拥有生成代码的符号资源的t_program对象到父类,第二个功能就是根据可选项参数初始化一些bool变量,以便后面根据这些bool变量做相应的处理,代码很简单就不列出来了,下面用一个表格说明各个bool变量的作用(或功能)。

gen_pure_enums_

是否生成纯净的枚举类型,而不是采用类包装的形式

gen_dense_

是否应该为TDenseProtocol生成本地反射的元数据。

gen_templates_

是否要生成模板化的读/写方法

use_include_prefix_

是否应该为了thrift生成的其他头文件在#include中使用前缀路径

gen_cob_style_

是否应该生成继承扩展功能类(主要是异步)

gen_no_client_completion_

是否应该省略客户端类调用completion__()

构造函数只是做了最基本的初始化,更详细的初始化是上面介绍的代码生成器初始化函数init_generator,那我们看看C++代码生成器是怎么详细初始化的,都做了一些什么样的工作和实现了一些什么的功能。我们分步骤介绍这一个函数:

第一步:制作代码文件的输出目录:MKDIR(get_out_dir().c_str());MKDIR是一个宏函数,调用了mkdir来创建目录;

第二部:创建代码头文件和实现文件,如果需要生成模板化的读和写的方法还会创建一个文件单独实现,代码如下:

 string f_types_name = get_out_dir()+program_name_+”_types.h”; 
  f_types_.open(f_types_name.c_str()); 
  string f_types_impl_name = get_out_dir()+program_name_+”_types.cpp”; 
  f_types_impl_.open(f_types_impl_name.c_str()); 
  if (gen_templates_) { 
  string f_types_tcc_name = get_out_dir()+program_name_+”_types.tcc”; 
  f_types_tcc_.open(f_types_tcc_name.c_str()); 
  } 

这里需要说明几个用于输出流的成员变量,之所以定义成员变量是因为很多函数会用到,这样就不用用参数来传递它们了,它们定义和说明如下:

 std::ofstream f_types_;//专门用于类型声明的输出流,也就是头文件(.h文件) 
 std::ofstream f_types_impl_;//专门用于类型实现的输出流,也就是实现文件(.cpp文件) 
  std::ofstream f_types_tcc_;//专门用于模板化的读和写方法实现的输出流 
  std::ofstream f_header_;//专门用于服务声明生成的输出流 
  std::ofstream f_service_;//专门用于服务实现生成的输出流 
  std::ofstream f_service_tcc_;//专门用于模板的服务的输出流 

第三步:为每个文件打印头部注释,注释的作用就是说明这个文件是由Thrift自动生成的,代码如下:

 f_types_ << autogen_comment(); 
 f_types_impl_ << autogen_comment(); 
 f_types_tcc_ << autogen_comment(); 

第四步:开始ifndef

第五步:包含各种头文件

第六步:打开命名空间,生成的代码都是在一个命令空间里面的。

以上步骤的功能都比较简单,主要就是注意输出格式和逻辑处理。通过这些功能基本内容都做好了,下面就是真正开始生成具体类型和服务的时候了,每一种数据类型都由一个单独的函数来负责生成为代码。

(1)枚举类型生成函数generate_enum

首先在头文件中生成定义枚举类型的代码,具体的过程就是得到枚举的所有常量值和枚举类型的名称,然后根据C++定义枚举类型的语法输出代码到头文件,输出过程中根据是否需要用类来包装而所有不同,同时生成的代码也需要格式控制。具体实现如下:

 vector<t_enum_value*> constants = tenum->get_constants(); 
  std::string enum_name = tenum->get_name(); 
  if (!gen_pure_enums_) { 
  enum_name = “type”; 
  f_types_ << indent() << “struct ” << tenum->get_name() << ” {” << endl; 
  indent_up(); 
  } 
  f_types_ << indent() << “enum ” << enum_name; 
  generate_enum_constant_list(f_types_, constants, “”, “”, true); 
  if (!gen_pure_enums_) { 
  indent_down(); 
  f_types_ << “};” << endl; 
  } 
  f_types_ << endl; 

接着在后面在实现文件中定义一个整型数组和一个字符的数组并用定义的枚举类型的常量值来初始化这两个数组,后然在说这两个数组的值初始化一个map,其实这么做的目的就是为了测试这个枚举类型定义是否正确。

最后调用函数generate_local_reflection决定是否为TDenseProtocol协议生成对应类型的本地反射类型。这个函数功能比较复杂,后面单独详细讲解。

(2)类型定义生成函数generate_typedef

此函数功能简单,就是在头文件中生成一个typedef的定义,就只有一句实现:

 f_types_ <<  indent() << “typedef ” << type_name(ttypedef->get_type(), true) << ” ”  
 << ttypedef->get_symbolic() << “;” << endl << endl; 

(3)常量类型生成函数generate_consts

常量类型的实现是采用一个类来包装所有的常量并且使用单独的文件来实现,所有首先创建常量类型定义头文件和实现文件,代码如下:

 string f_consts_name = get_out_dir()+program_name_+”_constants.h”; 
 ofstream f_consts; 
 f_consts.open(f_consts_name.c_str()); 
 string f_consts_impl_name = get_out_dir()+program_name_+”_constants.cpp”; 
 ofstream f_consts_impl; 
 f_consts_impl.open(f_consts_impl_name.c_str()); 

接着按照就开始按照类的定义格式在头文件中生成定义类的代码并在实现文件中定义这个类的常量类型;在这个类的构造函数中给定义的数据类型赋值:

 f_consts_impl << “const ” << program_name_ << “Constants g_” << program_name_ << “_constants;” << endl << 
     endl << program_name_ << “Constants::” << program_name_ << “Constants() {” << endl; 
 indent_up(); 
 for (c_iter = consts.begin(); c_iter != consts.end(); ++c_iter) { 
     print_const_value(f_consts_impl, (*c_iter)->get_name(), (*c_iter)->get_type(), (*c_iter)->get_value()); 
   } 
 indent_down(); 
 indent(f_consts_impl) << “}” << endl; 

其中调用了print_const_value函数来根据数据类型来赋值,这些定义在类中的成员变量本身不是常量类型,只是在实现文件中定义了一个类的全局常量对象,在头文件中声明,以便其他地方可以被使用。

(4)异常类型生成函数generate_xception

这个函数其实调用下面需要详细分析的一个函数实现的,就是generate_struct函数,因为异常也是通过结构体来定义和实现的。不过C++语言生成器中也自己实现了这个函数,不过它是调用generate_cpp_struct函数实现,C++的generate_struct函数也是调用这个函数实现,只是传递一个bool变量来区分是否是异常类型,具体的实现在分析generate_struct函数时一起详细分析了,因为它们的基本实现功能都是相同的。

(5)结构体类型生成函数generate_struct

上面已经说了这个函数也是调用generate_cpp_struct函数实现,也就是说异常类型和结构体类型都是用同样的流程实现的,它们都是定义为一个类,只是异常都从TException继承,而一般的结构体没有。首先调用函数generate_struct_definition在头文件中生成定义类的代码,这个过程如下:

第一步:得到所有的成员变量;

第二步:根据是否有可选成员决定是否定义一个结构体_XXX_isset,这个结果主要针对需要定义的类的可选成员而定义一些bool变量,来标识这些可选成员变量是否存在。设计这个功能的目的是为了灵活控制数据传输的结构;

第三步:开始生成定义类(IDL文件中定义的struct在C++都是用class来实现)的代码,生成的代码主要包括默认的构造函数、析构函数、各个字段、比较函数(等于、不等于和小于)等;

第四步:最后一步生成一个模板的读和写数据的函数的声明,模板参数是协议类型,实现代码如下:

 if (read) {//读数据的模板函数 
     if (gen_templates_) { 
  out <<indent() << “template <class Protocol_>” << endl << 
  indent() << “uint32_t read(Protocol_* iprot);” << endl; 
     } else { 
       out << indent() << “uint32_t read(” << “::apache::thrift::protocol::TProtocol* iprot);” << endl; 
     } 
 } 
 if (write) {//写数据的模板函数 
     if (gen_templates_) { 
  out << indent() << “template <class Protocol_>” << endl << 
  indent() << “uint32_t write(Protocol_* oprot) const;” << endl; 
     } else { 
       out << indent() << “uint32_t write(” << “::apache::thrift::protocol::TProtocol* oprot) const;” << endl; 
     } 
 } 

然后调用函数generate_struct_fingerprint在实现文件中初始化两个静态变量,一个是字符串,一个是8位的整型数组,这两个变量都是用来唯一的标识一个类。这个歌标识符的作用就是用于生成本地的反射类型,当使用TDenseProtocol协议传输数据时会用到。

接着两次调用generate_local_reflection函数分别来声明和定义用于类的本地反射的类型,调用generate_local_reflection_pointer函数来生成一个类的静态指针的本地反射类型。

最后分别调用函数generate_struct_reader和generate_struct_writer实现数据读和写函数。到此整个IDL定义的struct类型生成为C++的代码就完成了。

(6)服务类型生成函数generate_service

这个函数的功能是最复杂的,它会做很多的工作(分别调用其它函数来实现),也会生成单独的头文件和实现文件。生成头文件的代码如下:

 string f_header_name = get_out_dir()+svcname+”.h”; 
  f_header_.open(f_header_name.c_str()); 

下面就开始在头文件中生成一些包含头文件的代码。

生成实现文件的代码:

 string f_service_name = get_out_dir()+svcname+”.cpp”; 
  f_service_.open(f_service_name.c_str()); 

后面也是生成一些包含头文件的代码。接着就开始生成正在的各种实现这个服务的代码了,如下:

 generate_service_interface(tservice, “”);//生成服务的接口类(在C++为抽象类) 
  generate_service_null(tservice, “”);//生成一个空实现服务接口类的类 
  generate_service_helpers(tservice);//生成一些帮助类,如参数类、返回结果类等 
  generate_service_client(tservice, “”);//生成一个客户类 
  generate_service_processor(tservice, “”);//生成处理数据的类(就是生成用于远程调用) 
  generate_service_multiface(tservice);//生成一个实现多接口的单一的服务器类 
  generate_service_skeleton(tservice);//生成一个服务器的框架文件 
 	如果gen_cob_style_为true,还会生成一些扩展功能的类,代码如下: 
 if (gen_cob_style_) { 
     generate_service_interface(tservice, “CobCl”); 
     generate_service_interface(tservice, “CobSv”); 
     generate_service_null(tservice, “CobSv”); 
     generate_service_client(tservice, “Cob”); 
     generate_service_processor(tservice, “Cob”); 
     generate_service_async_skeleton(tservice); 
 } 

到此C++的代码的生成全部结束,最后调用close_generator函数来完成收尾工作和清理一些资源,如果关闭文件。

(7)总结

对于生成C++代码这一块内容把基本的生成过程详细分析了一遍,主要集中在整个流程中。但是很多功能还没有详细分析或还没有涉及到,因为整个代码有4千多行,要完全详细用文字分析下来工作量很多(代码肯定都看了一遍),而且也觉得没有必要,因为很多功能实现都挺简单,只要一看代码便能够理解。

上面分析过程没有提到的功能主要包括:数据的序列化和反序列化、具体生成服务需要的每一个类等等。其实整个代码并没有什么难点,主要是必须要思考周全,还有就是注意生成C++代码的合理性。下面把这个C++代码生成过程函数的调用层次用图形表示如下:

本来打算继续详细分析Java和Python的代码生成的代码,但是我阅读了这部分代码,发现和C++基本相同,只是由于各种语言语法不相同而在生成代码的时候处理不同,但是处理方法和流程都是一样的,所以就不详细分析了,可以参照C++的生成代码对照分析。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏开源优测

[接口测试_B] 14 pytest+requests实战-参数化

上一篇在一个py文件中,写了一堆test_开头的方法,所有数据和用例都在一个py文件中,本篇尝试读取json文件的测试数据,执行用例。

1674
来自专栏对角另一面

lodash源码分析之缓存方式的选择

本文为读 lodash 源码的第八篇,后续文章会更新到这个仓库中,欢迎 star:pocket-lodash

2029
来自专栏Vamei实验室

来玩Play框架03 模板

在上一章节中,我把字符串通过ok()返回给客户。我可以把一个完整的html页面放入字符串中返回。然而,现代的框架都提供了更好的方法——模板。模板将视图和数据分开...

2025
来自专栏电光石火

获取URL地址中的GET参数

/*-----------------实现1--------------------*/ function getPar(par){ //获取当前URL...

2089
来自专栏流媒体人生

linux eval

eval 就是执行以下两个步骤 1.第一次,执行变量替换,类似与C语言的宏替代

862
来自专栏好好学java的技术栈

「附数据结构资源」玩转java并发(六):深入线程Thread类的start()方法和run()方法

java的线程是通过java.lang.Thread类来实现的。VM启动时会有一个由主方法所定义的线程。可以通过创建Thread的实例来创建新的线程。每个线程都...

1062
来自专栏电光石火

获取URL地址中的GET参数

/*-----------------实现1--------------------*/ function getPar(par){ //获取当前URL...

2339
来自专栏武军超python专栏

2018年7月23日数据存储到文件中的代码介绍:

******************************************************************

1045
来自专栏desperate633

第7课 创建计算字段拼接字段执行简单的算术运算

什么是计算字段? 就是直接从数据库中检索出转换,计算或者格式化的数据,而不是检索出数据之后,再在客户端应用程序中重新格式化。

732
来自专栏python 实践经验

import导入第三方库或者模块

通常模块为一个文件,直接使用 import 文件名 就可以导入。可以作为module的文件类型有".py"、".pyo"、".pyc"、".pyd"、...

2135

扫码关注云+社区

领取腾讯云代金券