如何创建包含Protobuf元素且无Protobuf依赖的C++可分发库
现有操作的核心错误
- 头文件设计存在严重缺陷:同时维护内部、对外两个版本的
Backbone.h,内部版本类包含Protobuf类型的成员变量,对外版本是空类,两者内存布局完全不一致,编译运行时会直接触发内存踩踏;且内部头文件直接引入Protobuf头文件依赖,无法做到对用户透明。 - 静态库编译命令逻辑错误:
- 加了无意义的
-DBUILD_SHARED_LIBS=ON宏,该宏是CMake构建系统专用参数,g++直接编译时添加无任何作用,反而可能触发Protobuf头文件的符号可见性异常。 - 编译阶段(
-c参数为只编译不链接)添加了-lprotobuf -lpthread等链接参数,这类参数仅在链接阶段生效,编译时添加完全无效。 - 打包静态库时仅打包了自身代码生成的
.o文件,没有把Protobuf的实现代码打包进去,用户侧自然会找不到Protobuf相关符号。
- 加了无意义的
- 库文件命名、链接路径不规范:自定义
.sll后缀不属于Linux静态库标准后缀,链接时写全路径的写法容易出现路径匹配错误。 - 生命周期逻辑风险:在库的析构函数中调用
google::protobuf::ShutdownProtobufLibrary(),该函数会关闭整个进程的Protobuf运行环境,如果用户侧代码也使用了Protobuf,会直接触发崩溃。
正确实现步骤
第一步:重构代码,用PIMPL模式完全隐藏Protobuf依赖
不需要维护两个版本的头文件,只保留一个对外暴露的头文件,所有Protobuf相关实现全部藏在源文件中,彻底避免暴露依赖:
对外发布的头文件Backbone.h(给用户使用,无任何Protobuf痕迹)
#include <memory> class Backbone{ public: Backbone(); ~Backbone(); // 禁用默认拷贝/赋值,避免智能指针impl的浅拷贝问题 Backbone(const Backbone&) = delete; Backbone& operator=(const Backbone&) = delete; void save_Number(int value); int get_Number(); private: // 前置声明内部实现类,头文件中不需要给出具体定义 struct Impl; std::unique_ptr<Impl> impl_; };
内部实现文件Backbone.cpp(不对外发布)
#include "Backbone.h" #include "alias.pb.h" // 内部实现类,所有Protobuf相关成员全部放在这里 struct Backbone::Impl { aliasPackage::NumberSave number; }; Backbone::Backbone() : impl_(std::make_unique<Impl>()) { impl_->number.set_number(0); } Backbone::~Backbone() { // 禁止在这里调用ShutdownProtobufLibrary,否则会影响用户侧其他Protobuf逻辑 } void Backbone::save_Number(int value) { impl_->number.set_number(value); } int Backbone::get_Number() { return impl_->number.number(); }
alias.proto和生成代码的命令不需要修改,正常用protoc -I. --cpp_out=. alias.proto生成alias.pb.h和alias.pb.cc即可。
第二步:编译生成所有目标文件
首先确认本地已经编译安装了带-fPIC参数的Protobuf静态库(默认路径/usr/local/lib/libprotobuf.a),如果没有需要重新编译Protobuf静态版本,添加位置无关代码支持。
执行以下命令编译业务代码和Protobuf生成的代码:
g++ -c --std=c++20 -O3 -fPIC -I/usr/local/include alias.pb.cc Backbone.cpp
参数说明:
-c:只编译不链接,不需要加任何链接参数-fPIC:生成位置无关代码,兼容静态库、动态库的链接需求-I/usr/local/include:指定Protobuf头文件路径
第三步:打包合并静态库,把Protobuf依赖全部打包进库文件
不能直接用ar打包自己的.o文件,需要把Protobuf静态库中的所有目标文件解压出来,和自己的.o文件合并成一个完整的静态库:
# 创建临时目录存放Protobuf静态库解压出的目标文件 mkdir -p tmp_pb cd tmp_pb ar x /usr/local/lib/libprotobuf.a cd .. # 合并所有目标文件,生成标准命名的静态库libBackbone.a(Linux静态库统一以lib为前缀,.a为后缀) ar rcs libBackbone.a *.o tmp_pb/*.o # 清理临时文件 rm -rf tmp_pb *.o
如果使用的Protobuf版本依赖abseil库,链接时出现absl相关符号找不到的错误,需要把对应absl静态库也用同样的方式解压,把目标文件一起打包进libBackbone.a。
第四步:编译用户测试程序
用户侧编译时只需要拿到Backbone.h和libBackbone.a,不需要安装任何Protobuf相关环境,编译命令如下:
g++ -Wall -Wextra -Wpedantic --std=c++20 -O3 main.cpp -L. -lBackbone -lpthread -o test_app
参数说明:
-L.:指定静态库搜索路径为当前目录-lBackbone:自动匹配搜索路径下的libBackbone.a文件-lpthread:链接系统自带的线程库,Protobuf依赖该库,所有Linux系统默认自带,不需要用户额外安装。
注意事项
- 编译静态库时使用的Protobuf版本、C++标准、编译选项必须和库本身保持一致,否则会出现ABI兼容问题。
- 绝对不要在库代码中调用
google::protobuf::ShutdownProtobufLibrary(),该函数会销毁整个进程的Protobuf全局状态,只要用户侧有其他模块使用Protobuf就会触发崩溃。如果确实需要清理,要提供单独的全局清理接口,明确告知用户调用时机。 - 发布库的时候需要和编译时使用的系统libc/libstdc++版本做兼容,尽量在低版本Linux系统上编译,避免高版本系统编译后低版本系统无法运行的问题。
内容的提问来源于stack exchange,提问作者L.knaeble
相关产品推荐
相关产品推荐

