blob: 48f5fc91013104211ce5db239c001d8bd083dd5a [file] [log] [blame] [view]
# Protocol Buffer Extras
## Motivation
Protocol buffers are used extensively throughout Chromium for data serialization. To keep Chromium's binary size as small as possible, most protobufs are compiled with the `LITE_RUNTIME` option. This uses the `google::protobuf::MessageLite` interface, which is significantly smaller than the full `google::protobuf::Message` interface because it strips out reflection and other descriptors.
While `LITE_RUNTIME` is great for performance and size, the lack of reflection makes implementing otherwise trivial functionality difficult and tedious. This includes:
* Debug serialization (e.g., printing a message to a log).
* Equality comparison.
Historically, this has led to developers writing boilerplate, error-prone, hand-written code to perform these actions. For example, equality was often implemented by comparing the results of `SerializeAsString()`, which can produce false negatives for messages containing map fields (as map field ordering is not guaranteed). This manual approach is a maintenance burden, as the serialization or equality logic can easily become outdated when the proto definition changes.
The `proto_extras` library was created to solve these problems. It is a code generator that automatically creates this missing functionality for `MessageLite` protos, providing robust, efficient, and easy-to-maintain helpers for serialization and testing.
## Features
- Serialization of a proto message to `base::Value::Dict`
- `operator<<` stream support for printing a proto message.
- `operator==` and `operator!=` equality support for proto messages.
- `gmock` matchers for testing proto messages.
## Usage
### `proto_extras`
The `proto_extras` template is used to generate the serialization, stream
operator, and equality support. It is used in a `BUILD.gn` file as follows:
```gn
import("//components/proto_extras/proto_extras.gni")
proto_extras("my_proto_extras") {
sources = [ "my_proto.proto" ]
deps = [ ":my_proto_cc_proto" ] # Must be a proto_library
}
```
By default, all functionality is generated. To disable functionality, the
following properties can be set:
- `omit_to_value_serialization`: Disables serialization to `base::Value::Dict`.
- `omit_stream_operators`: Disables `operator<<` stream support.
- `omit_equality`: Disables `operator==` and `operator!=` equality support.
The generated files can be included in C++ as follows:
- Serialization: `#include "path/to/my_proto.to_value.h"`
- Stream operator: `#include "path/to/my_proto.ostream.h"`
- Equality: `#include "path/to/my_proto.equal.h"`
### `proto_test_extras`
The `proto_test_extras` template is used to generate gmock matchers for proto
messages. It is used in a `BUILD.gn` file as follows:
```gn
import("//components/proto_extras/proto_extras.gni")
proto_test_extras("my_proto_test_extras") {
testonly = true
sources = [ "my_proto.proto" ]
deps = [ ":my_proto_cc_proto" ] # Must be a proto_library
extras_deps = [ ":my_proto_extras" ]
}
```
This will generate a `<name>.test.h` file that can be included in test files.
## Generated Code Examples
### `base::Value::Dict` Serialization
Given the following proto:
```protobuf
message TestMessage {
optional string name = 1;
optional int32 id = 2;
}
```
The generated `to_value.h` header will contain:
```cpp
// Generated by the proto_extras plugin. DO NOT EDIT!
// source: test.proto
#ifndef TEST_TO_VALUE_H_
#define TEST_TO_VALUE_H_
#include <optional>
#include <string_view>
namespace base {
class DictValue;
} // namespace base
namespace my_package::proto {
class TestMessage;
} // namespace my_package::proto
namespace my_package::proto {
base::DictValue Serialize(const TestMessage& message);
void MaybeSerialize(const std::optional<TestMessage>& opt_message,
std::string_view output_dictionary_field_name,
base::DictValue& output_dictionary);
} // namespace my_package::proto
#endif // TEST_TO_VALUE_H_
```
And can be used as follows:
```cpp
#include "path/to/test.to_value.h"
my_package::proto::TestMessage message;
message.set_name("test");
message.set_id(123);
// The `Serialize` function is in the same namespace as the message.
base::Value::Dict dict = my_package::proto::Serialize(message);
// dict is now: {"name": "test", "id": 123}
```
The serialization handles all field types, including repeated fields, maps, and
oneofs.
### Stream Operator
The generated `ostream.h` header will contain:
```cpp
// Generated by the proto_extras plugin. DO NOT EDIT!
// source: test.proto
#ifndef TEST_OSTREAM_H_
#define TEST_OSTREAM_H_
#include <iosfwd>
namespace my_package::proto {
class TestMessage;
} // namespace my_package::proto
namespace my_package::proto {
std::ostream& operator<<(std::ostream& out, const TestMessage& message);
} // namespace my_package::proto
#endif // TEST_OSTREAM_H_
```
And can be used as follows:
```cpp
#include "path/to/test.ostream.h"
my_package::proto::TestMessage message;
message.set_name("test");
message.set_id(123);
// The stream operator is in the same namespace as the message.
std::cout << message;
// This will print the same JSON representation as `base::Value::Dict`
```
### Equality Operator
The generated `equal.h` header will contain:
```cpp
// Generated by the proto_extras plugin. DO NOT EDIT!
// source: test.proto
#ifndef TEST_EQUAL_H_
#define TEST_EQUAL_H_
namespace my_package::proto {
class TestMessage;
} // namespace my_package::proto
namespace my_package::proto {
bool operator==(const TestMessage& lhs, const TestMessage& rhs);
} // namespace my_package::proto
#endif // TEST_EQUAL_H_
```
This can be used to compare two proto messages for equality. The generated
`operator==` handles all field types, including oneofs and maps, and recursively
calls `operator==` for nested messages.
### Gmock Matchers
The generated `test.h` header will contain a matcher called `Equals<MessageName>`
for each message. For the `TestMessage` proto, the matcher is `EqualsTestMessage`.
Example usage:
```cpp
#include "path/to/test.test.h"
#include "testing/gmock/include/gmock/gmock.h"
using ::testing::Not;
TEST(MyTest, Test) {
my_package::proto::TestMessage message1;
message1.set_name("test");
message1.set_id(123);
my_package::proto::TestMessage message2;
message2.set_name("test");
message2.set_id(123);
my_package::proto::TestMessage message3;
message3.set_name("different");
message3.set_id(456);
// The matcher is in the same namespace as the message.
EXPECT_THAT(message1, my_package::proto::EqualsTestMessage(message2));
EXPECT_THAT(message1, Not(my_package::proto::EqualsTestMessage(message3)));
}
```
The generated `.test.h` file also contains a `PrintTo` implementation for each
message, which allows gtest to pretty-print the message on test failures.
The matcher handles all field types, including repeated fields, maps, and
oneofs.
## Forcing full protobuf library support
For cases where the message uses the full `google::protobuf::Message` type,
the `protobuf_full_support` option can be used in the `proto_extras` GN target
to ensure the generated code with the full protobuf library. Due to android
build complications, this also requires the `use_fuzzing_engine_with_lpm` build
flag to be set. This option is relevant for `base::Value` serialization and
equality.
## AI Agent Guide
This section contains information for AI agents that are tasked with working on
the `proto_extras` library.
### Code Generation
The `proto_extras` library uses two main files for code generation:
- **`proto_extras_plugin.cc`**: This file is responsible for generating the
`to_value`, `ostream`, and `equal` files. It is a protobuf compiler plugin
that is invoked by the `proto_library` GN template.
- **`proto_test_extras_plugin.cc`**: This file is responsible for generating the
`.test.h` and `.test.cc` files, which contain gmock matchers and `PrintTo`
implementations. It is also a protobuf compiler plugin.
### Support Library
The generated code relies on a support library for common functionality:
- **`proto_extras_lib.h`**: This file contains common helper functions used by
the generated code, such as `ToNumericTypeForValue` for converting between
numeric types and `SerializeUnknownFields` for serializing unknown fields in
`MessageLite` protos.
- **`protobuf_full_support.h/.cc`**: These files provide an alternative
implementation of some of the helper functions in `proto_extras_lib.h` for
messages that use the full `google::protobuf::Message` type. This is
necessary for fuzzing targets and other cases where the full protobuf library
is used. It also provides `MessageDifferencerEquals` to compare two full
protobuf messages.
- **`proto_matchers.h`**: This file contains helper `gmock` matchers that are
used by the generated test code.
### Modifying the Library
When tasked with modifying the `proto_extras` library, it is important to
understand which file is responsible for the desired functionality.
- For changes to the `base::Value::Dict` serialization, stream operator, or
equality operators, the relevant file is `proto_extras_plugin.cc`.
- For changes to the `gmock` matchers, the relevant files are
`proto_test_extras_plugin.cc` and `proto_matchers.h`.
- For changes to the helper functions used by the generated code, the relevant
file is `proto_extras_lib.h` or `protobuf_full_support.h`/`.cc`.
When adding new functionality, it is recommended to follow the existing pattern
of creating a new generation function in the appropriate plugin file and adding
a new command-line option to enable it.