前言
年初的时候我们项目组的构建系统( cmake-toolset )里把 protobuf 升级到了 v20/v3.20 版本, gRPC 也升级到了 v1.54 版本。然而这两个版本在Linux的ELF ABI和MacOS的Macho ABI下都出现了一些符号未定义的问题(当然也包含Android和iOS)。 这些问题也不仅限于 protobuf v20/v3.20 和 gRPC v1.54,后续的版本有些修复了,有些没有。在官方完全修复之前,我们自己打了一些patch去修复这些问题。
protobuf 的链接和符号问题
InternalMetadata::~InternalMetadata()
未定义
protobuf的问题主要分两组,第一组报的是 InternalMetadata::~InternalMetadata()
未定义。这个问题存在于 v20/v3.20 和 v21/v3.21 。v22/v4.22 版本已修复(v22/v4.22在构建和依赖上有个很大的变化,后面再写分享说明)。这个问题的issue也可以见于 https://github.com/protocolbuffers/protobuf/issues/9947 。
其本质原因可以先看 v20/v3.20 版本的 metadata_lite.h#L73
#if defined(NDEBUG) || defined(_MSC_VER)
~InternalMetadata() {
if (HasMessageOwnedArenaTag()) {
delete reinterpret_cast<Arena*>(ptr_ - kMessageOwnedArenaTagMask);
}
}
#else
~InternalMetadata();
#endif
还有源文件 message_lite.cc#L528
// Non-inline implementations of InternalMetadata routines
#if defined(NDEBUG) || defined(_MSC_VER)
// for opt and MSVC builds, the destructor is defined in the header.
#else
// This is moved out of the header because the GOOGLE_DCHECK produces a lot of code.
InternalMetadata::~InternalMetadata() {
if (HasMessageOwnedArenaTag()) {
GOOGLE_DCHECK(!HasUnknownFieldsTag());
delete reinterpret_cast<Arena*>(ptr_ - kMessageOwnedArenaTagMask);
}
}
#endif
这里的本意是在非Debug模式下这个析构写头文件里,这样某些编译器和编译选项可以被自动内连,可以优化掉。而调试模式下有额外的检查走本地的符号。 但是这里的问题是,我们经常会在编译依赖库采用Release模式,而使用者可能处于Debug模式。这就意味着编译 protobuf 的时候是可能被优化掉而没有这个符号的。 但是使用者认为有这个符号,最终链接失败。
这个问题在 protobuf v21.4/v3.21.4 版本里进行了部分修复,但是某些编译环境还是有问题。
我们先来看 protobuf v21/v3.21 版本里 metadata_lite.h#L81 的代码。
~InternalMetadata() {
#if defined(NDEBUG) || defined(_MSC_VER)
if (HasMessageOwnedArenaTag()) {
delete reinterpret_cast<Arena*>(ptr_ - kMessageOwnedArenaTagMask);
}
#else
CheckedDestruct();
#endif
}
咋一看好像是没问题了。无论什么情况都有 ~InternalMetadata()
了,但是C++编译器在自动内联一说,😂。某些编译器在编译 .pb.cc
时如果走下面的分支自动内联了,那么就不会生成 ~InternalMetadata()
这个符号。这个类的析构在 MessageLite
这个类中被调用,在生成的 .pb.cc
里是配有被直接调用的。但是某些编译器会生成对它的析构符号的引用(可能也属于编译器的BUG)。
这时候又会导致符号未定义。
我们发现问题的环境是编译iOS版本时,具体编译器版本号忘记了,好像是AppleClang 12或者AppleClang 13。
解决这个问题也很简单,把声明改成 PROTOBUF_NOINLINE ~InternalMetadata()
就行了。
以上问题也可以在我们的构建系统项目中找到Patch文件(还包含少量其他问题的适配):
- https://github.com/atframework/cmake-toolset/blob/main/ports/protobuf/protobuf-v3.21.1.patch
- https://github.com/atframework/cmake-toolset/blob/main/ports/protobuf/protobuf-v3.21.4.patch
XXX_default_instance_
未定义
第二个问题是默认的instance符号未定义的问题。这个问题存在于 v21/v3.21 到目前最新版本 (v23/4.23)。我没有追查更早版本,大概率也有这个问题。
报的错误大致是 "struct XXX YYYY_default_instance_"
符号未定义。触发条件比较多:
- 需要编译成动态库
- 默认符号隐藏(Windows默认隐藏,Linux默认可见)
- 使用
dllexport_decl=
来设置导出符号
在Windows中个,每一个dll和exec都有自己的符号表和堆管理。所以当使用dll时,需要把要导出的符号设置为 __declspec(dllexport)/__attribute__((__dllexport__))
, 导入的时候设置为 __declspec((dllimport))/__attribute__((__dllimport__))
。
而在Linux里,默认是共享且全局可见的。而很多构建系统中会把Windows版本依赖使用静态库,所以很多同学不会碰到这些问题。
有一些更严谨更安全防止符号冲突的方式是吧Linux下符号也通过 __attribute__((visibility("hidden")))
设为默认隐藏,然后对于要导出的符号设置为 __attribute__((visibility("default")))
来共享(比如Unreal Engine的构建系统)。这时候通常的做法是提供一个宏,在编译的时候设置PRIVATE定义为 __declspec(__dllimport__)/__attribute__((__dllimport__))/__attribute__((visibility("default")))
对外传递的PUBLIC定义为 __declspec(dllexport)/__attribute__((__dllexport__))/__attribute__((visibility("default")))
。
在 protobuf 生成的代码中,由于 .pb.cc
中存在全局变量,我们也不能允许同一个全局变量在多个动态库中,否则会重复注册和执行构造析构函数。那么为了实现上面的流程,protobuf 提供了一个生成选项 dllexport_decl
,用来对要导出的符号指定这个宏。
那么我们再来看一个这个问题对应的生成代码(选项是 --cpp_out=dllexport_decl=TGF_BATTLE_PROTOCOL_API:<OUT DIR>
)。
首先对于 .pb.h
:
class DBattleAffix;
struct DBattleAffixDefaultTypeInternal;
TGF_BATTLE_PROTOCOL_API extern DBattleAffixDefaultTypeInternal _DBattleAffix_default_instance_;
// ...
class TGF_BATTLE_PROTOCOL_API DBattleAffix final : // ...
{
// ...
static inline const DBattleAffix* internal_default_instance() {
return reinterpret_cast<const DBattleAffix*>(
&_DBattleAffix_default_instance_);
}
}
可以看到,有接口引用了 _DBattleAffix_default_instance_
这个全局变量的地址。那么在 .pb.cc
里:
struct DBattleAffixDefaultTypeInternal {
PROTOBUF_CONSTEXPR DBattleAffixDefaultTypeInternal()
: _instance(::_pbi::ConstantInitialized{}) {}
~DBattleAffixDefaultTypeInternal() {}
union {
DBattleAffix _instance;Bu
};
};
PROTOBUF_ATTRIBUTE_NO_DESTROY PROTOBUF_CONSTINIT PROTOBUF_ATTRIBUTE_INIT_PRIORITY1 DBattleAffixDefaultTypeInternal _DBattleAffix_default_instance_;
可以看到,.pb.cc
里面并没有设置 TGF_BATTLE_PROTOCOL_API
来导出符号,最终就会导致类似下面这样的链接错误:
7>TGFBattleUtility.lib(BattleAffixAlgorithm.obj): Error LNK2001: unresolved external symbol "struct tgf::DBattleAffixDefaultTypeInternal tgf::_DBattleAffix_default_instance_" (?_DBattleAffix_default_instance_@tgf@@3UDBattleAffixDefaultTypeInternal@1@A)
那么解决方法也很简单,就是在 protoc
的代码里补上这些,(v21和v23版本变化比较大,具体可以参考下面的patch files)
- https://github.com/atframework/cmake-toolset/blob/main/ports/protobuf/protobuf-v3.21.12.patch
- https://github.com/atframework/cmake-toolset/blob/8c8659f2d64283b02a24b7c34ffcd0d24eb03ca6/ports/protobuf/protobuf-v23.patch
这个issue已经提交到 https://github.com/protocolbuffers/protobuf/issues/13084 并且BUG修复的PR在 https://github.com/protocolbuffers/protobuf/pull/13085 。有兴趣的小伙伴也可以跟进。
gRPC 的链接和编译问题
gRPC 的 v1.54.0 的链接符号问题
我们在使用高版本编译器时,会尽可能使用高版本的STD标准。在C++17以后 constexpr
关键字隐式会被附加 inline
(详情参考: https://en.cppreference.com/w/cpp/language/constexpr)。
这意味着这些变量可能不会生成实际的符号。那么问题就来了。我们来看v1.54.0 版本的代码:
首先 src/core/ext/gcp/metadata_query.h
:
class MetadataQuery : public InternallyRefCounted<MetadataQuery> {
public:
static const char kZoneAttribute[];
static const char kClusterNameAttribute[];
static const char kRegionAttribute[];
static const char kInstanceIdAttribute[];
static const char kIPv6Attribute[];
// ...
然后 src/core/ext/gcp/metadata_query.cc
:
constexpr const char MetadataQuery::kZoneAttribute[] =
"/computeMetadata/v1/instance/zone";
constexpr const char MetadataQuery::kClusterNameAttribute[] =
"/computeMetadata/v1/instance/attributes/cluster-name";
constexpr const char MetadataQuery::kRegionAttribute[] =
"/computeMetadata/v1/instance/region";
constexpr const char MetadataQuery::kInstanceIdAttribute[] =
"/computeMetadata/v1/instance/id";
constexpr const char MetadataQuery::kIPv6Attribute[] =
"/computeMetadata/v1/instance/network-interfaces/0/ipv6s";
这时候,某些编译环境下这些变量被inline了以后,就会出现未定义符号的问题。 相关修复放在了 https://github.com/atframework/cmake-toolset/blob/main/ports/grpc/grpc-v1.54.0.patch 。 同时这个BUG在 v1.54.2 里已经被修复。
gRPC 的在部分编译器上的兼容性问题
按照 https://en.cppreference.com/w/cpp/memory/unique_ptr 的要求, std::unqieur_ptr<T, std::default_delete<T>>
中的 T
应该是允许前置声明的。
但是某些编译器或者STL实现的问题,导致开启最高支持标准时 src/core/lib/surface/server.h
里前置申明类型用在 std::unqieur_ptr
时会报 incomplete type
的问题。
临时的处理方式是把它们改成 shared_ptr
,虽然暂时有轻微的性能损失,但是也可以后续等官方更完整的修复。Patch文件在 https://github.com/atframework/cmake-toolset/blob/main/ports/grpc/grpc-v1.54.2.patch
最后
近期碰到的 protobuf 和 gRPC 的构建坑也就这么多。后续针对 protobuf v22+ 和 gRPC v1.55+ 还有一些比较重大的变化。导致我们的Patch过程有一个大改造。特别是设计upb的部分。 这部分后续在写分享吧,也欢迎有兴趣的小伙伴交流。