C++ Protocol Buffers

protobuf是google推出的一个数据序列化/反序列化库,和Json/XML类似,但是其以二进制编码进行传输,而Json/XML以文本格式传输,因此文件大小更小(3-10倍)、传输速度更快(20-100倍)

protobuf是一种语言无关的库,支持C++、Python、Java、Js、Ruby等主流语言,广泛用于网络、数据传输中,例如网络协议grpc;

protobuf也不是完美的,例如它不能像Json一样即插即用,对C++而言尤为复杂,需要编译器以及runtime环境,对其他语言略微友好一点;以下记录了Win10 + MSVC编译protobuf过程,尽量列出要点,对版本无要求的可以按照本文进行,经过二次验证成功率较高,对于非C++用户/配置/VS小白/高血压患者建议使用Unix环境包管理器或vcpkg等安装依赖,无需参考本文(本文也针对禁git的生产环境。

protobufgithub仓库给出了各种语言的安装方法,但就C++安装而言其指向仍然是不清晰的,这也是issue和迭代频繁的原因,网上提供的方法许多已经过时,因为protobuf不再根据语言划分出cpp包,而且3.21以后版本增加了Abseil的依赖,涉及版本匹配问题、C++标准等,官方对此也没有加以具体规范说明,这也是官方和许多博客建议使用包管理器的原因;

对Cpp而言问题更是如此,因为作者在win64/32安装包中提到:This binary is intended for users who want to use Protocol Buffers in languages other than C++ but do not want to compile protoc themselves.,无依赖情况下C++甚至无法使用该二进制程序生成的头文件,也无法进行序列化读写,因为其需要相关的环境支持,因此接下来只能通过编译获取了。

环境编译

  • Win10 + Visual Studio 2019(MSVC 16)

  • CMake 3.27.3

  • Abseil 20240722版本

  • protobuf 29.3

CMake编译abseil

abseil是C++标准库的补充,protobuf的编译依赖于这个静态库,需要使用Cmake编译这个静态库:从此处下载。

注意第一个要点,较新的protobuf使用的abseil宜使用C++ 17标准编译,在CMake环境中点击Add Entry添加CMAKE_CXX_STANDARDstring填入17

建议使用cmake的gui窗口,新建build文件夹,选择输出文件夹为build,直接configure并且generate即可,可见build出现MSVC的解决方案absl.sln文件,使用VS打开,默认采用Debug模式,将标准改为17(我也不确定二者谁真正起作用),使用ctrl+F5执行生成任务,出现“"成功92个、失败为0个”,说明静态库生成没有出错;

进入build文件夹指定输出路径并打包:

1
cmake --install . --prefix D:\Documents\Desktop\abseil\abseil-cpp-20240722.0\output --config Debug 

output文件夹下出现libinclude,编译打包完成。

CMake编译protobuf

github获取realease,我选择的版本目前是25年的最新版本protobuf-29.3.zip,不要使用win32/64等

同样方式建立build文件夹,并使用CMake gui窗口指定源文件夹与输出文件夹,此处:

  • 取消勾选protobuf_BUILD_TESTS,因为这里我没有clone相关的测试文件;

  • 勾选protobuf_BUILD_SHARED_LIBS,这里选择输出动态库想要静态库的无需勾选

第一次configure,会发现一堆红色,实际上报错的只有一个,指向:D:\Documents\Desktop\proto\protobuf-29.3\build\third_party\abseil-cpp缺少CMakeLists.txt:将刚刚abseilCMakeLists.txt(即根目录)拷贝到该文件夹,重新configure,配置无误,点击generate生成sln并通过VS打开;

找到左侧CMakePredefinedTargets下的ALL_BUILD,将其属性标准设置成C++ 17,然后右键点击生成,出现"0错误",动态库编译完成

此时的依赖分散在若干个文件夹,如protobuf-29.3\build\bin\Debug存储了S可执行文件和dll、lib库文件S,它们是我们需要的动态库依赖protobuf-29.3\src\google\protobuf存储的是头文件依赖,通过此条命令好好打包一下:

在build中执行:

1
cmake --install . --prefix D:\Documents\Desktop\proto\protobuf-29.3\build\output --config Debug
可见output下出现了bin、include、lib三个文件夹,其中:

  • bin:存储了4个可执行文件,最主要是protoc.exe以及其动态库依赖;

  • include:存储了头文件依赖,包含google、absl、upb等等;

  • lib:存储了cmake、pkgconfig两个文件夹以及若干.lib库文件

:我使用27.5编译时,打包没有bin文件,lib下也没有库文件,但文件夹分散是存在的,最后编译没有成功,没有验证是版本特性还是说明出现问题;

至此,得到上述文件,protobuf编译成功。将bin文件夹包含到环境变量中,验证:

1
protoc --version

protobuf验证

工程目录下新建一个data.proto:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
syntax = "proto3";

enum PhoneType {
MOBILE = 0;
HOME = 1;
WORK = 2;
}

message PhoneNumber {
string number = 1;
PhoneType type = 2;
}

message Address {
string country = 1;
string detail = 2;
}

message Person {
int32 id =1;
string name = 2;
int32 age = 3;
repeated string email = 4;
repeated PhoneNumber phone = 5;
Address address = 6;
}

使用命令将其转换成头文件data.pb.h和源文件data.pb.cc接口:

1
protoc .\data.proto --cpp_out=.\
将其添加到工程头文件、源文件,如果protobuf选择的是动态库一定要在data.pb.h下定义#define PROTOBUF_USE_DLLS,否则默认使用静态库会出现错误;

在工程中添加头文件、库文件依赖关系,主要是三个地方:

  • 项目 - 属性 - VC++目录 - 包含目录或外部包含目录添加刚刚install的路径,如D:\Documents\Desktop\proto\protobuf-29.3\build\output\include

  • 上述同样的地方加入库目录D:\Documents\Desktop\proto\protobuf-29.3\build\output\lib

如果打包并不规范,可能目录路径有别

最后添加依赖项,此处有若干方法:

  • 法1项目 - 属性 - 链接器 - 附加依赖项中添加:

  • 法2左侧资源管理器 - 工程右击 - 添加 - 新建筛选器 - 重命名成库文件并且添加文件夹的lib文件

添加对象是libprotocd.liblibprotobufd.libabseil_dll.lib,有的只需要添加前二者,我这里三者均需要;

一些说明:

  1. 第一次建议将所有lib通过法二先行导入,以排除符号缺失故障;

  2. 在使用27.5 Realease版本protobuf时生成命名是libprotoc.liblibprotobuf.lib,缺少d,未验证失败是否与此有关,也许只是版本特性影响,这里成功版本均采用29.3的Debug以及Aseil的Debug版本,且为C++ 17标准;

最后验证正常,这里甚至无需写入什么复杂逻辑代码,空实现一个main函数且将data.pb.h包含在cpp文件中,如果头文件被成功包含且能通过编译,即可正常使用;注意检查是否x64平台,因为所有的库均在x64下生成;

Q&A

  1. 编译abseil/protobuf,出现visual studio2019无法启动程序,\ALL_BUILD 拒绝访问:
  • 第一次生成完属正常现象,无需理会,只需要看到"失败为0个",静态库/动态库成功生成即可;
  1. 4个无法解析外部符号:内容如下:
    1
    2
    3
    4
    无法解析的外部符号 "protected: static struct google::protobuf::internal::DescriptorMethods const google::protobuf::Message::kDescriptorMethods" (?kDescriptorMethods@Message@protobuf@google@@1UDescriptorMethods@internal@23@B)
    无法解析的外部符号 "class std::array<char,7> const absl::lts_20240722::log_internal::kCharNull" (?kCharNull@log_internal@lts_20240722@absl@@3V?
    无法解析的外部符号 "private: static struct google::protobuf::internal::ThreadSafeArena::ThreadCache google::protobuf::internal::ThreadSafeArena::thread_cache_" (?thread_cache_@ThreadSafeArena@internal@protobuf@google@@0UThreadCache@1234@A)
    无法解析的外部符号 "class google::protobuf::internal::ExplicitlyConstructed<class std::basic_string<char,struct std::char_traits<char>,class std::allocator<char> >,8>
  • data.pb.h头文件没有添加#define PROTOBUF_USE_DLLS,导致无法解析部分符号;
  1. 出现4个以上乃至几十个不等未解析外部符号
  • 多因素影响,总而言之是库文件没有正确配置,根据具体符号排查原因,例如protobuf动态库或Abseil静态库未正确引入,或者C++标准未严格使用C++ 17等、使用x64编译但main使用了默认x86编译;
  1. 编译成功执行出现“无法定位入口....dll”
  • 这里是我配置了环境变量后仍然出现的报错,不清楚为什么没有从环境变量找到引入的三个动态库,主要没有找到abseil_dll.dll,最便捷的方式将其复制到程序同级目录即可。