My goal is to serialize information by using protobuf in C++.
proto file:
syntax = "proto3";
package PhoneBookSerialize;
message Date {
int32 year = 1;
int32 month = 2;
int32 day = 3;
}
message Contact {
string name = 1;
Date birthday = 2;
repeated string phone_number = 3;
}
message ContactList{
repeated Contact contact = 1;
}
Contact related code:
struct Contact {
std::string name;
std::optional<Date> birthday;
std::vector<std::string> phones;
bool operator<(const Contact& other) const {
return name < other.name;
}
};
class PhoneBook {
public:
explicit PhoneBook(std::vector<Contact> contacts);
void SaveTo(std::ostream& output) const;
private:
std::vector<Contact> contact_book;
};
PhoneBook::PhoneBook(std::vector<Contact> contacts) : contact_book(contacts) {
std::sort(contact_book.begin(), contact_book.end());
};
Serialization function:
void PhoneBook::SaveTo(std::ostream& output) const {
PhoneBookSerialize::ContactList contact_list;
for(const auto& contact : contact_book){
PhoneBookSerialize::Contact* pb_contact = contact_list.add_contact();
pb_contact->set_name(contact.name);
if(contact.birthday.has_value()){
PhoneBookSerialize::Date* pb_date = pb_contact->mutable_birthday();
pb_date->set_year(contact.birthday->year);
pb_date->set_month(contact.birthday->month);
pb_date->set_day(contact.birthday->day);
}
for(const auto& phone : contact.phones){
pb_contact->add_phone_number(phone);
}
}
contact_list.SerializeToOstream(&output);
};
main.cpp file
#include "phone_book.h"
#include "contact.pb.h"
#include <sstream>
using namespace std;
int main(){
const PhoneBook ab({
{"Ab ba", Date{1980, 1, 13}, {"+79850685521"}},
{"Ac ca", Date{1989, 4, 23}, {"+79998887766", "+71112223344"}},
{"Ad da", Date{1989, 4, 23}, {}},
{"No Birthday", std::nullopt, {"+7-4862-77-25-64"}},
});
ostringstream output(std::ios::binary);
ab.SaveTo(output);
istringstream input(output.str(), std::ios::binary);
PhoneBookSerialize::ContactList list;
list.ParseFromIstream(&input);
return 0;
}
CMakeLists.txt file:
cmake_minimum_required(VERSION 3.10)
project(PhoneBookProtobuf LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# Find Protocol Buffers package
find_package(Protobuf REQUIRED)
# Find Abseil package
find_package(absl REQUIRED)
include_directories(${Protobuf_INCLUDE_DIRS})
include_directories(${CMAKE_CURRENT_BINARY_DIR})
# Generate protobuf files from proto directory
protobuf_generate_cpp(PROTO_SRCS PROTO_HDRS proto/contact.proto)
# Add all source files
add_executable(main
src/main.cpp
src/phone_book.cpp
src/phone_book.h
${PROTO_SRCS}
${PROTO_HDRS}
)
# Link necessary libraries
target_link_libraries(main
${Protobuf_LIBRARIES}
absl::log
absl::log_internal_message
absl::log_internal_check_op
)
I am Using the following commands to build and run my code:
1. Configure project:
cmake --preset default
2. Build Project:
cmake --build --preset debug
3. Run Project:
./build/main
After I run the project I am getting a segmentation fault at the list.ParseFromIstream(&input);
. I suppose, it has something to do with my Cmake configuration file. I also added abseil package into my Cmake configuration, because without it on my OS (macOS) code doesn't compile.
Error messages:
AddressSanitizer:DEADLYSIGNAL
=================================================================
==4319==ERROR: AddressSanitizer: SEGV on unknown address 0x000000000020 (pc 0x000000000020 bp 0x7ff7b0d5f350 sp 0x7ff7b0d5f318 T0)
==4319==Hint: pc points to the zero page.
==4319==The signal is caused by a READ memory access.
==4319==Hint: address points to the zero page.
#0 0x000000000020 (<unknown module>)
#1 0x00010f5e6a24 in google::protobuf::internal::TcParser::FastMtR1(google::protobuf::MessageLite*, char const*, google::protobuf::internal::ParseContext*, google::protobuf::internal::TcFieldData, google::protobuf::internal::TcParseTableBase const*, unsigned long long)+0x74 (libprotobuf.29.3.0.dylib:x86_64+0xb8a24)
#2 0x00010f63abdb in bool google::protobuf::internal::MergeFromImpl<false>(google::protobuf::io::ZeroCopyInputStream*, google::protobuf::MessageLite*, google::protobuf::internal::TcParseTableBase const*, google::protobuf::MessageLite::ParseFlags)+0xd1 (libprotobuf.29.3.0.dylib:x86_64+0x10cbdb)
#3 0x00010f63bda2 in google::protobuf::MessageLite::ParseFromIstream(std::__1::basic_istream<char, std::__1::char_traits<char>>*)+0x32 (libprotobuf.29.3.0.dylib:x86_64+0x10dda2)
#4 0x00010f1adbf9 in main main.cpp:23
#5 0x7ff8165ad52f in start+0xbef (dyld:x86_64+0xfffffffffff1f52f)
==4319==Register values:
rax = 0x0000606000001700 rbx = 0x00007ff7b0d5fb20 rcx = 0x0000000000000020 rdx = 0x0000000000000000
rdi = 0x000000010f220840 rsi = 0x0000606000001700 rbp = 0x00007ff7b0d5f350 rsp = 0x00007ff7b0d5f318
r8 = 0xffffffffffffffff r9 = 0x0000000000000000 r10 = 0x00007fffffffff01 r11 = 0x00007fffffffff01
r12 = 0x000000010f222480 r13 = 0x00007ff7b0d5f3e8 r14 = 0x0000000000000000 r15 = 0x0000000000000000
AddressSanitizer can not provide additional info.
SUMMARY: AddressSanitizer: SEGV (<unknown module>)
==4319==ABORTING
zsh: abort ./build/main
The on-wire representation of PhoneBookSerialize::ContactList
isn't equivalent of sequence of on-wire representation of individual structs PhoneBookSerialize::Contact
. Protobuf serializes size of "array" represented by repeated
field as well as field id an structure information headers and separators.
Which is good, because defaulted structures have length equal to zero on-wire. So PhoneBookSerialize::Contact empty{};
would write no data at all to stream. But ContactList
would aknowledge that there is an empty element and retain information about its existence.
To deserialize from stream you usually need data bound separators as you never can predict size of expected data structure, Protobuf agressively reduces amount of written data. With packet-like I/O (which this concept is originally used in 1990s, e.g. in telemetry or hardware protocols, see ASN and co.) that's not required. repeated
fields reduce the need in such separators when transferring multiple similar records.
Why program crashes though? Is it supposed to be safe to use list.ParseFromIstream
? Yes, provided protobuf or string storage wasn't corrupted beforehand. Plus you treat field of Date as optional
whie you didn't declare it as such and didn't gave it a default value in protobuf definition.