This guide explains the C++ code generated by the hpp-proto plugin of the Protocol Buffer compiler for any given .proto schema file.
Once the hpp-proto package has been added via find_package
or FetchContent
, the Protocol Buffer compiler can be invoked using the protobuf_generate_hpp
function in the following format:
protobuf_generate_hpp(
TARGET <TargetName>
[IMPORT_DIRS <dirs>]
[PROTOC_OUT_DIR <output_dir>]
[PROTOS <protobuf_files>]
[PLUGIN_OPTIONS <plugin_options>])
- IMPORT_DIRS: Specifies common parent directories for schema files.
- PROTOC_OUT_DIR: Output directory for generated source files. Defaults to CMAKE_CURRENT_BINARY_DIR.
- PROTOS: List of proto schema files. If omitted, then every source file ending in proto of TARGET will be used.
- PLUGIN_OPTIONS: A comma-separated string forwarded to the protoc-gen-hpp plugin to customize code generation. Options include:
* `root_namespace=`: Prepends a root namespace to the generated code, in addition to the package namespace.
* `top_directory=`: Prepends a directory to all import dependencies.
* `proto2_explicit_presence=`: For Proto2 only, makes optional scalar fields implicitly present except for specified scopes. This option can be specified multiple times. For example: the option `proto2_explicit_presence=.pkg1.msg1.field1,proto2_explicit_presence=.pkg1.msg2` instructs the code generator that explicit presence is only applicable for the `field1` of `pkg1.msg1` and all fields of `pkg1.msg2`.
* `non_owning`: Generates non-owning messages.
The compiler generates several header files for each .proto file, transforming the filename and extension as follows:
- The
.proto
extension is replaced with.msg.hpp
,.pb.hpp
,.glz.hpp
and.desc.hpp
for different header file types. - The proto path (specified with --proto_path= or -I flags) is replaced with the output path (specified with the --hpp_out flag).
Example CMake configuration:
add_library(non_owning_unittest_proto3_proto_lib INTERFACE)
target_include_directories(non_owning_unittest_proto3_proto_lib INTERFACE ${CMAKE_CURRENT_BINARY_DIR})
protobuf_generate_hpp(
TARGET non_owning_unittest_proto3_proto_lib
IMPORT_DIRS ${CMAKE_CURRENT_SOURCE_DIR}
PROTOC_OUT_DIR ${CMAKE_CURRENT_BINARY_DIR}/non_owning
PROTOS ${CMAKE_CURRENT_SOURCE_DIR}/google/protobuf/unittest_proto3.proto
PLUGIN_OPTIONS non_owning,root_namespace=non_owning,top_directory=non_owning)
You can invoke the hpp-proto plugin from the command line with the following command:
protoc --hpp_out=<output_dir> [--hpp_opt=<plugin_option>] [--proto_path=<dir>] <protobuf_files...>
If a .proto file contains a package declaration, the entire contents are placed in a corresponding C++ namespace. For example:
package foo.bar;
All declarations in the file will reside in the foo::bar
namespace.
If you need to use both hpp-proto
and the official Google Protobuf library in the same program, you can use the root_namespace option to customize the top-level namespace in the generated files. For example:
protoc --proto_path=src --hpp_out build/gen --hpp_opt=root_namespace=baz src/foo.proto
This command will place all declarations in the baz::foo::bar
namespace.
Given a simple message declaration like:
message Foo {}
The protocol buffer compiler generates a struct called Foo
.
For each field defined in a .proto message, the compiler generates corresponding member variables in the C++ struct
.
syntax = "proto3";
message Foo {
int32 f1 = 1;
string f2 = 2;
bytes f3 = 3;
}
The compiler will generate the following struct
:
Regular Mode
struct Foo {
int32_t f1;
std::string f2;
std::vector<std::byte> f3;
};
Non-owning Mode
struct Foo {
int32_t f1;
std::string f2;
hpp::proto::equality_comparable_span<const std::byte> f3;
};
The hpp::proto::equality_comparable_span
is a template class which inherits std::span
and adds an additional equality comparison operator.
For a Proto2 message with optional fields:
syntax = "proto2";
message Foo {
optional int32 f1 = 1;
optional string f2 = 2;
optional bytes f3 = 3;
optional int64 f4 = 4 [default = 100];
}
The compiler will generate the following struct
:
Regular Mode
struct Foo {
hpp::proto::optional<int32_t> f1;
hpp::proto::optional<std::string> f2;
hpp::proto::optional<std::vector<std::byte>> f3;
hpp::proto::optional<int64_t, 100> f4;
};
Non-owning Mode
struct Foo {
hpp::proto::optional<int32_t> f1;
hpp::proto::optional<std::string_view> f2;
hpp::proto::optional<hpp::proto::equality_comparable_span<const std::byte>> f3;
hpp::proto::optional<int64_t, 100> f4;
};
The hpp::proto::optional<T, DefaultValue>
type has the same interface with std::optional<T>
except the value()
and operator*()
member functions return the default value if the field is not present. Furthermore, the specialization for hpp::proto::optional<bool>
deletes type conversion to bool
to avoid ambiguity between a missing value and a false value.
Given Bar
is a message, any of these field definitions like:
//proto2
optional Bar foo = 1;
//proto3
Bar foo = 1;
The compiler will generate:
std::optional<Bar> foo;
For repeated fields, the compiler generates std::vector<T>
in regular mode and
hpp::proto::equality_comparable_span<const T>
in non-owning mode.
For oneof fields:
message TestOneof {
oneof foo {
int32 foo_int = 1;
string foo_string = 2;
NestedMessage foo_message = 3;
NestedMessage foo_lazy_message = 4 [lazy = true];
}
message NestedMessage {
double required_double = 1;
}
}
The compiler maps the oneof field to std::variant
, with the first alternative being std::monostate
:
struct TestOneof {
struct NestedMessage {
double required_double = {};
bool operator == (const NestedMessage&) const = default;
};
enum foo_oneof_case : int {
foo_int = 1,
foo_string = 2,
foo_message = 3,
foo_lazy_message = 4
};
std::variant<std::monostate, int32_t, std::string, NestedMessage, NestedMessage> foo;
bool operator == (const TestOneof&) const = default;
};
For map
fields:
message TestMap {
map<int32, int32> map1 = 1;
}
The compiler generates hpp::proto::flat_map<key_type, mapped_type>
for regular mode and with hpp::proto::equality_comparable_span<std::pair<key_type, mapped_type>>
for non-owning mode. In the non-owning mode,
the library does not handle key deduplication during serialization or deserialization.
Given a deserialized message with duplicate map keys, users are responsible to make sure that only the last key seen should be treated as valid.
Regular Mode
struct TestMap {
hpp::proto::flat_map<int32_t,int32_t> map1;
bool operator == (const TestMap&) const = default;
};
Non-owning Mode
struct TestMap {
hpp::proto::equality_comparable_span<std::pair<int32_t,int32_t>> map1;
bool operator == (const TestMap&) const = default;
};
message TestAny {
google.protobuf.Any any_value = 2;
}
The compiler generates ::google::protobuf::Any
.
struct TestAny {
std::optional<::google::protobuf::Any> any_value;
bool operator == (const TestAny&) const = default;
};
To read or write Any fields, use hpp::proto::unpack_any
and hpp::proto::pack_any
.
Regular Mode
TestAny message;
using namespace std::string_literals;
google::protobuf::FieldMask fm{.paths = {"/usr/share"s, "/usr/local/share"s}};
assert(hpp::proto::pack_any(message.any_value.emplace(), fm).ok());
std::vector<char> buf;
assert(hpp::proto::write_proto(message, buf).ok());
TestAny message2;
assert(hpp::proto::read_proto(message2, buf).ok());
google::protobuf::FieldMask fm2;
assert(hpp::proto::unpack_any(message2.any_value.value(), fm2).ok());
Non-owning Mode
TestAny message;
using namespace std::string_view_literals;
std::array<std::string_view, 2> paths = {"/usr/share"sw, "/usr/local/share"sw};
google::protobuf::FieldMask fm;
fm.paths = paths;
std::pmr::monotonic_buffer_resource pool;
hpp::proto::pb_context ctx{pool};
assert(hpp::proto::pack_any(message.any_value.emplace(), fm, ctx).ok());
std::pmr::vector<std::byte> buffer{&pool};
assert(hpp::proto::write_proto(message, buffer).ok());
TestAny message2;
assert(hpp::proto::read_proto(message2, buf, ctx).ok());
google::protobuf::FieldMask fm2;
assert(hpp::proto::unpack_any(message2.any_value.value(), fm2, ctx).ok());