前言

近期的 protobuf v22和 gRPC v1.55 版本在构建流程层面引入了一些比较大的变化。 最初我关注到这个问题是在我参与的一个社区项目 opentelemetry-cpp 的issue中( https://github.com/open-telemetry/opentelemetry-cpp/issues/2095 )。 直到后来,我们在自己的构建系统 cmake-toolsetprotobufgRPC 也进行了升级。所以顺带给社区的项目也提交了一些相关的Patch,在这里分享一下可能其他同学也会碰到。

protobuf 引入了新依赖

protobuf 从v22开始引入了对 abseil-cpp 的依赖。和 gRPC 类似,它也可以通过 "-Dprotobuf_ABSL_PROVIDER=package" 告知构建系统从已安装位置查找,而不是自己重新构建一套。 另外对构建流程的影响就是,我在 cmake-toolset 中把 abseil-cpp 单独抽离出来并放在了 protobuf 之前了。 同时在我们的UE工程里依赖protobuf的位置也要链接 abseil-cpp 的相关依赖库,比如 absl_strings, absl_bad_variant_access 等等。

同时,它还移除了一些和 abseil-cpp 内重复的接口,比如说原来做Base64转换的接口被移除了,现在适配 Base64Escape 的代码更长了, 比如下面对 opentelemetry-cpp 的PR #2163

#if defined(HAVE_ABSEIL)
#  include "absl/strings/escaping.h"
#elif defined(GOOGLE_PROTOBUF_VERSION) && GOOGLE_PROTOBUF_VERSION >= 3007000
#  include "google/protobuf/stubs/strutil.h"
#else
#  include "google/protobuf/stubs/port.h"
#  include "google/protobuf/stubs/stringpiece.h"
namespace google
{
namespace protobuf
{
LIBPROTOBUF_EXPORT void Base64Escape(StringPiece src, std::string *dest);
}  // namespace protobuf
}  // namespace google
#endif

// ...
      std::string base64_value;
#if defined(HAVE_ABSEIL)
      absl::Base64Escape(bytes, &base64_value);
#else
      google::protobuf::Base64Escape(bytes, &base64_value);
#endif

gRPC v1.55 和 upb

去年我们项目组尝试使用 protobuf 官方的 upb 来实现lua集成,并写了一篇相关的分享: 《集成 upb 和 lua binding 的踩坑小记》 。 当时的主要问题是 gRPC 中集成了一个精简版的 upb,只包含运行时,不包含protoc-upb,protoc-upbdefs和protoc-upblua插件。 当我们要使用完整版本时,就需要自己编译出这几个组件,并且和 gRPC 混用的时候还需要版本保持一致,以防出现ABI兼容性问题。

upb 主要使用的是 bazel 构建系统,而我们使用 cmakegRPC 也支持 cmake。由于 upb 的外部依赖只有一项,且使用的功能比较简单。所以在 upb 的仓库里有一个简单的工具,去hook了 bazel 的基础接口,输出 cmake 的工程文件。 由于这个输出的 cmake 的工程文件只包含了运行时,不包含上面提到的几个插件的编译,所以我们就需要是修改这个工具,让它也能输出插件和常见的 protobuf 的well known type的upb支持文件。

在之前版本中,几乎是手夯了这几个工具及依赖构建流程。然后由于依赖中有直接源码引入第三方库 utf8_range,并且文件的位置相当随意。所以当时也是重新调整了一些依赖文件的位置,以减少未来冲突的可能性。另外由于原有导出的 cmake 工程文件不支持导出现代化 cmake 的CONFIG的package文件,所以也需要我们自己做支持来实现更好的依赖关系管理。 这些修订都可以在 https://github.com/atframework/cmake-toolset/blob/8c8659f2d64283b02a24b7c34ffcd0d24eb03ca6/ports/grpc/upb-e4635f223e7d36dfbea3b722a4ca4807a7e882e2.patch 。这里面除了上述功能外还有一些对于高版本 lua 适配和某些接口使用是UB的修订。(这些修订已经推送上游,详见:https://github.com/protocolbuffers/upb/pull/735https://github.com/protocolbuffers/upb/issues/734 。)

在新版本的 protobufgRPC 中,protobuf 在开启单元测试时也依赖 utf8_range 了,但是我们出预编译包可以不开启单元测试。而新版本 gRPC 也更新了新版的 upb,这个变化就比较大了。

首先是bootstrap编译的过程增加了多个stage。stage0用预制的 descriptor.upb.hdescriptor.upb.cc 等等编译 protoc-upb 插件;然后用这个插件生成stage1阶段的 descriptor.upb.hdescriptor.upb.cc 编译 protoc-upb 插件;最后用stage1阶段生成的 protoc-upb 插件生成 descriptor.upb.hdescriptor.upb.cc 等去生成stage2阶段的protoc-upb 插件、protoc-upbdefs 插件和protoc-upblua 插件等等。最后发布的是stage2阶段的库和工具。同时每个stage都有一系列的依赖组件,需要去设置依赖链。

同时 upb 也升级了对 utf8_range 的版本引用。新版本 utf8_range 已经支持 cmake 的CONFIG的package包管理了。

为了我们整体可以用统一并且兼容性更好的管理方式,并且上面bootstrap编译过程全部手夯以后太难维护了,所以我干脆实现了原先 upb 里留空的 def cc_binary(self, **kwargs): , def bootstrap_upb_proto_library(self, **kwargs): , def bootstrap_cc_library(self, **kwargs):def bootstrap_cc_binary(self, **kwargs): 。其中 def bootstrap_upb_proto_library(self, **kwargs): 比较复杂一些,三个stage不一样。

另外新版本的 upb 也依赖 abseil-cpp 了,但是 abseil-cppbazel 的target名称和 cmake 不完全一样,所以针对这些名字我们仍然需要手动转换。同理对 protobuf 的libprotoc 相关的依赖也一样。

对这些变化的初版可用版本我放在了 https://github.com/atframework/cmake-toolset/blob/8c8659f2d64283b02a24b7c34ffcd0d24eb03ca6/ports/grpc/upb-61a97efa24a5ce01fb8cc73c9d1e6e7060f8ea98.patch ,这个版本对应 gRPC v1.55 里引用的版本。然而 upb 的主干分支这个构建系统的流程又有一些变化和优化,为了方便以后维护,我就推了一个PR到 upb 主仓库,详见: https://github.com/protocolbuffers/upb/pull/1352/ 和两个相关的功能的issue: https://github.com/protocolbuffers/upb/issues/1350https://github.com/protocolbuffers/upb/issues/1351 。不过按之前推的PR的响应速度,感觉这个仓库对PR的响应非常慢。

写在最后

以上这些适配其实都是为了给 cmake-toolset 下一次升级做准备,但是现在还有一个上游库的支持没完成 (opentelemetry-cpp 对protobuf v22+的支持,详见: https://github.com/open-telemetry/opentelemetry-cpp/pull/2163 ),所以现在是新拉了一个分支,尚未合入。这个仓库因为这个提交可能影响ABI兼容性,所以社区还需要继续评估。等最终这个上游仓库合入完成了,我们就可以给 cmake-toolset 也切到新版本了。

最后,欢迎对相关话题有兴趣的小伙伴们互相交流。