Protobuf 反射技术简介

这篇具有很好参考价值的文章主要介绍了Protobuf 反射技术简介。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

对于反射大家应该不会陌生,如果你接触过一些框架(如 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::cout<<reflecter->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 文件来源于何处,我们都需要对其做进一步的处理,将其解析成内存对象,并构建其与实例的映射,同时也要计算每个字段的内存偏移。可总结出如下步骤:

  1. 提供 .proto (范指 ProtoBuf Message 语法描述的元信息)
  2. 解析 .proto 构建 FileDescriptor、FieldDescriptor 等,即 .proto 对应的内存模型(对象)
  3. 之后每创建一个实例,就将其存到相应的实例池中
  4. 将 Descriptor 和 instance 的映射维护到表中备查
  5. 通过 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::cout<<reflecter->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.ccxxx.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\010int3"
      "2Val\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 =
    &DefaultRaw<ArenaStringPtr>(field).Get();

// DefaultRaw 底层调用:
// reinterpret_cast<const uint8*>
// (default_instance_) +
//     OffsetValue(offsets_[field->index()], field->type());

// .......

// assign 赋值
MutableField<ArenaStringPtr>(message, field)
    ->Mutable(default_ptr, GetArena(message))
    ->assign(std::move(value));

其他 SetInt32、SetInt64、SetBool 等等接口原理类似。

ProtoBuf 的源码十分庞大,本节只是解析了其中一条路径,但已经足够呈现 ProtoBuf 的反射原理。如果想了解更多工程细节可进一步阅读 ProtoBuf 源码。文章来源地址https://www.toymoban.com/news/detail-816102.html

到了这里,关于Protobuf 反射技术简介的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 分布式计算平台 Hadoop 简介

    Hadoop是一种分析和处理大数据的软件平台,是一个用Java语言实现的Apache的开源软件框架,在大量计算机组成的集群中实现了对海量数据的分布式计算。其主要采用MapReduce分布式计算框架,包括根据GFS原理开发的分布式文件系统HDFS、根据BigTable原理开发的数据存储系统HBase以及

    2024年02月01日
    浏览(61)
  • 分布式系统架构设计之分布式缓存技术选型

    随着互联网业务的快速发展,分布式系统已经成为了解决大规模并发请求、高可用性、可扩展性等问题的重要手段。在分布式系统中,缓存作为提高系统性能的关键技术,能够显著降低数据库负载、减少网络延迟、提高数据访问速度。当面对大量并发请求时,如果每次都直接

    2024年02月03日
    浏览(118)
  • SpringCloud入门实战(十五)分布式事务框架Seata简介

    📝 学技术、更要掌握学习的方法,一起学习,让进步发生 👩🏻 作者:一只IT攻城狮 ,关注我,不迷路 。 💐学习建议:1、养成习惯,学习java的任何一个技术,都可以先去官网先看看,更准确、更专业。 💐学习建议:2、然后记住每个技术最关键的特性(通常一句话或者

    2024年02月10日
    浏览(42)
  • Git分布式版本控制工具和GitHub(一)--简介

    1.Git简介 【1】什么是Git? Git就是代码版本管理工具。 【2】为什么要使用Git (1)版本控制 写代码就是不断写BUG的过程(当然我们是不会这么说的),很多时候你写了100行代码之后,突然醒悟! ​ “这是什么鬼?” “怎么不能跑了?”​ 如果没有版本留存,你想要回到昨天

    2024年02月14日
    浏览(61)
  • AI框架:9大主流分布式深度学习框架简介

    转载翻译Medium上一篇关于分布式深度学习框架的文章 https://medium.com/@mlblogging.k/9-libraries-for-parallel-distributed-training-inference-of-deep-learning-models-5faa86199c1fmedium.com/@mlblogging.k/9-libraries-for-parallel-distributed-training-inference-of-deep-learning-models-5faa86199c1f 大型深度学习模型在训练时需要大量内

    2024年02月09日
    浏览(49)
  • 【分布式技术】分布式存储ceph之RGW接口

    目录 1、对象存储概念  2、创建 RGW 接口 //在管理节点创建一个 RGW 守护进程 #创建成功后默认情况下会自动创建一系列用于 RGW 的存储池  #默认情况下 RGW 监听 7480 号端口  //开启 http+https ,更改监听端口 #更改监听端口 ​          //创建 RadosGW 账户 客户端测试 OSD 故障模拟

    2024年01月19日
    浏览(62)
  • 【分布式技术】分布式存储ceph之RBD块存储部署

    目录 创建 Ceph 块存储系统 RBD 接口 服务端操作 1、创建一个名为 rbd-demo 的专门用于 RBD 的存储池 2、将存储池转换为 RBD 模式 3、初始化存储池 4、创建镜像 5、在管理节点创建并授权一个用户可访问指定的 RBD 存储池 6、修改RBD镜像特性,CentOS7默认情况下只支持layering和stripin

    2024年01月18日
    浏览(75)
  • 【分布式技术专题】「分布式技术架构」 探索Tomcat技术架构设计模式的奥秘(Server和Service组件原理分析)

    Tomcat的总体结构从外到内进行分布,最大范围的服务容器是Server组件,Service服务组件(可以有多个同时存在),Connector(连接器)、Container(容器服务),其他组件:Jasper(Jasper解析)、Naming(命名服务)、Session(会话管理)、Logging(日志管理)、JMX(Java 管理器扩展服务

    2024年01月24日
    浏览(52)
  • 分布式补充技术 01.AOP技术

    01.AOP技术是对于面向对象编程(OOP)的补充。是按照OCP原则进行的编写,(ocp是修改模块权限不行,扩充可以) 02.写一个例子: 创建一个新的java项目,在main主启动类中,写如下代码。 一个接口,一个接口实现类,一个main主方法。 03.如果要实现显示接口实现类中的send方法运行

    2024年02月07日
    浏览(46)
  • 分布式技术剖析

    随着企业数字化进程的进一步深入,企业为了解决大数据的“4个V”问题,往往需要构建多个不同技术栈的大数据平台,其中不乏会使用到分布式相关的存储、计算、资源管理技术。分布式系统的出现解决了单机系统无法解决的成本、效率和高可用问题。那么什么是分布式技

    2023年04月10日
    浏览(35)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包