手机网站建设文章,直播平台开发多少钱,ui设计接单价目表,女性时尚资讯+淘宝客模式系列网站源码对于反射大家应该不会陌生#xff0c;如果你接触过一些框架#xff08;如 ORM、IOC、OSGi 等#xff09; 的内部实现#xff0c;应该更能体会反射技术的应用可谓无处不在。
反射概念最早出现于人工智能领域#xff0c;20 世纪 70 年代末被引入到程序语言设计中。1982 年 …对于反射大家应该不会陌生如果你接触过一些框架如 ORM、IOC、OSGi 等 的内部实现应该更能体会反射技术的应用可谓无处不在。
反射概念最早出现于人工智能领域20 世纪 70 年代末被引入到程序语言设计中。1982 年 MIT 的 Smith, Brian Cantwell 在他的博士论文中最早提出了程序反射的概念 既然我们可以构造“有关某个外部世界表示”的计算过程, 并通过它来对那个外部世界进行推理; 那么我们也可以构造能够对自身表示和计算进行推理的计算过程 ,它包含负责管理有关自身的操作和结构表示的内部过程。 —— 1982年 Smith, Brian Cantwell 博士论文首次提出 从某种角度来看所谓编程实际上就是在构造 “关于外部世界” 的计算过程。如果用 F 表示这个构造过程用 X 表示外部世界那么编写一个计算系统可表示为 F(X)。
那么非常有趣的点就在于我们完全可以构造对上述过程本身进行描述和推理的计算过程。即将 F(X) 视为新的 “世界” 和 研究对象构造 F(F(X))。
我们平时编写的计算系统是面向特定领域的通常是面向现实建模, 系统中包含 用来描述领域中的实体 和 实体间关系 的数据结构以及处理这些数据结构的规则。那么反射系统面向领域便是这个系统本身。
从上层概念往下走很容易就能理解反射将为我们提供这样的能力 计算机程序在运行时可以访问、检测和修改它本身状态或行为 —— 反射 (计算机科学) Wikipedia 很多编程语言可对程序本身进行构建如何构建见下文从而为程序员提供反射能力即可在运行时访问和修改自身状态和行为
// 运行时动态创建对象
Class? class1 Class.forName(cn::lcy::dog);
Dog dogObj (Dog) class1.newInstance();
// 运行时动态访问状态
Field dogNameField class1.getDeclaredField(name);
// 运行时动态修改状态
dogNameField.set(dogObj, 鸡你太美);
// 运行时动态修改行为
Method method class1.getMethod(signJumpRapAndBall);
method.invoke(dogObj);例子中写的是字符串常量形式如 cn::lcy::dog 但实际上 cn::lcy::dog 等可以是一个动态变化的变量它可以由程序计算生成可以从本地文件中读取也可以通过网络从远程获取这便是运行时动态的含义。
站在使用者的角度通过上面例子应该可以对反射有一定的直观感受。
而站在实现者的角度编程语言是如何实现程序反射的呢
如果用一句话来总结反射实现的关键笔者会概括为获取系统元信息 元信息即系统自描述信息用于描述系统本身。举例来讲即系统有哪些类类中有哪些字段、哪些方法字段属于什么类型、方法又有怎样的参数和返回值… 对于上述例子的 Java 语言而言其能够提供反射能力的关键是在编译阶段将程序的元信息编译进了 .class 文件在程序运行时 JVM 将会把 .class 文件加载到 JVM 内存模型中的方法区。此后程序运行时将有能力获取关于自身的元信息。除了 Java 语言之外JS、Python、GO、PHP 等各种语言也都在语言层面实现了程序反射。
而由于 C 在编译时并不会将类的元信息写进结果中最终编译结果中只会包含变量、函数地址偏移、函数关系等所以 C 自身无法获取元信息那么自然无法提供反射能力。但这并不意味着使用 C 就无法应用反射技术程序可以自己设计与实现程序元信息以及元信息与地址之间的映射。例如本文所要介绍的 ProtoBuf 反射实现。
ProtoBuf 反射技术简介
与其它语言提供的反射类似ProtoBuf 能够为使用者提供了如下的反射能力
/* 反射创建实例 */
auto descriptor google::protobuf::DescriptorPool::generated_pool()-FindMessageTypeByName(Dog);
auto prototype google::protobuf::MessageFactory::generated_factory()-GetPrototype(descriptor);
auto instance prototype-New();/* 反射相关接口 */
auto reflecter instance.GetReflection();
auto field descriptor-FindFieldByName(name);
reflecter-SetString(instance, field, 鸡你太美) ;// 获取属性的值.
std::coutreflecter-GetString(instance , field) std::endl ;
return 0 ;利用上述 ProtoBuf 的反射能力我们将能够实现许多强大的功能。例如各种 pb2json 库底层多是利用 ProtoBuf 的反射能力实际上ProtoBuf 自身对编码结果反序列化并构建内存对象的过程用的也正是反射。
正如上一节提到的反射的核心要点是获取程序元信息。
ProtoBuf 自然也不会例外那么 ProtoBuf 反射所需的元信息在哪答案便是使用 ProtoBuf 的第一步就会接触到的.proto 文件。
ProtoBuf 反射原理概述
我们在 深入 ProtoBuf - 简介 一文中介绍过使用 ProtoBuf 的第一步便是创建 .proto 文件定义我们所需的数据结构。但很多人没有意识到这个过程同时也是为 ProtoBuf 提供我们数据元信息的过程这些元信息包括数据由哪些字段构成字段又属于什么类型以及字段之间的组合关系等。
当然元信息也并非一定由 .proto 文件提供它也可来自于网络或其它可能的输入只要它满足 ProtoBuf Message 的定义语法即可。那么元信息的可能来源和处理就有 .proto 文件 使用 ProtoBuf 内置的工具 protoc 编译器编译protoc 将 .proto 文件内容编码并写入生成的代码中.pb.cc 文件使用 ProtoBuf 提供的编译 API 在运行时手动指编码解析 .proto 文件内容。实际上 protoc 底层调用的也正是这个编译 API。 非 .proto 文件 从远程读取如将数据与数据元信息一同进行 protobuf 编码并传输 message Req {optional string proto_file 1;optional string data 2;
}从 Json 或其它格式数据中转换而来 …
无论 .proto 文件来源于何处我们都需要对其做进一步的处理将其解析成内存对象并构建其与实例的映射同时也要计算每个字段的内存偏移。可总结出如下步骤
提供 .proto 范指 ProtoBuf Message 语法描述的元信息解析 .proto 构建 FileDescriptor、FieldDescriptor 等即 .proto 对应的内存模型对象之后每创建一个实例就将其存到相应的实例池中将 Descriptor 和 instance 的映射维护到表中备查通过 Descriptor 可查到相应的 instance又由于了解 instance 中字段类型FieldDescriptor所以知道字段的内存偏移那么就可以访问或修改字段的值
ProtoBuf 反射源码解析
反射源码有许多阅读路径在上一节中列出了元信息的不同来源不同来源元信息就会导致处理上有所不同。我们没有必要解读源码中所有的实现路径这里只选择最直观也是我们接触最多的一种路径。
重看 ProtoBuf 反射技术简介 中的例子可对这个例子一步步深入从而理解反射实现的原理代码 4-1
/* 反射创建实例 */
auto descriptor google::protobuf::DescriptorPool::generated_pool()-FindMessageTypeByName(Dog);
auto prototype google::protobuf::MessageFactory::generated_factory()-GetPrototype(descriptor);
auto instance prototype-New();/* 反射相关接口 */
auto reflecter instance.GetReflection();
auto field descriptor-FindFieldByName(name);
reflecter-SetString(instance, field, 鸡你太美) ;// 获取属性的值.
std::coutreflecter-GetString(instance , field) std::endl ;
return 0 ;一、 代码 4-1 第一步我们通过 DescriptorPool 的 FindMessageTypeByName 获得了元信息 Descriptor。
DescriptorPool 为元信息池对外提供了诸如 FindServiceByName、FindMessageTypeByName 等各类接口以便外部查询所需的元信息。当 DescriptorPool 不存在时需要查询的元信息时将进一步到 DescriptorDatabase 中去查找。
DescriptorDatabase 可从硬编码或磁盘中查询对应名称的 .proto 文件内容解析后返回查询需要的元信息。
DescriptorPool 相当于缓存了文件的 Descriptor底层使用 Map查询时将先到缓存中查询如果未能找到再进一步到 DB 中即 DescriptorDatabase查询此时可能需要从磁盘中读取文件内容然后再解析成 Descriptor 返回这里需要消耗一定的时间。
从上面的描述不难看出DescriptorPool 和 DescriptorDatabase 通过缓存机制提高了反射运行效率但这只是反射工程实现上的一种优化我们更感兴趣的应该是 Descriptor 的来源。
DescriptorDatabase 从磁盘中读取 .proto 内容并解析成 Descriptor 这一来源很容易理解但我们大多数时候并不会采用这种方式反射时也不会去读取 .proto 文件。那么我们的 .proto 内容在哪
实际上我们在使用 protoc 生成 xxx.pb.cc 和 xxx.pb.h 文件时其中不仅仅包含了读写数据的接口还包含了 .proto 文件内容。阅读任意一个 xxx.pb.cc 的内容你可以看到如下类似代码代码 4-2
static void AddDescriptorsImpl() {InitDefaults();// .proto 内容static const char descriptor[] GOOGLE_PROTOBUF_ATTRIBUTE_SECTION_VARIABLE(protodesc_cold) {\n\022single_int32.proto\\035\n\010Example1\022\021\n\010int32Val\030\232\005 \001(\005\ \n\010Example2\022\024\n\010int32Val\030\377\377\377\377\001 \003(\005b\006proto3};// 注册 descriptor::google::protobuf::DescriptorPool::InternalAddGeneratedFile(descriptor, 93);// 注册 instance::google::protobuf::MessageFactory::InternalRegisterGeneratedFile(single_int32.proto, protobuf_RegisterTypes);
}其中 descriptor 数组存储的便是 .proto 内容。这里当然不是简单的存储原始文本字符串而是经过了 SerializeToString 序列化处理而后将结果以硬编码的形式保存在 xxx.pb.cc 中真是充分利用了自己的高效编码能力。
硬编码的 .proto 元信息内容将以懒加载的方式被调用时才触发被 DescriptorDatabase 加载、解析并缓存到 DescriptorPool 中。
二、 代码 4-1 例子中的第二步是根据 MessageFactory 获得了一个实例。
MessageFactory 是实例工厂对外提供了根据元信息 descriptor 获取相应实例的能力。
其实在代码 4-2 中已经涉及到该工厂即
// 注册对应 descriptor 的 instance 到 MessageFactory
// InternalRegisterGeneratedFile 函数内部会将创建一个实例并做好 descriptor 与 instance 的映射
::google::protobuf::MessageFactory::InternalRegisterGeneratedFile(single_int32.proto, protobuf_RegisterTypes);每次构建实例后都将 descriptor 和 instance 维护到一个 _table 中即映射表以便获取。后续所谓通过反射获得某个类的某个实例子实际就是查表的过程。
三、 代码 4-1 例子中的第三步就是对 instance 实例对象的属性进行读写。
实例对象的 reflection 里面存储了对象属性的偏移地址而这些信息其实与 .proto 内容信息一样在 protoc 编译时通过解析 proto 文件内容获得且记录在 xxx.pb.cc 中阅读 xxx.pb.cc 代码可看到如下类似代码
const ::google::protobuf::uint32 TableStruct::offsets[] GOOGLE_PROTOBUF_ATTRIBUTE_SECTION_VARIABLE(protodesc_cold) {~0u, // no _has_bits_// 将会计算实例与属性的内存偏移GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(::Example1, _internal_metadata_),~0u, // no _extensions_~0u, // no _oneof_case_~0u, // no _weak_field_map_GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(::Example1, int32val_),~0u, // no _has_bits_GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(::Example2, _internal_metadata_),~0u, // no _extensions_~0u, // no _oneof_case_~0u, // no _weak_field_map_GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(::Example2, int32val_),
};有了属性的内存偏移自然可以对属性进行读写操作以例子中出现的 SetString 为例其内部实现位于 generated_message_reflection.cc 中核心代码如下
// 获取属性内存地址指针内部根据 __
const std::string* default_ptr DefaultRawArenaStringPtr(field).Get();// DefaultRaw 底层调用
// reinterpret_castconst uint8*
// (default_instance_)
// OffsetValue(offsets_[field-index()], field-type());// .......// assign 赋值
MutableFieldArenaStringPtr(message, field)-Mutable(default_ptr, GetArena(message))-assign(std::move(value));其他 SetInt32、SetInt64、SetBool 等等接口原理类似。
ProtoBuf 的源码十分庞大本节只是解析了其中一条路径但已经足够呈现 ProtoBuf 的反射原理。如果想了解更多工程细节可进一步阅读 ProtoBuf 源码。