Technical Documentation: protocolbuffers/protobuf
ℹ️ Provenance: Multi-Tier Ingestion · CodeWiki Reference
Overview (`README.md`)
Protocol Buffers - Google's data interchange format
Copyright 2008 Google LLC
Overview
Protocol Buffers (a.k.a., protobuf) are Google's language-neutral,
platform-neutral, extensible mechanism for serializing structured data. You
can learn more about it in protobuf's documentation.
This README file contains protobuf installation instructions. To install
protobuf, you need to install the protocol compiler (used to compile .proto
files) and the protobuf runtime for your chosen programming language.
Working With Protobuf Source Code
Most users will find working from
supported releases to be
the easiest path.
If you choose to work from the head revision of the main branch your build will
occasionally be broken by source-incompatible changes and insufficiently-tested
(and therefore broken) behavior.
If you are using C++ or otherwise need to build protobuf from source as a part
of your project, you should pin to a release commit on a release branch.
This is because even release branches can experience some instability in between
release commits.
Bazel with Bzlmod
Protobuf supports
Bzlmod with Bazel 8 +.
Users should specify a dependency on protobuf in their MODULE.bazel file as
follows.
bazel_dep(name = "protobuf", version = <VERSION>)Users can optionally override the repo name, such as for compatibility with
WORKSPACE.
bazel_dep(name = "protobuf", version = <VERSION>, repo_name = "com_google_protobuf")Bazel with WORKSPACE
Users can also add the following to their legacy
WORKSPACE file.
Note that with the release of 30.x there are a few more load statements to
properly set up rules_java and rules_python.
http_archive(
name = "com_google_protobuf",
strip_prefix = "protobuf-VERSION",
sha256 = ...,
url = ...,
)
load("@com_google_protobuf//:protobuf_deps.bzl", "protobuf_deps")
protobuf_deps()
load("@rules_java//java:rules_java_deps.bzl", "rules_java_dependencies")
rules_java_dependencies()
load("@rules_java//java:repositories.bzl", "rules_java_toolchains")
rules_java_toolchains()
load("@rules_python//python:repositories.bzl", "py_repositories")
py_repositories()Protobuf Compiler Installation
The protobuf compiler is written in C++. If you are using C++, please follow
the C++ Installation Instructions to install protoc along
with the C++ runtime.
For non-C++ users, the simplest way to install the protocol compiler is to
download a pre-built binary from our GitHub release page.
In the downloads section of each release, you can find pre-built binaries in
zip packages: protoc-$VERSION-$PLATFORM.zip. It contains the protoc binary
as well as a set of standard .proto files distributed along with protobuf.
If you are looking for an old version that is not available in the release
page, check out the Maven repository.
These pre-built binaries are only provided for released versions. If you want
to use the github main version at HEAD, or you need to modify protobuf code,
or you are using C++, it's recommended to build your own protoc binary from
source.
If you would like to build protoc binary from source, see the C++ Installation Instructions.
Protobuf Runtime Installation
Protobuf supports several different programming languages. For each programming
language, you can find instructions in the corresponding source directory about
how to install protobuf runtime for that specific language:
| Language | Source |
|---|---|
| C++ (include C++ runtime and protoc) | src |
| Java | java |
| Python | python |
| Objective-C | objectivec |
| C# | csharp |
| Ruby | ruby |
| Go | protocolbuffers/protobuf-go |
| PHP | php |
| Dart | dart-lang/protobuf |
| JavaScript | protocolbuffers/protobuf-javascript |
Quick Start
The best way to learn how to use protobuf is to follow the tutorials in our
developer guide.
If you want to learn from code examples, take a look at the examples in the
examples directory.
Documentation
The complete documentation is available at the Protocol Buffers doc site.
Support Policy
Read about our version support policy
to stay current on support timeframes for the language libraries.
Developer Community
To be alerted to upcoming changes in Protocol Buffers and connect with protobuf developers and users,
join the Google Group.
Overview (`.bazelci/README.md`)
Bazel CI test
The .bazelci/presubmit.yml file is used by https://buildkite.com/bazel/protobuf
to test building protobuf with Bazel on Bazel CI. It should contain the same
set of tests as .bcr/presubmit.yml which is the tests that will run before
publishing protobuf to BCR.
Overview (`.bcr/README.md`)
Bazel Central Registry
When protobuf is released, we want it to be published to the Bazel Central
Registry automatically: https://registry.bazel.build
This folder contains configuration files to automate the publish step. See
https://github.com/bazel-contrib/publish-to-bcr/blob/main/templates/README.md
for authoritative documentation about these files.
Overview (`ci/README.md`)
This directory contains CI-specific tooling.
Clang wrappers
CMake allows for compiler wrappers to be injected such as ccache, which
intercepts compiler calls and short-circuits on cache-hits. This can be done
by specifying CMAKE_C_COMPILER_LAUNCHER and CMAKE_CXX_COMPILER_LAUNCHER
during CMake's configure step. Unfortunately, X-Code doesn't provide anything
like this, so we use basic wrapper scripts to invoke ccache + clang.
Bazelrc files
In order to allow platform-specific .bazelrc flags during testing, we keep
3 different versions here along with a shared common.bazelrc that they all
include. Our GHA infrastructure will select the appropriate file for any test
and overwrite the default .bazelrc in our workspace, which is intended for
development only.
Overview (`cmake/README.md`)
This directory contains CMake files that can be used to build protobuf.
You need to have CMake and Git
installed on your computer before proceeding. If you don't have
Abseil installed, it will be fetched
from GitHub. We currently support CMake 3.22 and newer.
Most of the instructions will be given using CMake's command-line interface, but
the same actions can be performed using appropriate GUI tools.
Getting Sources
You can get the latest stable source packages from the release page:
https://github.com/protocolbuffers/protobuf/releases/latestOr you can use git to clone the protobuf git repository.
git clone -b [release_tag] https://github.com/protocolbuffers/protobuf.gitWhere [release_tag] is a git tag like v32.0-rc1 or a branch name like main
if you want to get the latest code.
Basic Build
It is considered good practice not to build CMake projects in the source tree
but in a separate folder. The following commands show a basic, cross-platform
way to configure, build, and install protobuf.
# From the protobuf source directory
cmake -S . -B build \
-DCMAKE_INSTALL_PREFIX=../install \
-DCMAKE_BUILD_TYPE=Release
# Compile the code
cmake --build build --parallel 10
# Run tests
ctest --test-dir build --verbose
# Install the libraries and headers
cmake --install buildThis will install the compiled libraries, headers, and protoc binary into the../install directory, relative to the source root.
CMake Configuration Flags
The following flags can be passed to cmake during the configuration step
(e.g., cmake -S . -B build -D<FLAG>=<VALUE>) to customize the build.
C++ Version
By default, CMake will use whatever C++ version is the system default. Since
protobuf requires C++17 or newer, sometimes you will need to explicitly override
this.
# Configure protobuf to be built with C++17
cmake . -DCMAKE_CXX_STANDARD=17See the CMake documentation forCXX_STANDARD{.external}
for all available options.
Dependency Management
Abseil and Google Test
During configuration, you can optionally specify where CMake should expect to
find your Abseil, Google Test, and jsoncpp installations, if they are installed.
If they aren't installed, they will be fetched from GitHub. To specify the
locations, set -DCMAKE_PREFIX_PATH to the path where you installed them.
# Path to where Abseil and GTest are installed
cmake . -DCMAKE_PREFIX_PATH=/path/to/my/dependenciesIf the installation of a dependency can't be found, CMake will default to
downloading and building a copy from GitHub. To prevent this and make it an
error condition, you can optionally set:-Dprotobuf_LOCAL_DEPENDENCIES_ONLY=ON.
Enabling Tests
To enable building the unit tests, set the following flag:
-Dprotobuf_BUILD_TESTS=ON
ZLib Support
If you want to include GzipInputStream and GzipOutputStream in libprotobuf,
you need to have ZLib installed. Ensure ZLib headers and libraries are in a
standard system location or a custom install prefix.
If ZLib is installed in a non-standard location, you can help CMake find it by
setting:
-DZLIB_INCLUDE_DIR=/path/to/zlib/headers-DZLIB_LIBRARY=/path/to/zlib/library.lib
Build Options
DLLs vs. Static Linking
Static linking is the default. To build shared libraries (DLLs on Windows), add
the following flag during configuration:
-Dprotobuf_BUILD_SHARED_LIBS=ON
When you build your own project against protobuf, you must also define #define PROTOBUF_USE_DLLS. For more platform-specific details, see the
Windows Builds section.
Compiling
The standard, cross-platform way to compile a CMake project is:
cmake --build <build_directory>For example:
cmake --build build --parallel 10If your generator supports multiple configurations (like Visual Studio), you
must also specify which one to build:
cmake --build build --config ReleaseTesting
To run unit tests, first compile protobuf as described above. Then run ctest:
ctest --test-dir build --progress --output-on-failureYou can also build the check target, which will compile and run the tests:
cmake --build build --target checkTo run specific tests, you need to pass arguments to the test program itself,
which requires finding the test executable in your build directory.
Installing
To install protobuf to the directory specified by CMAKE_INSTALL_PREFIX during
configuration, build the install target:
cmake --build <build_directory> --target installThis will create the following folders under the install location:
bin- contains protobufprotoccompilerinclude- contains C++ headers and.protofileslib- contains linking libraries and CMake package configuration files.
Platform-Specific Details
Windows Builds {#windows-builds}
On Windows, you can build using Visual Studio's command-line tools or IDE.
Generators
Of most interest to Windows programmers are the following
generators:
- Visual Studio: Generates a multi-configuration
.slnfile. Example:-G "Visual Studio 16 2019". - Ninja: Uses the external tool Ninja to
build. This is often the fastest solution.
Environment Setup
Open the appropriate Command Prompt from the Start menu (e.g., x86 Native
Tools Command Prompt for VS 2019) to ensure cl.exe and other build tools are
in your PATH.
DLLs vs. Static Linking on Windows
While shared libraries can be built, static linking is strongly recommended on
Windows. This is due to issues with Win32's use of a separate heap for each DLL
and binary compatibility issues between different versions of MSVC's STL
library.
If you are distributing your software, do NOT install libprotobuf.dll orlibprotoc.dll to a shared location. Keep them in your application's own
install directory.
Notes on Compiler Warnings
The following MSVC warnings have been disabled while building the protobuf
libraries. You may need to disable them in your own project as well.
C4065- # switch statement contains 'default' but no 'case' labelsC4146- # unary minus operator applied to unsigned typeC4244- # 'conversion' conversion from 'type1' to 'type2', possible loss
of dataC4251- # 'identifier' : class 'type' needs to have dll-interface to be
used by clients of class 'type2'C4267- # 'var' : conversion from 'size_t' to 'type', possible loss of
dataC4305- # 'identifier' : truncation from 'type1' to 'type2'C4307- # 'operator' : integral constant overflowC4309- # 'conversion' : truncation of constant valueC4334- # 'operator' : result of 32-bit shift implicitly converted to 64
bits (was 64-bit shift intended?)C4355- # 'this' : used in base member initializer listC4506- # no definition for inline function 'function'C4800- # 'type' : forcing value to bool 'true' or 'false' (performance
warning)C4996- # The compiler encountered a deprecated declaration.
C4251 is particularly notable if you build protobuf as a DLL. See the old
version of this document for a longer explanation if needed.
Linux Builds
Building with CMake works very similarly on Linux. You will need to have gcc
or clang installed. CMake will generate Makefiles by default, but can also be
configured to use Ninja.
After building, you can install the files system-wide using sudo:
sudo cmake --install buildOr directly with Makefiles:
cd build
sudo make installOverview (`conformance/README.md`)
Protocol Buffers - Google's data interchange format
Copyright 2008 Google LLC.
This directory contains conformance tests for testing completeness and
correctness of Protocol Buffers implementations. These tests are designed
to be easy to run against any Protocol Buffers implementation.
This directory contains the tester process conformance-test, which
contains all of the tests themselves. Then separate programs written
in whatever language you want to test communicate with the tester
program over a pipe.
If you're not using Bazel to run these tests, make sure you build the C++
tester code beforehand, e.g. from the base directory:
$ cmake . -Dprotobuf_BUILD_CONFORMANCE=ON && cmake --build .This will produce a conformance_test_runner binary that can be used to run
conformance tests on any executable. Pass it --help for more information.
Running the tests for C++
To run the tests against the C++ implementation, run:
$ bazel test //src:conformance_testOr alternatively with CMake:
$ ctest -R conformance_cpp_testRunning the tests for other languages
All of the languages in the Protobuf source tree are set up to run conformance
tests using similar patterns. You can either use Bazel to run theconformance_test target defined in the language's root BUILD.bazel file,
or create an executable for a custom test and pass it toconformance_test_runner.
Note: CMake can be used to build the conformance test runner, but not any of
the conformance test executables outside C++. So if you aren't using Bazel
you'll need to create the executable you pass to conformance_test_runner via
some alternate build system.
While we plan to model all our supported languages more completely in Bazel,
today some of them are a bit tricky to run. Below is a list of the commands
(and prerequisites) to run each language's conformance tests.
C#:
$ which dotnet || echo "You must have dotnet installed!"
$ bazel test //csharp:conformance_test \
--action_env=DOTNET_CLI_TELEMETRY_OPTOUT=1 --test_env=DOTNET_CLI_HOME=~ \
--action_env=DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1Java:
$ bazel test //java/core:conformance_test //java/lite:conformance_testObjective-C (Mac only):
$ bazel test //objectivec:conformance_test --macos_minimum_os=12.0PHP:
$ bazel test //php:conformance_testPHP (C):
$ which gcc || echo "gcc is required!"
$ which libtool || echo "libtool is required!"
$ which make || echo "make is required!"
$ which pear || echo "pear is required! It might require a development version of PHP such as a php-dev package"
$ which pecl || echo "pecl is required! It might require a development version of PHP such as a php-dev package"
$ which phpize || echo "phpize is required! It might require a development version of PHP such as a php-dev package"
$ bazel test //php:conformance_test_cPython:
$ bazel test //python:conformance_testPython (C++):
$ bazel test //python:conformance_test_cpp --define=use_fast_cpp_protos=trueRuby:
$ [[ $(ruby --version) == "ruby"* ]] || echo "Select a C Ruby!"
$ bazel test //ruby:conformance_test --define=ruby_platform=c \
--action_env=PATH --action_env=GEM_PATH --action_env=GEM_HOMERuby (JRuby):
$ [[ $(ruby --version) == "jruby"* ]] || echo "Switch to Java Ruby!"
$ bazel test //ruby:conformance_test_jruby --define=ruby_platform=java \
--action_env=PATH --action_env=GEM_PATH --action_env=GEM_HOMETesting other Protocol Buffer implementations
To run these tests against a new Protocol Buffers implementation, write a
program in your language that uses the protobuf implementation you want
to test. This program should implement the testing protocol defined in
conformance.proto.
This is designed to be as easy as possible: the C++ version is only
150 lines and is a good example for what this program should look like
(see conformance_cpp.cc).
The program only needs to be able to read from stdin and write to stdout.
Portability
Note that the test runner currently does not work on Windows. Patches
to fix this are welcome! (But please get in touch first to settle on
a general implementation strategy).
Overview (`csharp/README.md`)
This directory contains the C# Protocol Buffers runtime library.
Usage
The easiest way how to use C# protobufs is via the Google.Protobuf
NuGet package. Just add the NuGet package to your VS project.
You will also want to install the Google.Protobuf.Tools NuGet package, which
contains precompiled version of protoc.exe and a copy of well known .proto
files under the package's tools directory.
To generate C# files from your .proto files, invoke protoc with the--csharp_out option.
Supported platforms
The runtime library is built as a class library, supporting targets of:
- .NET 4.5+ (
net45) - .NET Standard 1.1 and 2.0 (
netstandard1.1andnetstandard2.0) - .NET 5+ (
net50)
You should be able to use Protocol Buffers in Visual Studio 2012 and
all later versions. This includes all code generated by protoc,
which only uses features from C# 3 and earlier. When compiling generated
code with old compilers (before C# 7.2) you need to define theGOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE symbol in your project
so that the generated classes don't implement IBufferMessage, which usesref struct types.
Building
Open the src/Google.Protobuf.sln solution in Visual Studio 2022 or
later.
Although users of this project are only expected to have Visual
Studio 2012 or later, developers of the library are required to
have Visual Studio 2022 or later, as the library uses C# 10 features
in its implementation and runs tests under .NET 6. These features
have no impact when using the compiled code - they're only relevant
when building the Google.Protobuf assembly.
Testing
The unit tests use NUnit 3. Tests can be
run using the Visual Studio Test Explorer or dotnet test.
.NET 3.5
We don't support .NET 3.5. It used to be feasible to build this library
targeting .NET 3.5, but a number of changes requiring newer runtime/framework
features have been added over time. While it would no doubt be possible to
rework the current implementation to allow most of the functionality to be built
in .NET 3.5, this would create an undue maintenance burden.
History of C# protobufs
This subtree was originally imported from https://github.com/jskeet/protobuf-csharp-port
and represents the latest development version of C# protobufs, that will now be developed
and maintained by Google. All the development will be done in open, under this repository
(https://github.com/protocolbuffers/protobuf).
The previous project differs from this project in a number of ways:
- The old code only supported proto2; the new code initially only supported
proto3 (so no unknown fields, no required/optional distinction, no
extensions); since then proto2 support has been added - The old code was based on immutable message types and builders for
them - The old code did not support maps or
oneof - The old code had its own JSON representation, whereas the new code
uses the standard protobuf JSON representation - The old code had no notion of the "well-known types" which have
special support in the new code - The old project supported some older platforms (such as older
versions of Silverlight) which are not currently supported in the
new project
Overview (`examples/README.md`)
Protocol Buffers - Code Example
This directory contains example code that uses Protocol Buffers to manage an
address book. Two programs are provided for each supported language. The
add_person example adds a new person to an address book, prompting the user to
input the person's information. The list_people example lists people already in
the address book. The examples use the exact same format in all three languages,
so you can, for example, use add_person_java to create an address book and then
use list_people_python to read it.
These examples are part of the Protocol Buffers tutorial, located at:
https://developers.google.com/protocol-buffers/docs/tutorials
Build the example using bazel
The example requires bazel 0.5.4 or newer to build. You can download/install
the latest version of bazel from bazel's release page:
https://github.com/bazelbuild/bazel/releasesOnce you have bazel installed, simply run the following command in this examples
directory to build the code:
$ bazel build :allThen you can run the built binary:
$ bazel-bin/add_person_cpp addressbook.dataTo use protobuf in your own bazel project, please follow instructions in the
BUILD.bazel file and WORKSPACE file.
Build the example using make
You must install the protobuf package before you can build it using make. The
minimum requirement is to install protocol compiler (i.e., the protoc binary)
and the protobuf runtime for the language you want to build.
You can simply run "make" to build the example for all languages (except for
Go). However, since different languages have different installation requirements,
it will likely fail. It's better to follow individual instructions below to
build only the language you are interested in.
C++
You can follow instructions in ../src/README.md to install
protoc from source.
Then run "make cpp" in this examples directory to build the C++ example. It
will create two executables: add_person_cpp and list_people_cpp. These programs
simply take an address book file as their parameter. The add_person_cpp
programs will create the file if it doesn't already exist.
To run the examples:
$ ./add_person_cpp addressbook.data
$ ./list_people_cpp addressbook.dataNote that on some platforms you may have to edit the Makefile and remove
"-lpthread" from the linker commands (perhaps replacing it with something else).
We didn't do this automatically because we wanted to keep the example simple.
Python
Follow instructions in ../README.md to install protoc and then
follow ../python/README.md to install protobuf python
runtime from source. You can also install python runtime using pip:
$ pip install protobufMake sure the runtime version is the same as protoc binary, or it may not work.
After you have install both protoc and python runtime, run "make python" to
build two executables (shell scripts actually): add_person_python and
list_people_python. They work the same way as the C++ executables.
Java
Follow instructions in ../README.md to install protoc and then
download protobuf Java runtime .jar file from maven:
https://mvnrepository.com/artifact/com.google.protobuf/protobuf-javaThen run the following:
$ export CLASSPATH=/path/to/protobuf-java-[version].jar
$ make javaThis will create the add_person_java/list_people_java executables (shell
scripts) and can be used to create/display an address book data file.
Go
Follow instructions in ../README.md to install protoc. Then
install the Go protoc plugin (protoc-gen-go):
$ go install google.golang.org/protobuf/cmd/protoc-gen-go@latestThe "go install" command will install protoc-gen-go into the GOBIN
directory. You can set the $GOBIN environment variable before
running "go install" to change the install location. Make sure the
install directory is in your shell $PATH.
Build the Go samples with "make go". This creates the following
executable files in the current directory:
add_person_go list_people_goTo run the example:
./add_person_go addressbook.datato add a person to the protocol buffer encoded file addressbook.data. The file
is created if it does not exist. To view the data, run:
./list_people_go addressbook.dataObserve that the C++, Python, Java, and Dart examples in this directory run in a
similar way and can view/modify files created by the Go example and vice
versa.
Dart
First, follow the instructions in ../README.md to install the Protocol Buffer Compiler (protoc).
Then, install the Dart Protocol Buffer plugin as described here.
Note, the executable bin/protoc-gen-dart must be in your PATH for protoc to find it.
Build the Dart samples in this directory with make dart.
To run the examples:
$ dart add_person.dart addressbook.data
$ dart list_people.dart addressbook.dataThe two programs take a protocol buffer encoded file as their parameter.
The first can be used to add a person to the file. The file is created
if it does not exist. The second displays the data in the file.
Overview (`hpb_generator/README.md`)
hpb Generator
This directory contains the generator for the hpb
API, an
experimental C++ protobuf implementation. Most users should use the standard
C++ implementation
here.
Overview (`java/README.md`)
Protocol Buffers - Google's data interchange format
Copyright 2008 Google Inc.
https://developers.google.com/protocol-buffers/
Use Java Protocol Buffers
To use protobuf in Java, first obtain the protocol compiler (a.k.a., protoc,
see instructions in the toplevel README.md) and use it to
generate Java code for your .proto files:
$ protoc --java_out=${OUTPUT_DIR} path/to/your/proto/fileInclude the generated Java files in your project and add a dependency on the
protobuf Java runtime.
Maven
If you are using Maven, use the following:
<dependency>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java</artifactId>
<version></version>
</dependency>And replace `` with a version from the
Maven Protocol Buffers Repository.
For example, 4.28.2.
Make sure the version number of the runtime matches (or is newer than) the
version number of the protoc.
If you want to use features like protobuf JsonFormat, add a dependency on the
protobuf-java-util package:
<dependency>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java-util</artifactId>
<version></version>
</dependency>Use Java Protocol Buffers on Android
For Android users, it's recommended to use protobuf Java Lite runtime because
of its smaller code size. Java Lite runtime also works better with Proguard
because it doesn't rely on Java reflection and is optimized to allow as much
code stripping as possible. You can following these instructions to use Java
Lite runtime.
Use Java Protocol Buffers with Bazel
Bazel has native build rules to work with protobuf. For Java, you can use thejava_proto_library rule for server and the java_lite_proto_library rule for
Android. Check out our build files examples to learn
how to use them.
Build from Source
Most users should follow the instructions above to use protobuf Java runtime.
If you are contributing code to protobuf or want to use a protobuf version
that hasn't been officially released yet, you can follow the instructions
below to build protobuf from source code.
Build from Source
You may follow these instructions to build from source. This does not require
Maven to be installed. Note that these instructions skip running unit tests and
only describes how to install the core protobuf library (without the util
package).
Build the C++ code, or obtain a binary distribution of protoc (see
the toplevel README.md). If you install a binary
distribution, make sure that it is the same version as this package.
If in doubt, run:$ protoc --version
If you built the C++ code without installing, the compiler binary
should be located in ../src.Invoke protoc to build DescriptorProtos.java:
$ protoc --java_out=core/src/main/java -I../src
../src/google/protobuf/descriptor.protoCompile the code in core/src/main/java using whatever means you prefer.
Install the classes wherever you prefer.
Kotlin Protocol Buffers
This directory also provides support for Kotlin protocol buffers, which are
built on top of Java protocol buffers. Kotlin protocol buffers require a
dependency on Java protocol buffers, and both Java and Kotlin protocol buffer
code must be generated for every proto file.
The main goal of Kotlin protobuf is to provide idiomatic ways to build and read
protocol buffers in Kotlin. Learn more about Kotlin protobufs in our
documentation.
Use Kotlin Protocol Buffers
To use protobuf in Kotlin, first install the protocol compiler (protoc --
instructions for installing are in the top-level README.md) and
use it to generate Java and Kotlin code for your .proto files:
$ protoc --java_out=${OUTPUT_DIR} --kotlin_out=${OUTPUT_DIR} path/to/your/proto/fileInclude the generated Java and Kotlin files in your project and add a dependency on the
protobuf Java and Kotlin runtime.
Maven
If you are using Maven, use the following:
<dependency>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java</artifactId>
<version></version>
</dependency>
<dependency>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-kotlin</artifactId>
<version></version>
</dependency>Replace `` with a version from the
Maven Protocol Buffers Repository,
such as 4.28.2.
Make sure the version number of the runtimes match each other and match (or are
newer than) the version number of the protoc.
Use Kotlin Protocol Buffers on Android
For Android users, it's recommended to use the Java Lite runtime for its smaller
code size. We provide a protobuf-kotlin-lite package in Maven and Bazel to
pair with the Java Lite runtime. Use these if you want to use Kotlin on
Android or in another context where you want to use Java Lite. Similar to the
full runtime, protobuf-kotlin-lite requires a dependency onprotobuf-java-lite.
Compatibility Notice
Protobuf minor version releases are backwards-compatible. If your code
can build/run against the old version, it's expected to build/run against
the new version as well. Both binary compatibility and source compatibility
are guaranteed for minor version releases if the user follows the guideline
described in this section.Protobuf major version releases may also be backwards-compatible with the
last release of the previous major version. See the release notice for more
details.APIs marked with the @ExperimentalApi annotation are subject to change. They
can be modified in any way, or even removed, at any time. Don't use them if
compatibility is needed. If your code is a library itself (i.e. it is used on
the CLASSPATH of users outside your own control), you should not use
experimental APIs, unless you repackage them (e.g. using ProGuard).Deprecated non-experimental APIs will be removed two years after the release
in which they are first deprecated. You must fix your references before this
time. If you don't, any manner of breakage could result (you are not
guaranteed a compilation error).Protobuf message interfaces/classes are designed to be subclassed by protobuf
generated code only. Do not subclass these message interfaces/classes
yourself. We may add new methods to the message interfaces/classes which will
break your own subclasses.Don't use any method/class that is marked as "used by generated code only".
Such methods/classes are subject to change.Protobuf LITE runtime APIs are not stable yet. They are subject to change even
in minor version releases.
Documentation
The complete documentation for Protocol Buffers is available via the
web at:
https://developers.google.com/protocol-buffers/
Overview (`lua/README.md`)
upb Lua bindings
These are some bare-bones upb bindings for Lua.
These bindings exist primarily for experimentation and testing.
They are incomplete and are not really intended for use in any application.
This is by no means a complete or supported protobuf library, and in fact
we don't even claim it to be functional.
Overview (`objectivec/README.md`)
Protocol Buffers - Google's data interchange format
Copyright 2008 Google Inc.
This directory contains the Objective C Protocol Buffers runtime library.
Requirements
The Objective C implementation requires:
- Objective C 2.0 Runtime (32bit & 64bit iOS, 64bit OS X).
- Xcode 13.3.1 (or later).
- The library code does not use ARC (for performance reasons), but it all can
be called from ARC code.
Installation
The distribution pulled from github includes the sources for both the
compiler (protoc) and the runtime (this directory). After cloning the distribution
and needed submodules (see the src directory's README),
to build the compiler and run the runtime tests, you can use:
$ objectivec/DevTools/full_mac_build.shThis will generate the protoc binary.
Building
There are two ways to include the Runtime sources in your project:
Add objectivec/*.h, objectivec/google/protobuf/*.pbobjc.h, andobjectivec/GPBProtocolBuffers.m to your project.
or
Add objectivec/*.h, objectivec/google/protobuf/*.pbobjc.h,objectivec/google/protobuf/*.pbobjc.m, and objectivec/*.m except forobjectivec/GPBProtocolBuffers.m to your project.
If the target is using ARC, remember to turn off ARC (-fno-objc-arc) for the.m files.
The files generated by protoc for the *.proto files (*.pbobjc.h and*.pbobjc.m) are then also added to the target.
Usage
The objects generated for messages should work like any other Objective C
object. They are mutable objects, but if you don't change them, they are safe
to share between threads (similar to passing an NSMutableDictionary between
threads/queues; as long as no one mutates it, things are fine).
There are a few behaviors worth calling out:
A property that is type NSString* will never return nil. If the value is
unset, it will return an empty string (@""). This is inpart to align things
with the Protocol Buffers spec which says the default for strings is an empty
string, but also so you can always safely pass them to isEqual:/compare:, etc.
and have deterministic results.
A property that is type NSData* also won't return nil, it will return an empty
data ([NSData data]). The reasoning is the same as for NSString not returning
nil.
A property that is another GPBMessage class also will not return nil. If the
field wasn't already set, you will get a instance of the correct class. This
instance will be a temporary instance unless you mutate it, at which point it
will be attached to its parent object. We call this pattern autocreators.
Similar to NSString and NSData properties it makes things a little safer when
using them with isEqual:/etc.; but more importantly, this allows you to write
code that uses Objective C's property dot notation to walk into nested objects
and access and/or assign things without having to check that they are not nil
and create them each step along the way. You can write this:
- (void)updateRecord:(MyMessage *)msg {
...
// Note: You don't have to check subMessage and otherMessage for nil and
// alloc/init/assign them back along the way.
msg.subMessage.otherMessage.lastName = @"Smith";
...
}If you want to check if a GPBMessage property is present, there is always ashas\[NAME\] property to go with the main property to check if it is set.
A property that is of an Array or Dictionary type also provides autocreator
behavior and will never return nil. This provides all the same benefits you
see for the message properties. Again, you can write:
- (void)updateRecord:(MyMessage *)msg {
...
// Note: Just like above, you don't have to check subMessage and otherMessage
// for nil and alloc/init/assign them back along the way. You also don't have
// to create the siblingsArray, you can safely just append to it.
[msg.subMessage.otherMessage.siblingsArray addObject:@"Pat"];
...
}If you are inspecting a message you got from some other place (server, disk,
etc), you may want to check if the Array or Dictionary has entries without
causing it to be created for you. For this, there is always a \[NAME\]_Count
property also provided that can return zero or the real count, but won't trigger
the creation.
All message fields always have a value when accessed. For primitive type
fields (ints, floats, bools, enum) there the concept of presence, that allows
you to tell the difference between when the field is the default value because
it was not set and when it was set, but explicitly to the default
value for the field. For fields with that do support presence, you can test
if the value was explicitly set via the has\[NAME\] property. If the value
has been set, and you want to clear it, you can set the has\[NAME\] to NO.
The Objective C classes/enums can be used from Swift code.
Objective C Generator Proto File Options
*objc_class_prefix=* (no default)
This options allow you to provide a custom prefix for all the symbols generated
from a proto file (classes (from message), enums, the Root for extension
support).
If not set, the generation options package_to_prefix_mappings_path anduse_package_as_prefix (documented below) controls what is used instead. Since
Objective C uses a global namespace for all of its classes, there can be collisions.use_package_as_prefix=yes should avoid collisions since proto package are used to
scope/name things in other languages, but this option can be used to get shorter
names instead. Convention is to base the explicit prefix on the proto package.
Objective C Generator `protoc` Options
When generating Objective C code, protoc supports a --objc_opt argument; the
argument is comma-delimited name/value pairs (key=value,key2=value2). The
keys are used to change the behavior during generation. The currently
supported keys are:
generate_for_named_framework: Thevalueused for this key will be used
when generating the#importstatements in the generated code. Instead
of being plain#import "some/path/file.pbobjc.h"lines, they will be
framework based, i.e. -#import <VALUE/file.pbobjc.h>.NOTE: If this is used with
named_framework_to_proto_path_mappings_path,
then this is effectively the default to use for everything that wasn't
mapped by the other.named_framework_to_proto_path_mappings_path: Thevalueused for this key
is a path to a file containing the listing of framework names and proto
files. The generator uses this to decide if another proto file referenced
should use a framework style import vs. a user level import
(#import <FRAMEWORK/file.pbobjc.h>vs#import "dir/file.pbobjc.h").The format of the file is:
- An entry is a line of
frameworkName: file.proto, dir/file2.proto. - Comments start with
#. - A comment can go on a line after an entry.
(i.e. -frameworkName: file.proto # comment)
Any number of files can be listed for a framework, just separate them with
commas.There can be multiple lines listing the same frameworkName in case it has a
lot of proto files included in it; and having multiple lines makes things
easier to read.- An entry is a line of
runtime_import_prefix: Thevalueused for this key to be used as a
prefix on#imports of runtime provided headers in the generated files.
When integrating ObjC protos into a build system, this can be used to avoid
having to add the runtime directory to the header search path since the
generate#importwill be more complete.package_to_prefix_mappings_path: Thevalueused for this key is a
path to a file containing a list of proto packages and prefixes.
The generator will use this to locate which ObjC class prefix to use when
generating sources unless theobjc_class_prefixfile option is set.
This option can be useful if multiple apps consume a common set of
proto files but wish to use a different prefix for the generated sources
between them. This option takes precedent over theuse_package_as_prefix
option.The format of the file is:
- An entry is a line of "package=prefix".
- Comments start with
#. - A comment can go on a line after a expected package/prefix pair.
(i.e. - "package=prefix # comment") - For files that do NOT have a proto package (not recommended), an
entry can be made as "no_package:PATH=prefix", where PATH is the
path for the .proto file.
use_package_as_prefix,package_as_prefix_forced_prefixandproto_package_prefix_exceptions_path: Thevalueforuse_package_as_prefixcan beyesorno, and indicates if a prefix
should be derived from the proto package for all the symbols for files that
don't have theobjc_class_prefixfile option (mentioned above). This helps
ensure the symbols are more unique and means there is less chance of ObjC
class name collisions.To help in migrating code to using this support,
proto_package_prefix_exceptions_pathcan be used to provide the path
to a file that contains proto package names (one per line, comments allowed
if prefixed with#). These package won't get the derived prefix, allowing
migrations to the behavior one proto package at a time across a code base.package_as_prefix_forced_prefixcan be used to provide a value that will
be used before all prefixes derived from the packages to help group all of
these types with a common prefix. Thus it only makes sense to use it whenuse_package_as_prefixis also enabled. For example, setting this to
"XYZ_" and generating a file with the package "something" defining
"MyMessage", would have Objective-C class beXYZ_Something_MyMessage.use_package_as_prefixcurrently defaults tono(existing behavior), but
that could change in the future as it helps avoid collisions when more
protos get added to the build. Note that this would be a breaking change.headers_use_forward_declarations: Thevaluefor this can beyesorno, and indicates if the generated headers use forward declarations for
Message and Enum types from other .proto files or if the files should be
imported into the generated header instead.By using forward declarations, less code is likely to recompile when the
files do change, but Swift generally doesn't like forward declarations and
will fail to include properties when the concrete definition of the type is
known at import time. If your proto usages span modules, this can be a
problem.headers_use_forward_declarationscurrently defaults toyes(existing
behavior), but in a future release, that default may change to provide
better Swift support by default.
Contributing
Please make updates to the tests along with changes. If just changing the
runtime, the Xcode projects can be used to build and run tests. If your change
also requires changes to the generated code,objectivec/DevTools/full_mac_build.sh can be used to easily rebuild and test
changes. Passing -h to the script will show the addition options that could
be useful.
Documentation
The complete documentation for Protocol Buffers is available via the
web at:
https://developers.google.com/protocol-buffers/
Overview (`php/README.md`)
This directory contains the Protocol Buffers runtime implementation via both a
pure PHP package and a native c extension. The pure PHP package is intended to
provide usability to wider range of PHP platforms, while the c extension is
intended to provide higher performance. Both implementations provide the same
runtime APIs and share the same generated code. Users don’t need to re-generate
code for the same proto definition when they want to switch the implementation
later.
Both implementations make use of generated PHP code that defines message and
enum types in PHP. We strongly recommend using protoc's PHP generation support
with .proto files. The build process in this directory only installs the
extension/package; you need to install protoc as well to have PHP code
generation functionality.
Requirements
For information on supported language versions and how the support levels for
PHP versions will change over time, see
Supported PHP versions.
Installation
C Extension
Prerequirements
To install the c extension, the following tools are needed: * gcc * libtool *
make * pear * pecl * phpize
On Ubuntu, you can install them with:
sudo apt-get install -y php-pear php-dev libtool make gccOn other platforms, please use the corresponding package managing tool to
install them before proceeding.
Installation from Source (Building extension)
To build the c extension, run the following command:
cd ext/google/protobuf
pear package
sudo pecl install protobuf-{VERSION}.tgzInstallation from PECL
When we release a version of Protocol Buffers, we will upload the extension to
PECL. To use this pre-packaged extension, simply
install it as you would any other extension:
sudo pecl install protobuf-{VERSION}PHP Package
Installation from composer
Simply add "google/protobuf" to the 'require' section of composer.json in your
project.
To use the pure PHP implementation, you need to install bcmath.
Protoc
Once the extension or package is installed, if you wish to generate PHP code
from a .proto file, you will also want to install the Protocol Buffers
compiler (protoc), as described in this repository's main README file. The
version of protoc included in the latest release supports the --php_out
option to generate PHP code:
protoc --php_out=out_dir test.protoUsage
For generated code:
https://developers.google.com/protocol-buffers/docs/reference/php-generated
Known Issues
- Missing native support for well known types.
- Missing support for proto2.
- No API provided for clear/copy messages.
- No API provided for encoding/decoding with stream.
- Map fields may not be garbage-collected if there is cycle reference.
- No debug information for messages in c extension.
- HHVM not tested.
- C extension not tested on windows, mac, php 7.0.
- Message name cannot be Empty.
Development
Test Native PHP
# Install Dependencies (Linux)
apt-get install bazel composer php-dev
# Download protobuf
git clone https://github.com/protocolbuffers/protobuf.git
cd protobuf
# Build protoc
bazel build :protoc
# Test native php
cd php
composer install
composer testTest C Extension
After you have finished testing the native php, you can test the c extension:
cd tests
./test.sh 5.6 # The php runtime version.
# We provide 5.5, 5.5-zts, 5.6, 5.6-zts, 7.0, 7.0-zts, 7.1, 7.1-zts, 7.2, 7.2-zts, 7.3 and 7.3-zts
# ls /usr/local for more detailsIf you want to use gdb to debug the c extension, you can do:
./gdb_test.shOverview (`pkg/README.md`)
Protobuf packaging
This directory contains Bazel rules for building packaging and distribution
artifacts.
Everything in this directory should be considered internal and subject to
change.
Protocol compiler binary packaging
The protocol compiler is used in binary form in various places. There are rules
which package it, along with commonly used .proto files, for distribution.
Source distribution packaging
Protobuf releases include source distributions, sliced by target language (C++,
Java, etc.). There are rules in this package to define those source archives.
These depend upon pkg_files rules elsewhere in the repo to get the contents.
The source distribution files should include the outputs from autogen.sh, but
this isn't something we can reliably do from Bazel. To produce fully functioning
source distributions, run autogen.sh before building the archives (this
populates the necessary files directly into the source tree).
C++ runtime binary distribution
The cc_dist_library rule creates composite libraries from several othercc_library targets. Bazel uses a "fine-grained" library model, where eachcc_library produces its own library artifacts, without transitive
dependencies. The cc_dist_library rule combines several other libraries
together, creating a single library that may be suitable for distribution.
Overview (`python/README.md`)
Protocol Buffers Python
This directory contains the Protobuf library for Python.
For user documentation about how to use Protobuf Python, see
https://protobuf.dev/getting-started/pythontutorial/
Installation
In most cases you should install the library using pip or another package
manager:
$ pip install protobufThe packages released on https://pypi.org/project/protobuf/#files include both a
source distribution and binary wheels.
Building packages from this repo
If for some reason you wish to build the packages directly from this repo, you
can use the following Bazel commands:
$ bazel build //python/dist:source_wheel
$ bazel build //python/dist:binary_wheelThe binary wheel will build against whatever version of Python is installed on
your system. The source package is always the same and does not depend on a
local version of Python.
Building from `setup.py`
We support building from setup.py, but only from a Python source package.
You cannot build from setup.py using the GitHub repo or the GitHub source
tarball.
To build a source package from this repo, see the instructions in the previous
section.
Implementation backends
There are three separate implementations of Python Protobuf. All of them offer
the same API and are thus functionally the same, though they have very different
performance characteristics.
The runtime library contains a switching layer that can choose between these
backends at runtime. Normally it will choose between them automatically, using
priority-ordered list, and skipping any backends that are not available. However
you can request a specific backend by setting thePROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION environment variable to one of the
following values:
- upb: Built on the
upb C library,
this is a new extension module
released in 4.21.0. It offers
better performance than any of the previous backends, and it is now the
default. It is distributed in our PyPI packages, and requires no special
installation. The code for this module lives in this directory. - cpp: This extension module wraps the C++ protobuf library. It is
deprecated and is no longer released in our PyPI packages, however it is
still used in some legacy cases where apps want to perform zero-copy message
sharing between Python and C++. It must be installed separately before it
can be used. The code for this module lives in
google/protobuf/pyext. - python: The pure-Python backend, this does not require any extension
module to be present on the system. The code for the pure-Python backend
lives in google/protobuf/internal
The order given above is the general priority order, with upb being preferred
the most and the python backend preferred the least. However this ordering can
be overridden by the presence of agoogle.protobuf.internal._api_implementation module. See the logic in
api_implementation.py
for details.
You can check which backend you are using with the following snippet:
$ python
Python 3.10.9 (main, Dec 7 2022, 13:47:07) [GCC 12.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> from google.protobuf.internal import api_implementation
>>> print(api_implementation.Type())
upbThis is not an officially-supported or stable API, but it is useful for ad hoc
diagnostics.
More information about sharing messages between Python and C++ is available
here: https://protobuf.dev/reference/python/python-generated/#sharing-messages
Code generator
The code for the Protobuf Python code generator lives in
//src/google/protobuf/compiler/python.
The code generator can output two different files for each proto foo.proto:
- foo_pb2.py: The module you import to actually use the protos.
- foo_pb2.pyi: A stub file that describes the interface of the protos.
The foo_pb2.pyi file is useful for IDEs or for users who want to read the
output file. The foo_pb2.py file is optimized for fast loading and is not
readable at all.
Note that the pyi file is only generated if you pass the pyi_out option toprotoc:
$ protoc --python_out=pyi_out:output_dirOverview (`ruby/README.md`)
This directory contains the Ruby extension that implements Protocol Buffers
functionality in Ruby.
The Ruby extension makes use of generated Ruby code that defines message and
enum types in a Ruby DSL. You may write definitions in this DSL directly, but we
recommend using protoc's Ruby generation support with .proto files. The build
process in this directory only installs the extension; you need to install
protoc as well to have Ruby code generation functionality. You can build protoc
from source using bazel build //:protoc.
Installation from Gem
In Gemfile (Please check which version of Protocol Buffers you need: RubyGems):
gem 'google-protobuf'Or for using this pre-packaged gem, simply install it as you would any other gem:
$ gem install [--prerelease] google-protobufOnce the gem is installed, you may or may not need protoc. If you write your
message type descriptions directly in the Ruby DSL, you do not need it.
However, if you wish to generate the Ruby DSL from a .proto file, you will
also want to install Protocol Buffers itself, as described in this repository's
main README file. The version of protoc included in the latest release
supports the --ruby_out option to generate Ruby code.
A simple example of using the Ruby extension follows. More extensive
documentation may be found on
rubydoc.info.
require 'google/protobuf'
# generated from my_proto_types.proto with protoc:
# $ protoc --ruby_out=. my_proto_types.proto
require 'my_proto_types'
mymessage = MyTestMessage.new(:field1 => 42, :field2 => ["a", "b", "c"])
mymessage.field1 = 43
mymessage.field2.push("d")
mymessage.field3 = SubMessage.new(:foo => 100)
encoded_data = MyTestMessage.encode(mymessage)
decoded = MyTestMessage.decode(encoded_data)
assert_equal mymessage, decoded
puts "JSON:"
puts MyTestMessage.encode_json(mymessage)Installation from Source (Building Gem)
Protocol Buffers has a new experimental backend that uses the
ffi gem to provide a unified C-based
implementation across Ruby interpreters based on
UPB. For now, use
of the FFI implementation is opt-in. If any of the following are true, the
traditional platform-native implementations (MRI-ruby based on CRuby, Java based
on JRuby) are used instead of the new FFI-based implementation: 1. ffi andffi-compiler gems are not installed 2. PROTOCOL_BUFFERS_RUBY_IMPLEMENTATION
environment variable has a value other than FFI (case-insensitive). 3. FFI is
unable to load the native library at runtime.
To build this Ruby extension, you will need:
- Rake
- Bundler
- Ruby development headers
- a C compiler
To Build the JRuby extension, you will need:
- Maven
- The latest version of the protobuf java library (see ../java/README.md)
- Install JRuby via rbenv or RVM
First switch to the desired platform with rbenv or RVM.
Then install the required Ruby gems:
$ gem install bundler
$ bundleThen build the Gem:
$ rake
$ rake clobber_package gem
$ gem install `ls pkg/google-protobuf-*.gem`If you intend to debug the protobuf_c Ruby bindings with gdb, you can also
build a version with debug symbols enabled by setting the PROTOBUF_CONFIG
environment variable when you build the native extension:
$ PROTOBUF_CONFIG=dbg rakeTo run the specs:
$ rake testTo run the specs while using the FFI-based implementation:
$ PROTOCOL_BUFFERS_RUBY_IMPLEMENTATION=FFI rake testThis gem includes the upb parsing and serialization library as a single-file
amalgamation. It is up-to-date with upb git commit535bc2fe2f2b467f59347ffc9449e11e47791257.
Alternatively, you can use Bazel to build and to run tests.
From the project root (rather than the ruby directory):
$ bazel test //ruby/tests/...To run tests against the FFI implementation:
$ bazel test //ruby/tests/... //ruby:ffi_enabled --test_env=PROTOCOL_BUFFERS_RUBY_IMPLEMENTATION=FFIVersion Number Scheme
We are using a version number scheme that is a hybrid of Protocol Buffers'
overall version number and some Ruby-specific rules. Gem does not allow
re-uploads of a gem with the same version number, so we add a sequence number
("upload version") to the version. We also format alphabetical tags (alpha,
pre, ...) slightly differently, and we avoid hyphens. In more detail:
- First, we determine the prefix: a Protocol Buffers version "3.0.0-alpha-2"
becomes "3.0.0.alpha.2". When we release 3.0.0, this prefix will be simply
"3.0.0". - We then append the upload version: "3.0.0.alpha.2.0" or "3.0.0.0". If we need
to upload a new version of the gem to fix an issue, the version becomes
"3.0.0.alpha.2.1" or "3.0.0.1". - If we are working on a prerelease version, we append a prerelease tag:
"3.0.0.alpha.3.0.pre". The prerelease tag comes at the end so that when
version numbers are sorted, any prerelease builds are ordered between the
prior version and current version.
These rules are designed to work with the sorting rules for
Gem::Version:
release numbers should sort in actual release order.
Overview (`src/README.md`)
Protocol Buffers - Google's data interchange format
Copyright 2008 Google Inc.
CMake Installation
To compile or install protobuf from source using CMake, see
cmake/README.md.
C++ Protobuf - Unix
To build protobuf from source, the following tools are needed:
- Bazel
- git
- g++
- Abseil
On Ubuntu/Debian, for example, you can install them with:
sudo apt-get install g++ git bazelOn other platforms, use the corresponding package managing tool to install them
before proceeding. See https://bazel.build/install for further instructions on
installing Bazel, or to build from source using CMake, see
cmake/README.md. See https://github.com/abseil/abseil-cpp
for instructions on installing Abseil.
To get the source, download the release .tar.gz or .zip package in the
release page:
https://github.com/protocolbuffers/protobuf/releases/latestYou can also get the source by "git clone" our git repository. Make sure you
have also cloned the submodules and generated the configure script (skip this
if you are using a release .tar.gz or .zip package):
git clone https://github.com/protocolbuffers/protobuf.git
cd protobuf
git submodule update --init --recursiveTo build the C++ Protocol Buffer runtime and the Protocol Buffer compiler
(protoc) execute the following:
bazel build :protoc :protobufYou can then install the compiler, for example on Linux:
cp bazel-bin/protoc /usr/local/binFor more usage information on Bazel, see http://bazel.build.
Compiling dependent packages
To compile a package that uses Protocol Buffers, you need to setup a Bazel
WORKSPACE that's hooked up to the protobuf repository and loads its
dependencies. For an example, see WORKSPACE.
Note for Mac users
For a Mac system, Unix tools are not available by default. You will first need
to install Xcode from the Mac AppStore and then run the following command from
a terminal:
sudo xcode-select --installTo install Unix tools, you can install "port" following the instructions at
https://www.macports.org . This will reside in /opt/local/bin/port for most
Mac installations.
sudo /opt/local/bin/port install bazelAlternative for Homebrew users:
brew install bazelThen follow the Unix instructions above.
C++ Protobuf - Windows
If you only need the protoc binary, you can download it from the release
page:
https://github.com/protocolbuffers/protobuf/releases/latestIn the downloads section, download the zip file protoc-$VERSION-win32.zip. It
contains the protoc binary as well as public proto files of protobuf library.
The
protoc binary and the gencode version must match.
You can install protobuf and its dependencies directly by using vcpkg:
>vcpkg install protobuf protobuf:x64-windowsIf you want to use zlib, you'll also need to install the zlib feature:
>vcpkg install protobuf[zlib] protobuf[zlib]:x64-windowsSee https://github.com/Microsoft/vcpkg for more information.
To build from source using Microsoft Visual C++, see cmake/README.md.
To build from source using Cygwin or MinGW, follow the Unix installation
instructions, above.
Binary Compatibility Warning
Due to the nature of C++, it is unlikely that any two versions of the
Protocol Buffers C++ runtime libraries will have compatible ABIs.
That is, if you linked an executable against an older version of
libprotobuf, it is unlikely to work with a newer version without
re-compiling. This problem, when it occurs, will normally be detected
immediately on startup of your app. Still, you may want to consider
using static linkage. You can configure this in your cc_binary Bazel rules
by specifying:
linkstatic=TrueUsage
The complete documentation for Protocol Buffers is available at:
Overview (`upb/README.md`)
μpb: small, fast C protos
μpb (often written 'upb') is a small
protobuf implementation written
in C.
upb is the core runtime for protobuf languages extensions in
Ruby,
PHP, and
Python.
While upb offers a C API, the C API & ABI are not stable. For this reason,
upb is not generally offered as a C library for direct consumption, and there
are no releases.
Features
upb has comparable speed to protobuf C++, but is an order of magnitude smaller
in code size.
Like the main protobuf implementation in C++, it supports:
- a generated API (in C)
- reflection
- binary & JSON wire formats
- text format serialization
- all standard features of protobufs (oneofs, maps, unknown fields, extensions,
etc.) - full conformance with the protobuf conformance tests
upb also supports some features that C++ does not:
- optional reflection: generated messages are agnostic to whether
reflection will be linked in or not. - no global state: no pre-main registration or other global state.
- fast reflection-based parsing: messages loaded at runtime parse
just as fast as compiled-in messages.
However there are a few features it does not support:
- text format parsing
- deep descriptor verification: upb's descriptor validation is not as exhaustive
asprotoc.
Install
For Ruby, use RubyGems:
$ gem install google-protobufFor PHP, use PECL:
$ sudo pecl install protobufFor Python, use PyPI:
$ sudo pip install protobufAlternatively, you can build and install upb using
vcpkg dependency manager:
git clone https://github.com/Microsoft/vcpkg.git
cd vcpkg
./bootstrap-vcpkg.sh
./vcpkg integrate install
./vcpkg install upbThe upb port in vcpkg is kept up to date by microsoft team members and community
contributors.
If the version is out of date, please
create an issue or pull request on the
vcpkg repository.
Contributing
Please see CONTRIBUTING.md.
Overview (`.github/workflows/README.md`)
This directory contains all of our automatically triggered workflows.
Test runner
Our top level test_runner.yml is responsible for kicking off all tests, which
are represented as reusable workflows. This is carefully constructed to satisfy
the design laid out in go/protobuf-gha-protected-resources (see below), and
duplicating it across every workflow file would be difficult to maintain. As an
added bonus, we can manually dispatch our full test suite with a single button
and monitor the progress of all of them simultaneously in GitHub's actions UI.
There are five ways our test suite can be triggered:
Post-submit tests (
push): These are run over newly submitted code
that we can assume has been thoroughly reviewed. There are no additional
security concerns here and these jobs can be given highly privileged access to
our internal resources and caches.Pre-submit tests from a branch (
push_request): These are run over
every PR as changes are made. Since they are coming from branches in our
repository, they have secret access by default and can also be given highly
privileged access. However, we expect many of these events per change,
and likely many from abandoned/exploratory changes. Given the much higher
frequency, we restrict the ability to write to our more expensive caches.Pre-submit tests from a fork (
pull_request_target): These are run
over every PR from a forked repository as changes are made. These have much
more restricted access, since they could be coming from anywhere. To protect
our secret keys and our resources, tests will not run until a commit has been
labeledsafe to submit. Further commits will require further approvals to
run our test suite. Once marked as safe, we will provide read-only access to
our caches and Docker images, but will generally disallow any writes to shared
resources.Continuous tests (
schedule): These are run on a fixed schedule. We
currently have them set up to run daily, and can help identify non-hermetic
issues in tests that don't get run often (such as due to test caching) or during
slow periods like weekends and holidays. Similar to post-submit tests, these
are run over submitted code and are highly privileged in the resources they
can use.Manual testing (
workflow_dispatch): Our test runner can be triggered
manually over any branch. This is treated similarly to pre-submit tests,
which should be highly privileged because they can only be triggered by the
protobuf team.
Staleness handling
While Bazel handles code generation seamlessly, we do support build systems that
don't. There are a handful of cases where we need to check in generated files
that can become stale over time. In order to provide a good developer
experience, we've implemented a system to make this more manageable.
Stale files should have a corresponding
staleness_testBazel target. This
should be markedmanualto avoid getting picked up in CI, but will fail if
files become stale. It also provides a--fixflag to update the stale files.Bazel tests will never depend on the checked-in versions, and will generate
new ones on-the-fly during build.Non-Bazel tests will always regenerate necessary files before starting. This
is done using ourbashanddockeractions, which should be used for any
non-Bazel tests. This way, no tests will fail due to stale files.A post-submit job will immediately regenerate any stale files and commit them
if they've changed.A scheduled job will run late at night every day to make sure the post-submit
is working as expected (that is, it will run all the staleness tests).
The regenerate_stale_files.sh script is the central script responsible for all
the re-generation of stale files.
Forked PRs
Because we need secret access to run our tests, we use the pull_request_target
event for PRs coming from forked repositories. We do checkout the code from the
PR's head, but the workflow files themselves are always fetched from the base
branch (that is, the branch we're merging to). Therefore, any changes to these
files won't be tested, so we explicitly ban PRs that touch these files.
Caches
We have a number of different caching strategies to help speed up tests. These
live either in GCP buckets or in our GitHub repository cache. The former has
a lot of resources available and we don't have to worry as much about bloat.
On the other hand, the GitHub repository cache is limited to 10GB, and will
start pruning old caches when it exceeds that threshold. Therefore, we need
to be very careful about the size and quantity of our caches in order to
maximize the gains.
Bazel remote cache
As described in https://bazel.build/remote/caching, remote caching allows us to
offload a lot of our build steps to a remote server that holds a cache of
previous builds. We use our GCP project for this storage, and configure
every Bazel call to use it. This provides substantial performance
improvements at minimal cost.
We do not allow forked PRs to upload updates to our Bazel caches, but they
do use them. Every other event is given read/write access to the caches.
Because Bazel behaves poorly under certain environment changes (such as
toolchain, operating system), we try to use finely-grained caches. Each job
should typically have its own cache to avoid cross-pollution.
Bazel repository cache
When Bazel starts up, it downloads all the external dependencies for a given
build and stores them in the repository cache. This cache is separate from
the remote cache, and only exists locally. Because we have so many Bazel
dependencies, this can be a source of frequent flakes due to network issues.
To avoid this, we keep a cached version of the repository cache in GitHub's
action cache. Our full set of repository dependencies ends up being ~300MB,
which is fairly expensive given our 10GB maximum. The most expensive ones seem
to come from Java, which has some very large downstream dependencies.
Given the cost, we take a more conservative approach for this cache. Only push
events will ever write to this cache, but all events can read from them.
Additionally, we only store three caches for any given commit, one per platform.
This means that multiple jobs are trying to update the same cache, leading to a
race. GitHub rejects all but one of these updates, so we designed the system so
that caches are only updated if they've actually changed. That way, over time
(and multiple pushes) the repository caches will incrementally grow to encompass
all of our dependencies. A scheduled job will run monthly to clear these caches
to prevent unbounded growth as our dependencies evolve.
ccache
In order to speed up non-Bazel builds to be on par with Bazel, we make use of
ccache. This intercepts all calls to the compiler, and
caches the result. Subsequent calls with a cache-hit will very quickly
short-circuit and return the already computed result. This has minimal affect
on any single job, since we typically only run a single build. However, by
caching the ccache results in GitHub's action cache we can substantially
decrease the build time of subsequent runs.
One useful feature of ccache is that you can set a maximum cache size, and it
will automatically prune older results to keep below that limit. On Linux and
Mac cmake builds, we generally get 30MB caches and set a 100MB cache limit. On
Windows, with debug symbol stripping we get ~70MB and set a 200MB cache limit.
Because CMake build tend to be our slowest, bottlenecking the entire CI process,
we use a fairly expensive strategy with ccache. All events will cache their
ccache directory, keyed by the commit and the branch. This means that each
PR and each branch will write its own set of caches. When looking up which
cache to use initially, each job will first look for a recent cache in its
current branch. If it can't find one, it will accept a cache from the base
branch (for example, PRs will initially use the latest cache from their target
branch).
While the ccache caches quickly over-run our GitHub action cache, they also
quickly become useless. Since GitHub prunes caches based on the time they were
last used, this just means that we'll see quicker turnover.
sccache
An alternative to ccache is sccache. The
two tools are very similar in function, but sccache requires (and allows) much
less configuration and supports GCS storage right out of the box. By hooking
this up to our project that we already use for Bazel caching, we're able to get
even bigger CMake wins in CI because we're no longer constrained by GitHub's
10GB cache limit.
Similar to the Bazel remote cache, we give read access to every CI run, but
disallow writing in PRs from forks.
Bazelisk
Bazelisk will automatically download a pinned version of Bazel on first use.
This can lead to flakes, and to avoid that we cache the result keyed on the
Bazel version. Only push events will write to this cache, but it's unlikely
to change very often.
Docker images
Instead of downloading a fresh Docker image for every test run, we can save it
as a tar and cache it using docker image save and later restore usingdocker image load. This can decrease download times and also reduce flakes.
Note, Docker's load can actually be significantly slower than a pull in certain
situations. Therefore, we should reserve this strategy for only Docker images
that are causing noticeable flakes.
Pip dependencies
The actions/setup-python action we use for Python supports automated caching
of pip dependencies. We enable this to avoid having to download these
dependencies on every run, which can lead to flakes.
Custom actions
We've defined a number of custom actions to abstract out shared pieces of our
workflows.
Bazel use this for running all Bazel tests. It can take either a single
Bazel command or a more general bash command. In the latter case, it provides
environment variables for running Bazel with all our standardized settings.Bazel-Docker nearly identical to the Bazel action, this additionally
runs everything in a specified Docker image.Bash use this for running non-Bazel tests. It takes a bash command and
runs it verbatim. It also handles the regeneration of stale files (which does
use Bazel), which non-Bazel tests might depend on.Docker nearly identical to the Bash action, this additionally runs
everything in a specified Docker image.ccache this sets up a ccache environment, and initializes some
environment variables for standardized usage of ccache.Cross-compile protoc this abstracts out the compilation of protoc using
our cross-compilation infrastructure. It will set aPROTOCenvironment
variable that gets automatically picked up by a lot of our infrastructure.
This is most useful in conjunction with the Bash action with non-Bazel
tests.
Overview (`csharp/keys/README.md`)
Contents
- Google.Protobuf.public.snk:
Public key to verify strong name of Google.Protobuf assemblies. - Google.Protobuf.snk:
Signing key to provide strong name of Google.Protobuf assemblies.
As per Microsoft guidance
signing key should be checked into the repository.
Overview (`csharp/protos/README.md`)
This directory contains unit test protos adapted from those in
src/google/protobuf, and C#-specific test protos for regression
tests against bugs found in the C# codegen or library.
Overview (`java/protoc/README.md`)
Publish pre-compiled protoc artifacts
protoc is the compiler for .proto files. It generates language bindings
for the messages and/or RPC services from .proto files.
Because protoc is a native executable, the scripts under this directory
publish a protoc executable (a.k.a. artifact) to Maven repositories. The
artifact can be used by build automation tools so that users would not need to
compile and install protoc for their systems.
If you would like us to publish protoc artifact for a new platform, please
open an issue to request it.
Maven Location
The published protoc artifacts are available on Maven here:
https://repo.maven.apache.org/maven2/com/google/protobuf/protoc/Versioning
The version of the protoc artifact must be the same as the version of the
Protobuf project.
Artifact name
The name of a published protoc artifact is in the following format:protoc-<version>-<os>-<arch>.exe, e.g., protoc-3.6.1-linux-x86_64.exe.
Note that artifacts for linux/macos also have the .exe suffix but they are
not windows binaries.
System requirement
Install Apache Maven if you don't have it.
The scripts only work under Unix-like environments, e.g., Linux, MacOSX, and
Cygwin or MinGW for Windows. Please see README.md of the Protobuf project
for how to set up the build environment.
Tested build environments
We have successfully built artifacts on the following environments:
- Linux x86_32 and x86_64:
- Centos 6.9 (within Docker 1.6.1)
- Ubuntu 14.04.5 64-bit
- Linux aarch_64: Cross compiled with
g++-aarch64-linux-gnuon Ubuntu 14.04.5 64-bit - Mac OS X x86_32 and x86_64: Mac OS X 10.9.5
Overview (`python/protobuf_distutils/README.md`)
Python setuptools extension
This is an extension for Python setuptools which uses an installed protobuf
compiler (protoc) to generate Python sources.
Installing
To use this extension, it needs to be installed so it can be imported by other
projects' setup.py.
$ python setup.py build
$ python -m pip install .(If you want to test changes to the extension, you can use python setup.py develop.)
Usage
Example setup.py configuration
from setuptools import setup
setup(
# ...
name='example_project',
# Require this package, but only for setup (not installation):
setup_requires=['protobuf_distutils'],
options={
# See below for details.
'generate_py_protobufs': {
'source_dir': 'path/to/protos',
'extra_proto_paths': ['path/to/other/project/protos'],
'output_dir': 'path/to/project/sources', # default '.'
'proto_files': ['relative/path/to/just_this_file.proto'],
'protoc': 'path/to/protoc.exe',
},
},
)Example build invocation
These steps will generate protobuf sources so they are included when building
and installing example_project (see above):
$ python setup.py generate_py_protobufs
$ python setup.py build
$ python -m pip install .Options
source_dir:This is the directory holding .proto files to be processed.
The default behavior is to generate sources for all .proto files found under
source_dir, recursively. This behavior can be controlled with options below.proto_root_path:This is the root path for resolving imports in source .proto files.
The default is the shortest prefix of
source_diramong[source_dir] + self.extra_proto_paths.extra_proto_paths:Specifies additional paths that should be used to find imports, in
addition tosource_dir.This option can be used to specify the path to other protobuf sources,
which are imported by files undersource_dir. No Python code will
be generated for .proto files underextra_proto_paths.output_dir:Specifies where generated code should be placed.
Typically, this should be the root package that generated Python modules
should be below.The generated files will be placed under
output_diraccording to the
relative source paths underproto_root_path. For example, the source file${proto_root_path}/subdir/message.protowill be generated as the Python
module${output_dir}/subdir/message_pb2.py.proto_files:A list of strings, specific .proto file paths for generating code, instead of
searching for all .proto files undersource_path.These paths are relative to
source_dir. For example, to generate code
for just${source_dir}/subdir/message.proto, specify['subdir/message.proto'].protoc:By default, the protoc binary (the Protobuf compiler) is found by
searching the environment path. To use a specific protoc binary, its
path can be specified. Resolution of theprotocvalue is as follows:- If the
--protoc=VALUEflag is passed togenerate_py_protobufs,
thenVALUEwill be used.
For example:shell$ python setup.py generate_py_protobufs --protoc=/path/to/protoc - Otherwise, if a value was set in the
options, it will be used.
(See "Example setup.py configuration," above.) - Otherwise, if the
PROTOCenvironment variable is set, it will be
used. For example:
For example:shell$ PROTOC=/path/to/protoc python setup.py generate_py_protobufs - Otherwise,
$PATHwill be searched.
- If the
Overview (`rust/release_crates/README.md`)
This directory contains one directory per crate that we release.
Each directory contains a pkg_tar() which packages up the crate into a tar which
can be pushed to crates.io.
New development happens in the google_protobuf* crates. The legacyprotobuf* crates are kept as re-exports of their google_protobuf*
equivalents so that existing users keep building, but they setpublish = false and are no longer pushed to crates.io.
Overview (`rust/upb/README.md`)
Rust upb bindings
This directory contains Rust bindings for upb, attempting to be the a direct
mapping to expose upb to Rust. This should not be used directly by application
code, instead is an implementation detail for Rust Protobuf, which is still
under development but can be found here:
here.
Overview (`third_party/utf8_range/README.md`)
Fast UTF-8 validation with Range algorithm (NEON+SSE4+AVX2)
This is a brand new algorithm to leverage SIMD for fast UTF-8 string validation. Both NEON(armv8a) and SSE4 versions are implemented. AVX2 implementation contributed by ioioioio.
Four UTF-8 validation methods are compared on both x86 and Arm platforms. Benchmark result shows range base algorithm is the best solution on Arm, and achieves same performance as Lemire's approach on x86.
- Range based algorithm
- range-neon.c: NEON version
- range-sse.c: SSE4 version
- range-avx2.c: AVX2 version
- range2-neon.c, range2-sse.c: Process two blocks in one iteration
- Lemire's SIMD implementation
- lemire-sse.c: SSE4 version
- lemire-avx2.c: AVX2 version
- lemire-neon.c: NEON porting
- naive.c: Naive UTF-8 validation byte by byte
- lookup.c: Lookup-table method
About the code
- Run "make" to build. Built and tested with gcc-7.3.
- Run "./utf8" to see all command line options.
- Benchmark
- Run "./utf8 bench" to benchmark all algorithms with default test file.
- Run "./utf8 bench size NUM" to benchmark specified string size.
- Run "./utf8 test" to test all algorithms with positive and negative test cases.
- To benchmark or test specific algorithm, run something like "./utf8 bench range".
Benchmark result (MB/s)
Method
- Generate UTF-8 test buffer per test file or buffer size.
- Call validation sub-routines in a loop until 1G bytes are checked.
- Calculate speed(MB/s) of validating UTF-8 strings.
NEON(armv8a)
| Test case | naive | lookup | lemire | range | range2 |
|---|---|---|---|---|---|
| UTF-demo.txt | 562.25 | 412.84 | 1198.50 | 1411.72 | 1579.85 |
| 32 bytes | 651.55 | 441.70 | 891.38 | 1003.95 | 1043.58 |
| 33 bytes | 660.00 | 446.78 | 588.77 | 1009.31 | 1048.12 |
| 129 bytes | 771.89 | 402.55 | 938.07 | 1283.77 | 1401.76 |
| 1K bytes | 811.92 | 411.58 | 1188.96 | 1398.15 | 1560.23 |
| 8K bytes | 812.25 | 412.74 | 1198.90 | 1412.18 | 1580.65 |
| 64K bytes | 817.35 | 412.24 | 1200.20 | 1415.11 | 1583.86 |
| 1M bytes | 815.70 | 411.93 | 1200.93 | 1415.65 | 1585.40 |
SSE4(E5-2650)
| Test case | naive | lookup | lemire | range | range2 |
|---|---|---|---|---|---|
| UTF-demo.txt | 753.70 | 310.41 | 3954.74 | 3945.60 | 3986.13 |
| 32 bytes | 1135.76 | 364.07 | 2890.52 | 2351.81 | 2173.02 |
| 33 bytes | 1161.85 | 376.29 | 1352.95 | 2239.55 | 2041.43 |
| 129 bytes | 1161.22 | 322.47 | 2742.49 | 3315.33 | 3249.35 |
| 1K bytes | 1310.95 | 310.72 | 3755.88 | 3781.23 | 3874.17 |
| 8K bytes | 1348.32 | 307.93 | 3860.71 | 3922.81 | 3968.93 |
| 64K bytes | 1301.34 | 308.39 | 3935.15 | 3973.50 | 3983.44 |
| 1M bytes | 1279.78 | 309.06 | 3923.51 | 3953.00 | 3960.49 |
Range algorithm analysis
Basic idea:
- Load 16 bytes
- Leverage SIMD to calculate value range for each byte efficiently
- Validate 16 bytes at once
UTF-8 coding format
http://www.unicode.org/versions/Unicode6.0.0/ch03.pdf, page 94
Table 3-7. Well-Formed UTF-8 Byte Sequences
| Code Points | First Byte | Second Byte | Third Byte | Fourth Byte |
|---|---|---|---|---|
| U+0000..U+007F | 00..7F | |||
| U+0080..U+07FF | C2..DF | 80..BF | ||
| U+0800..U+0FFF | E0 | A0..BF | 80..BF | |
| U+1000..U+CFFF | E1..EC | 80..BF | 80..BF | |
| U+D000..U+D7FF | ED | 80..9F | 80..BF | |
| U+E000..U+FFFF | EE..EF | 80..BF | 80..BF | |
| U+10000..U+3FFFF | F0 | 90..BF | 80..BF | 80..BF |
| U+40000..U+FFFFF | F1..F3 | 80..BF | 80..BF | 80..BF |
| U+100000..U+10FFFF | F4 | 80..8F | 80..BF | 80..BF |
To summarise UTF-8 encoding:
- Depending on First Byte, one legal character can be 1, 2, 3, 4 bytes
- For First Byte within C0..DF, character length = 2
- For First Byte within E0..EF, character length = 3
- For First Byte within F0..F4, character length = 4
- C0, C1, F5..FF are not allowed
- Second,Third,Fourth Bytes must lie in 80..BF.
- There are four special cases for Second Byte, shown bold italic in above table.
Range table
Range table maps range index 0 ~ 15 to minimal and maximum values allowed. Our task is to observe input string, find the pattern and set correct range index for each byte, then validate input string.
| Index | Min | Max | Byte type |
|---|---|---|---|
| 0 | 00 | 7F | First Byte, ASCII |
| 1,2,3 | 80 | BF | Second, Third, Fourth Bytes |
| 4 | A0 | BF | Second Byte after E0 |
| 5 | 80 | 9F | Second Byte after ED |
| 6 | 90 | BF | Second Byte after F0 |
| 7 | 80 | 8F | Second Byte after F4 |
| 8 | C2 | F4 | First Byte, non-ASCII |
| 9..15(NEON) | FF | 00 | Illegal: unsigned char >= 255 && unsigned char <= 0 |
| 9..15(SSE) | 7F | 80 | Illegal: signed char >= 127 && signed char <= -128 |
Calculate byte ranges (ignore special cases)
Ignoring the four special cases(E0,ED,F0,F4), how should we set range index for each byte?
- Set range index to 0(00..7F) for all bytes by default
- Find non-ASCII First Byte (C0..FF), set their range index to 8(C2..F4)
- For First Byte within C0..DF, set next byte's range index to 1(80..BF)
- For First Byte within E0..EF, set next two byte's range index to 2,1(80..BF) in sequence
- For First Byte within F0..FF, set next three byte's range index to 3,2,1(80..BF) in sequence
To implement above operations efficiently with SIMD:
- For 16 input bytes, use lookup table to map C0..DF to 1, E0..EF to 2, F0..FF to 3, others to 0. Save to first_len.
- Map C0..FF to 8, we get range indices for First Byte.
- Shift first_len one byte, we get range indices for Second Byte.
- Saturate subtract first_len by one(3->2, 2->1, 1->0, 0->0), then shift two bytes, we get range indices for Third Byte.
- Saturate subtract first_len by two(3->1, 2->0, 1->0, 0->0), then shift three bytes, we get range indices for Fourth Byte.
Example(assume no previous data)
| Input | F1 | 80 | 80 | 80 | 80 | C2 | 80 | 80 | ... |
|---|---|---|---|---|---|---|---|---|---|
| first_len | 3 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | ... |
| First Byte | 8 | 0 | 0 | 0 | 0 | 8 | 0 | 0 | ... |
| Second Byte | 0 | 3 | 0 | 0 | 0 | 0 | 1 | 0 | ... |
| Third Byte | 0 | 0 | 2 | 0 | 0 | 0 | 0 | 0 | ... |
| Fourth Byte | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | ... |
| Range index | 8 | 3 | 2 | 1 | 0 | 8 | 1 | 0 | ... |
Range_index = First_Byte | Second_Byte | Third_Byte | Fourth_ByteError handling
- C0,C1,F5..FF are not included in range table and will always be detected.
- Illegal 80..BF will have range index 0(00..7F) and be detected.
- Based on First Byte, according Second, Third and Fourth Bytes will have range index 1/2/3, to make sure they must lie in 80..BF.
- If non-ASCII First Byte overlaps, above algorithm will set range index of the latter First Byte to 9,10,11, which are illegal ranges. E.g, Input = F1 80 C2 90 --> Range index = 8 3 10 1, where 10 indicates error. See table below.
Overlapped non-ASCII First Byte
| Input | F1 | 80 | C2 | 90 |
|---|---|---|---|---|
| first_len | 3 | 0 | 1 | 0 |
| First Byte | 8 | 0 | 8 | 0 |
| Second Byte | 0 | 3 | 0 | 1 |
| Third Byte | 0 | 0 | 2 | 0 |
| Fourth Byte | 0 | 0 | 0 | 1 |
| Range index | 8 | 3 | 10 | 1 |
Adjust Second Byte range for special cases
Range index adjustment for four special cases
| First Byte | Second Byte | Before adjustment | Correct index | Adjustment |
|---|---|---|---|---|
| E0 | A0..BF | 2 | 4 | 2 |
| ED | 80..9F | 2 | 5 | 3 |
| F0 | 90..BF | 3 | 6 | 3 |
| F4 | 80..8F | 3 | 7 | 4 |
Range index adjustment can be reduced to below problem:
Given 16 bytes, replace E0 with 2, ED with 3, F0 with 3, F4 with 4, others with 0.
A naive SIMD approach:
- Compare 16 bytes with E0, get the mask for eacy byte (FF if equal, 00 otherwise)
- And the mask with 2 to get adjustment for E0
- Repeat step 1,2 for ED,F0,F4
At least eight operations are required for naive approach.
Observing special bytes(E0,ED,F0,F4) are close to each other, we can do much better using lookup table.
NEON
NEON tbl instruction is very convenient for table lookup:
- Table can be up to 16x4 bytes in size
- Return zero if index is out of range
Leverage these features, we can solve the problem with as few as two operations:
- Precreate a 16x2 lookup table, where table[0]=2, table[13]=3, table[16]=3, table[20]=4, table[others]=0.
- Subtract input bytes with E0 (E0 -> 0, ED -> 13, F0 -> 16, F4 -> 20).
- Use the subtracted byte as index of lookup table and get range adjustment directly.
- For indices less than 32, we get zero or required adjustment value per input byte
- For out of bound indices, we get zero per
tblbehaviour
SSE
SSE pshufb instruction is not as friendly as NEON tbl in this case:
- Table can only be 16 bytes in size
- Out of bound indices are handled this way:
- If 7-th bit of index is 0, least four bits are used as index (E.g, index 0x73 returns 3rd element)
- If 7-th bit of index is 1, return 0 (E.g, index 0x83 returns 0)
We can still leverage these features to solve the problem in five operations:
- Precreate two tables:
- table_df[1] = 2, table_df[14] = 3, table_df[others] = 0
- table_ef[1] = 3, table_ef[5] = 4, table_ef[others] = 0
- Subtract input bytes with EF (E0 -> 241, ED -> 254, F0 -> 1, F4 -> 5) to get the temporary indices
- Get range index for E0,ED
- Saturate subtract temporary indices with 240 (E0 -> 1, ED -> 14, all values below 240 becomes 0)
- Use subtracted indices to look up table_df, get the correct adjustment
- Get range index for F0,F4
- Saturate add temporary indices with 112(0x70) (F0 -> 0x71, F4 -> 0x75, all values above 16 will be larger than 128(7-th bit set))
- Use added indices to look up table_ef, get the correct adjustment (index 0x71,0x75 returns 1st,5th elements, per
pshufbbehaviour)
Error handling
- For overlapped non-ASCII First Byte, range index before adjustment is 9,10,11. After adjustment (adds 2,3,4 or 0), the range index will be 9 to 15, which is still illegal in range table. So the error will be detected.
Handling remaining bytes
For remaining input less than 16 bytes, we will fallback to naive byte by byte approach to validate them, which is actually faster than SIMD processing.
- Look back last 16 bytes buffer to find First Byte. At most three bytes need to look back. Otherwise we either happen to be at character boundray, or there are some errors we already detected.
- Validate string byte by byte starting from the First Byte.
Tests
It's necessary to design test cases to cover corner cases as more as possible.
Positive cases
- Prepare correct characters
- Validate correct characters
- Validate long strings
- Round concatenate characters starting from first character to 1024 bytes
- Validate 1024 bytes string
- Shift 1 byte, validate 1025 bytes string
- Shift 2 bytes, Validate 1026 bytes string
- ...
- Shift 16 bytes, validate 1040 bytes string
- Repeat step3, test buffer starting from second character
- Repeat step3, test buffer starting from third character
- ...
Negative cases
- Prepare bad characters and bad strings
- Bad character
- Bad character cross 16 bytes boundary
- Bad character cross last 16 bytes and remaining bytes boundary
- Test long strings
- Prepare correct long strings same as positive cases
- Append bad characters
- Shift one byte for each iteration
- Validate each shift
Code breakdown
Below table shows how 16 bytes input are processed step by step. See range-neon.c for according code.
Overview (`upb/util/README.md`)
upb util library
The libraries in this directory contain useful functionality that is layered
on top of the main upb APIs. In other words, the APIs in this directory have
no special access to upb internals; you could easily implement the same things
yourself.
Overview (`upb_generator/common/README.md`)
This directory contains APIs that are used by multiple code generators, but not
public to users.
Overview (`bazel/private/oss/README.md`)
Bazel-specific Build Rules
This directory contains rule implementations that are specific to Bazel (ie.
where Blaze and Bazel rules have diverged).
Overview (`docs/design/editions/README.md`)
Protocol Buffers - Protobuf Editions design documents
This directory contains historical design documents that describe plans for
implementing Protobuf Editions. For an up-to-date overview of this feature of
Protocol Buffers, see
Protobuf Editions Overview.
These files represent the state that the original documents were in at the time
that they were published to this repository. While some updates may be made to
the files after their initial upload, you should consider the possibility that
they are outdated as you read them. These are purely for historical value and
should not be treated as documentation of the current state.
Note that the authors listed in the topics were the authors of the original
version of the document; it may have changed since they last worked on the
document.
Design Documents
The following topics are in this repository:
- What are Protobuf Editions?
- Life of an Edition
- Edition Lifetimes
- Protobuf Editions Design: Features
- Editions: Life of a Featureset
- Edition Zero Features
- Minimum Required Edition
- Protobuf Editions for Schema Producers
- Stricter Schemas with Editions
- Edition Zero: JSON Handling
- Edition Zero: Converged Semantics
- Edition Zero Feature: Enum Field Closedness
- C++ APIs for Edition Zero
- Java Lite For Editions
- Edition Evolution
- Edition Naming
- Editions Feature Visibility
- Legacy Syntax Editions
- Editions: Feature Extension Layout
- Editions: Group Migration Issues
Overview (`docs/design/prototiller/README.md`)
Protocol Buffers - Prototiller design documents
This directory contains historical design documents that describe plans for
implementing Prototiller, a tool for updating proto files to be conformant with Protobuf Editions functionality. For design documents related to Protobuf Editions, see the README.
These files represent the state that the original documents were in at the time
that they were published to this repository. While some updates may be made to
the files after their initial upload, you should consider the possibility that
they are outdated as you read them. These are purely for historical value and
should not be treated as documentation of the current state.
Note that the authors listed in the topics were the authors of the original
version of the document; it may have changed since they last worked on the
document.
Design Documents
The following topics are in this repository:
Overview (`rust/release_crates/google_protobuf/README.md`)
The runtime of the official Google Rust Protobuf implementation.
This is currently a beta release: the API is subject to change, and there may be
some rough edges, including missing documentation and features.
An example for how to use this crate can be found in the
google-protobuf-example crate
Ownership and implementation change
This crate is officially supported by the Protobuf team at Google.
This is a completely new implementation with a different API, as well as a
fundamentally different approach than prior versions of the protobuf crate. It
focuses on delivering a high-quality Rust API which is backed by either a pure C
implementation (upb) or the Protobuf C++ implementation. This choice was made
for performance, feature parity, development velocity, and security reasons.
It is not planned for the pure Rust lineage to be actively developed going
forward. While it is not expected to receive significant further development, as
a stable and high quality pure Rust implementation, many open source projects
may reasonably continue to stay on the protobuf crate.
Development was previously done under the protobuf crate by a community
project by stepancheg who generously donated
the crate name to Google. V4.36.0 of the protobuf crate corresponds to V0.36.0
of this crate.
How to get a compatible version of protoc
The protoc binary that you use to generate code needs to have a version that
exactly matches the version of the protobuf crate you are using. More
specifically, if you are using Rust protobuf x.y.z then you need to use protocy.z. See here for more
details on our versioning scheme.
The easiest way to get ahold of protoc is to download a prebuilt binary from the
matching release here.
Just make sure protoc is on your $PATH when you run cargo.
Overview (`rust/release_crates/google_protobuf_codegen/README.md`)
The build time dependency crate for generating code for the official Google Rust
Protobuf implementation.
The code generated by this is expected to be used in conjunction with the
official Google Protobuf crate.
This is currently a beta release: the API is subject to change,
and there may be some rough edges, including missing documentation and features.
How to get a compatible version of protoc
The protoc binary that you use to generate code needs to have a version that
exactly matches the version of the protobuf crate you are using. More
specifically, if you are using Rust protobuf x.y.z then you need to use protocy.z. See here for more
details on our versioning scheme.
The easiest way to get ahold of protoc is to download a prebuilt binary from the
matching release here.
Just make sure protoc is on your $PATH when you run cargo.
Overview (`rust/release_crates/google_protobuf_example/README.md`)
An example that demonstrates how to use the google-protobuf andgoogle-protobuf-codegen crates together.
The source of the latest version of this example can be read
here.
How to get a compatible version of protoc
The protoc binary that you use to generate code needs to have a version that
exactly matches the version of the protobuf crate you are using. More
specifically, if you are using Rust protobuf x.y.z then you need to use protocy.z. See here for more
details on our versioning scheme.
The easiest way to get ahold of protoc is to download a prebuilt binary from the
matching release here.
Just make sure protoc is on your $PATH when you run cargo.
Overview (`rust/release_crates/google_protobuf_macros/README.md`)
Internal crate. Use google-protobuf
instead.
Overview (`rust/release_crates/google_protobuf_well_known_types/README.md`)
This crate contains the protobuf
well-known types for
the official Google Protobuf crate.
Overview (`rust/release_crates/protobuf/README.md`)
The runtime of the official Google Rust Protobuf implementation.
This is currently a beta release: the API is subject to change, and there may be
some rough edges, including missing documentation and features.
An example for how to use this crate can be found in the
protobuf_example crate
This crate is superseded by
google-protobuf. New development
happens there; please migrate there to get the latest releases.
V4 ownership and implementation change
V4 of this crate is officially supported by the Protobuf team at Google. Prior
major versions were developed by as a community project by
stepancheg who generously donated the crate
name to Google.
V4 is a completely new implementation with a different API, as well as a
fundamentally different approach than prior versions of this crate. It focuses
on delivering a high-quality Rust API which is backed by either a pure C
implementation (upb) or the Protobuf C++ implementation. This choice was made
for performance, feature parity, development velocity, and security reasons.
More discussion about the rationale and design philosophy can be found at
https://protobuf.dev/reference/rust/.
It is not planned for the V3 pure Rust lineage to be actively developed going
forward. While it is not expected to receive significant further development, as
a stable and high quality pure Rust implementation, many open source projects
may reasonably continue to stay on the V3 API.
Overview (`rust/release_crates/protobuf_codegen/README.md`)
The build time dependency crate for generating code for the official Google Rust
Protobuf implementation.
This crate is superseded by
google-protobuf-codegen.
New development happens there; please migrate there to get the latest
releases.
The code generated by this is expected to be used in conjunction with the
official Protobuf crate.
This is currently a beta release: the API is subject to change,
and there may be some rough edges, including missing documentation and features.
How to get a compatible version of protoc
The protoc binary that you use to generate code needs to have a version that
exactly matches the version of the protobuf crate you are using. More
specifically, if you are using Rust protobuf x.y.z then you need to use protocy.z. See here for more
details on our versioning scheme.
The easiest way to get ahold of protoc is to download a prebuilt binary from the
matching release here.
Just make sure protoc is on your $PATH when you run cargo.
Overview (`rust/release_crates/protobuf_example/README.md`)
An example that demonstrates how to use the protobuf and protobuf_codegen
crates together.
The source of the latest version of this example can be read
here.
How to get a compatible version of protoc
The protoc binary that you use to generate code needs to have a version that
exactly matches the version of the protobuf crate you are using. More
specifically, if you are using Rust protobuf x.y.z then you need to use protocy.z. See here for more
details on our versioning scheme.
The easiest way to get ahold of protoc is to download a prebuilt binary from the
matching release here.
Just make sure protoc is on your $PATH when you run cargo.
Overview (`rust/release_crates/protobuf_macros/README.md`)
Internal crate. Use protobuf instead.
This crate is superseded by
google-protobuf-macros,
which it re-exports. New development happens there.
Overview (`rust/release_crates/protobuf_well_known_types/README.md`)
This crate contains the protobuf
well-known types.
This crate is superseded by
google-protobuf-well-known-types,
which it re-exports. New development happens there; please migrate there to
get the latest releases.
Overview (`src/google/protobuf/compiler/kotlin/README.md`)
Kotlin DSL Generator
This code generator implements the Kotlin DSL. The Kotlin DSL sits on top of
another proto implementation (written in Java or Kotlin) and adds convenient
support for building proto messages using DSL syntax, as documented in
Kotlin Generated Code Guide.
This code generator is invoked by passing --kotlin_out to protoc.
When using Kotlin on the JVM, you will also need to pass --java_out to
generate the Java code that implements the generated classes themselves.
When using Kotlin on other platforms (eg. Kotlin Native), there is currently no
support for generating message classes, so it is not possible to use the Kotlin
DSL at this time.
Overview (`src/google/protobuf/util/python/README.md`)
Python wrappers protobuf/util
The field_mask_util wrappers are maintained by the
Risk AI Team for their own
benefit. Other teams may add themselves to the visibility rule, but have to sign
up as OWNERs in return.
Cmake Protobuf Generate (`docs/cmake_protobuf_generate.md`)
How to use `protobuf_generate`
This document explains how to use the function protobuf_generate which is
provided by protobuf's CMake module.
Usage
In the same directory that called find_package(protobuf CONFIG) and any of its
subdirectories, the CMake function protobuf_generate is made available byprotobuf-generate.cmake. It can be used to
automatically generate source files from .proto schema files at build time.
Basic example
Let us see how protobuf_generate can be used to generate and compile the
source files of a proto schema whenever an object target called proto-objects
is built.
Given the following directory structure:
proto/helloworld/helloworld.protoCMakeLists.txt
where helloworld.proto is a protobuf schema file and CMakeLists.txt
contains:
find_package(protobuf CONFIG REQUIRED)
add_library(proto-objects OBJECT "${CMAKE_CURRENT_LIST_DIR}/proto/helloworld/helloworld.proto")
target_link_libraries(proto-objects PUBLIC protobuf::libprotobuf)
set(PROTO_BINARY_DIR "${CMAKE_CURRENT_BINARY_DIR}/generated")
target_include_directories(proto-objects PUBLIC "$<BUILD_INTERFACE:${PROTO_BINARY_DIR}>")
protobuf_generate(
TARGET proto-objects
IMPORT_DIRS "${CMAKE_CURRENT_LIST_DIR}/proto"
PROTOC_OUT_DIR "${PROTO_BINARY_DIR}")Building the target proto-objects will generate the files:
${CMAKE_CURRENT_BINARY_DIR}/generated/helloworld/helloworld.pb.h${CMAKE_CURRENT_BINARY_DIR}/generated/helloworld/helloworld.pb.cc
and (depending on the build system) output:
[build] [1/2] Running cpp protocol buffer compiler on /proto/helloworld/helloworld.proto
[build] [2/2] Building CXX object /build/generated/helloworld/helloworld.pb.cc.ogRPC example
protobuf_generate can also be customized to invoke plugins like gRPC'sgrpc_cpp_plugin. Given the same directory structure as in the
basic example and let CMakeLists.txt contain:
find_package(gRPC CONFIG REQUIRED)
add_library(proto-objects OBJECT "${CMAKE_CURRENT_LIST_DIR}/proto/helloworld/helloworld.proto")
target_link_libraries(proto-objects PUBLIC protobuf::libprotobuf gRPC::grpc++)
set(PROTO_BINARY_DIR "${CMAKE_CURRENT_BINARY_DIR}/generated")
set(PROTO_IMPORT_DIRS "${CMAKE_CURRENT_LIST_DIR}/proto")
target_include_directories(proto-objects PUBLIC "$<BUILD_INTERFACE:${PROTO_BINARY_DIR}>")
protobuf_generate(
TARGET proto-objects
IMPORT_DIRS ${PROTO_IMPORT_DIRS}
PROTOC_OUT_DIR "${PROTO_BINARY_DIR}")
protobuf_generate(
TARGET proto-objects
LANGUAGE grpc
GENERATE_EXTENSIONS .grpc.pb.h .grpc.pb.cc
PLUGIN "protoc-gen-grpc=\$<TARGET_FILE:gRPC::grpc_cpp_plugin>"
IMPORT_DIRS ${PROTO_IMPORT_DIRS}
PROTOC_OUT_DIR "${PROTO_BINARY_DIR}")Then building proto-objects will generate and compile:
${CMAKE_CURRENT_BINARY_DIR}/generated/helloworld/helloworld.pb.h${CMAKE_CURRENT_BINARY_DIR}/generated/helloworld/helloworld.pb.cc${CMAKE_CURRENT_BINARY_DIR}/generated/helloworld/helloworld.grpc.pb.h${CMAKE_CURRENT_BINARY_DIR}/generated/helloworld/helloworld.grpc.pb.cc
And protoc will automatically be re-run whenever the schema files change andproto-objects is built.
Note on unity builds
Since protobuf's generated source files are unsuited for
jumbo/unity builds
it is recommended to exclude them from such builds which can be achieved by
adjusting their properties:
protobuf_generate(
OUT_VAR PROTO_GENERATED_FILES
...)
set_source_files_properties(${PROTO_GENERATED_FILES} PROPERTIES SKIP_UNITY_BUILD_INCLUSION on)How it works
For each source file ending in proto of the argument provided to TARGET or
each file provided through PROTOS, protobuf_generate will set up a
add_custom_command
which depends on protobuf::protoc and the proto files. It declares the
generated source files as OUTPUT which means that any target that depends on
them will automatically cause the custom command to execute when it is brought
up to date. The command itself is made up of the arguments for protoc, like
the output directory, the schema files, the language to generate for, the
plugins to use, etc.
Reference
Arguments accepted by protobuf_generate.
Flag arguments:
APPEND_PATH— A flag that causes the base path of all proto schema files
to be added toIMPORT_DIRS.
Single-value arguments:
LANGUAGE— A single value: cpp or python. Determines what kind of source
files are being generated.OUT_VAR— Name of a CMake variable that will be filled with the paths to
the generated source files.EXPORT_MACRO— Name of a macro that is applied to all generated Protobuf
message classes and extern variables. It can, for example, be used to
declare DLL exports.PROTOC_EXE— Command name, path, or CMake executable used to run protoc
commands. Defaults toprotobuf::protoc.PROTOC_OUT_DIR— Output directory of generated source files. Defaults toCMAKE_CURRENT_BINARY_DIR.PLUGIN— An optional plugin executable. This could, for example, be the
path togrpc_cpp_plugin.PLUGIN_OPTIONS— Additional options provided to the plugin, such asgenerate_mock_code=truefor the gRPC cpp plugin.DEPENDENCIES— Arguments forwarded to theDEPENDSof the underlyingadd_custom_commandinvocation.TARGET— CMake target that will have the generated files added as sources.
Multi-value arguments:
PROTOS— List of proto schema files. If omitted, then every source file
ending in proto ofTARGETwill be used.IMPORT_DIRS— A common parent directory for the schema files. For example,
if the schema file isproto/helloworld/helloworld.protoand the import
directoryproto/then the generated files are${PROTOC_OUT_DIR}/helloworld/helloworld.pb.hand${PROTOC_OUT_DIR}/helloworld/helloworld.pb.cc.GENERATE_EXTENSIONS— If LANGUAGE is omitted then this must be set to the
extensions that protoc generates.PROTOC_OPTIONS— Additional arguments that are forwarded to protoc. Don't
use it for flags thatprotobuf_generatesets from its own arguments:
import paths (-I/--proto_path; useIMPORT_DIRSorAPPEND_PATH), the
output directory and generator options (--<lang>_out; usePROTOC_OUT_DIRandPLUGIN_OPTIONS) or plugins (--plugin; usePLUGIN). The generated file names are worked out from those arguments,
so flags passed here aren't taken into account.
Cpp Build Systems (`docs/cpp_build_systems.md`)
How Protobuf supports multiple C++ build systems
This document explains how the Protobuf project supports multiple C++ build
systems.
Background
Protobuf primarily uses Bazel to build the Protobuf C++
runtime and Protobuf compiler[^historical_sot]. However, there are several
different build systems in common use for C++, each one of which requires
essentially a complete copy of the same build definitions.
[^historical_sot]: On a historical note, prior to its
release as Open Source Software,
the Protobuf project was developed using Google's internal
build system, which was the predecessor to Bazel (the vast
majority of Google's contributions continue to be developed
this way). The Open Source Protobuf project, however,
historically used Autoconf to build the C++ implementation.
Over time, other build systems (including Bazel) have been
added, thanks in large part to substantial contributions from
the Open Source community. Since the Protobuf project deals
with multiple languages (all of which ultimately rely upon
C++, for the Protobuf compiler), Bazel is a natural choice
for a project-wide build system -- in fact, Bazel (and its
predecessor, Blaze) was designed in large part to support
exactly this type of rich, multi-language build.
Currently, C++ Protobuf can be built with Bazel and CMake. Each of these build
systems has different semantics and structure, but share in common the list of
files needed to build the runtime and compiler.
Design
Extracting information from Bazel
Bazel's Starlark API provides aspects to
traverse the build graph, inspect build rules, define additional actions, and
expose information through
providers. For example, thecc_proto_library rule uses an aspect to traverse the dependency graph ofproto_library rules, and dynamically attaches actions to generate C++ code
using the Protobuf compiler and compile using the C++ compiler.
In order to support multiple build systems, the overall build structure is
defined once for each system, and expose frequently-changing metadata from Bazel
in a way that can be included from the build definition. Primarily, this means
exposing the list of source files in a way that can be included in other build
definitions.
Two aspects are used to extract this information from the Bazel build
definitions:
cc_file_list_aspectextractssrcs,hdrs, andtextual_hdrsfrom build
rules likecc_library. The sources are exposed through a provider namedCcFileList.proto_file_list_aspectextracts thesrcsfrom aproto_library, and
also generates the expected filenames that would be generated by the
Protobuf compiler. This information is exposed through a provider namedProtoFileList.
On their own, these aspects have limited utility. However, they can be
instantiated by custom rules, so that an ordinary BUILD.bazel target can
produce outputs based on the information gleaned from these aspects.
(Aside) Distribution libraries
Bazel's native cc_library rule is typically used on a "fine-grained" level, so
that, for example, lightweight unit tests can be written with narrow scope.
Although Bazel does build library artifacts (such as .so and .a files on
Linux), they correspond to cc_library rules.
Since the entire "Protobuf library" includes many constituent cc_library
rules, a special rule, cc_dist_library, combines several fine-grained
libraries into a single, monolithic library.
For the Protobuf project, these "distribution libraries" are intended to match
the granularity of the CMake-based builds. Since the Bazel-built distribution
library covers the rules with the source files needed by other builds, thecc_dist_library rule invokes the cc_file_list_aspect on its input libraries.
The result is that a cc_dist_library rule not only produces composite library
artifacts, but also collect and provide the list of sources that were inputs.
For example:
$ cat cc_dist_library_example/BUILD.bazel
load("@rules_cc//cc:defs.bzl", "cc_library")
load("//pkg:cc_dist_library.bzl", "cc_dist_library")
cc_library(
name = "a",
srcs = ["a.cc"],
)
cc_library(
name = "b",
srcs = ["b.cc"],
deps = [":c"],
)
# N.B.: not part of the cc_dist_library, even though it is in the deps of 'b':
cc_library(
name = "c",
srcs = ["c.cc"],
)
cc_dist_library(
name = "lib",
deps = [
":a",
":b",
],
visibility = ["//visibility:public"],
)
# Note: the output below has been formatted for clarity:
$ bazel cquery //cc_dist_library_example:lib \
--output=starlark \
--starlark:expr='providers(target)["//pkg:cc_dist_library.bzl%CcFileList"]'
struct(
hdrs = depset([]),
internal_hdrs = depset([]),
srcs = depset([
<source file cc_dist_library_example/a.cc>,
<source file cc_dist_library_example/b.cc>,
]),
textual_hdrs = depset([]),
)The upshot is that the "coarse-grained" library can be defined by the Bazel
build, and then export the list of source files that are needed to reproduce the
library in a different build system.
One major difference from most Bazel rule types is that the file list aspects do
not propagate. In other words, they only expose the immediate dependency's
sources, not transitive sources. This is for two reasons:
- Immediate dependencies are conceptually simple, while transitivity requires
substantially more thought. For example, if transitive dependencies were
considered, then some way would be needed to exclude dependencies that
should not be part of the final library (for example, a distribution library
for//:protobufcould be defined not to include all of//:protobuf_lite). While dependency elision is an interesting design
problem, the protobuf library is small enough that directly listing
dependencies should not be problematic. - Dealing only with immediate dependencies gives finer-grained control over
what goes into the composite library. For example, a Starlarkselect()
could conditionally add fine-grained libraries to some builds, but not
others.
Another subtlety for tests is due to Bazel internals. Internally, a slightly
different configuration is used when evaluating cc_test rules as compared tocc_dist_library. If cc_test targets are included in a cc_dist_library
rule, and both are evaluated by Bazel, this can result in a build-time error:
the config used for the test contains additional options that tell Bazel how to
execute the test that the cc_file_list_aspect build config does not. Bazel
detects this as two conflicting actions generating the same outputs. (Forcc_test rules, the simplest workaround is to provide sources through afilegroup or similar.)
File list generation
Lists of input files are generated by Bazel in a format that can be imported to
other build systems. Currently only CMake-style files can be generated.
The lists of files are derived from Bazel build targets. The sources can be: *cc_dist_library rules (as described above) * proto_library rules *
individual files * filegroup rules * pkg_files or pkg_filegroup rules from
https://github.com/bazelbuild/rules_pkg
For example:
/* Detailed source-code truncated for AI context efficiency. */A hand-written CMake build rule could then use the generated file to define
libraries, such as:
include(source_lists.cmake)
add_library(distlib ${distlib_srcs} ${buff_srcs})Protobuf usage
The main C++ runtimes (lite and full) and the Protobuf compiler use their
corresponding cc_dist_library rules to generate file lists. Forproto_library targets, the file list generation can extract the source files
directly. For other targets, notably cc_test targets, the file list generators
use filegroup rules.
In general, adding new targets to a non-Bazel build system in Protobuf (or
adding a new build system altogether) requires some one-time setup:
- The overall structure of the new build system has to be defined. It should
import lists of files and refer to them by variable, instead of listing
files directly. - (Only if the build system is new) A new rule type has to be added to
//pkg:build_systems.bzl. Most of the implementation is shared, but a
"fragment generator" is need to declare a file list variable, and the rule
type itself has to be defined and call the shared implementation.
When files are added or deleted, or when the Protobuf Bazel structure is
changed, these changes may need to be reflected in the file list logic. These
are some example scenarios:
- Files are added to (or removed from) the
srcsof an existingcc_library:
no changes needed. If thecc_libraryis already part of acc_dist_library, then regenerating the source lists will reflect the
change. - A
cc_libraryis added: the new target may need to be added to the Protobufcc_dist_librarytargets, as appropriate. - A
cc_libraryis deleted: if acc_dist_librarydepends upon the deleted
target, then a build-time error will result. The library needs to be removed
from thecc_dist_library. - A
cc_testis added or deleted: test sources are handled byfilegroup
rules defined in the same package as thecc_testrule. Thefilegroups
are usually given a name like"test_srcs", and often useglob()to find
sources. This means that adding or removing a test may not require any extra
work, but this can be verified within the same package as the test rule. - Test-only proto files are added: the
proto_librarymight need to be added
to the file list map in//pkg:BUILD.bazel, and then the file added to
various build systems. However, most test-only protos are already exposed
through libraries like//src/google/protobuf:test_protos.
If there are changes, then the regenerated file lists need to be copied back
into the repo. That way, the corresponding build systems can be used with a git
checkout, without needing to run Bazel first.
(Aside) Distribution archives
A very similar set of rules is defined in //pkg to build source distribution
archives for releases. In addition to the full sources, Protobuf releases also
include source archives sliced by language, so that, for example, a Ruby-based
project can get just the sources needed to build the Ruby runtime. (The
per-language slices also include sources needed to build the protobuf compiler,
so they all effectively include the C++ runtime.)
These archives are defined using rules from the
rules_pkg project. Although they are
similar to cc_dist_library and the file list generation rules, the goals are
different: the build system file lists described above only apply to C++, and
are organized according to what should or should not be included in different
parts of the build (e.g., no tests are included in the main library). On the
other hand, the distribution archives deal with languages other than C++, and
contain all the files that need to be distributed as part of a release (even for
C++, this is more than just the C++ sources).
While it might be possible to use information from the CcFileList andProtoFileList providers to define the distribution files, additional files
(such as the various BUILD.bazel files) are also needed in the distribution
archive. The lists of distribution files can usually be generated by glob(),
anyhow, so sharing logic with the file list aspects may not be beneficial.
Currently, all of the file lists are checked in. However, it would be possible
to build the file lists on-the-fly and include them in the distribution
archives, rather than checking them in.
Field Presence (`docs/field_presence.md`)
Application note: Field presence
This application note explains the various presence tracking disciplines for
protobuf fields. It also explains the behaviour of explicit presence tracking
for singular proto3 fields with basic types.
Background
Field presence is the notion of whether a protobuf field has a value. There
are two different manifestations of presence for protobufs: no presence, where
the generated message API stores field values (only), and explicit presence,
where the API also stores whether or not a field has been set.
Historically, proto2 has mostly followed explicit presence, while proto3
exposes only no presence semantics. Singular proto3 fields of basic types
(numeric, string, bytes, and enums) which are defined with the optional label
have explicit presence, like proto2 (this feature is enabled by default as
release 3.15).
Presence disciplines
Presence disciplines define the semantics for translating between the API
representation and the serialized representation. The no presence
discipline relies upon the field value itself to make decisions at
(de)serialization time, while the explicit presence discipline relies upon the
explicit tracking state instead.
Presence in *tag-value stream* (wire format) serialization
The wire format is a stream of tagged, self-delimiting values. By definition,
the wire format represents a sequence of present values. In other words, every
value found within a serialization represents a present field; furthermore,
the serialization contains no information about not-present values.
The generated API for a proto message includes (de)serialization definitions
which translate between API types and a stream of definitionally present (tag,
value) pairs. This translation is designed to be forward- and
backward-compatible across changes to the message definition; however, this
compatibility introduces some (perhaps surprising) considerations when
deserializing wire-formatted messages:
- When serializing, fields with no presence are not serialized if they
contain their default value.- For numeric types, the default is 0.
- For enums, the default is the zero-valued enumerator.
- For strings, bytes, and repeated fields, the default is the zero-length
value. - For messages, the default is the language-specific null value.
- "Empty" length-delimited values (such as empty strings) can be validly
represented in serialized values: the field is "present," in the sense that
it appears in the wire format. However, if the generated API does not track
presence, then these values may not be re-serialized; i.e., the empty field
may be "not present" after a serialization round-trip. - When deserializing, duplicate field values may be handled in different ways
depending on the field definition.- Duplicate
repeatedfields are typically appended to the field's API
representation. (Note that serializing a packed repeated field
produces only one, length-delimited value in the tag stream.) - Duplicate
optionalfield values follow the rule that "the last one
wins."
- Duplicate
oneoffields expose the API-level invariant that only one field is set at
a time. However, the wire format may include multiple (tag, value) pairs
which notionally belong to theoneof. Similar tooptionalfields, the
generated API follows the "last one wins" rule.- Out-of-range values are not returned for enum fields in generated proto2
APIs. However, out-of-range values may be stored as unknown fields in the
API, even though the wire-format tag was recognized.
Presence in *named-field mapping* formats
Protobufs can be represented in human-readable, textual forms. Two notable
formats are TextFormat (the output format produced by generated messageDebugString methods) and JSON.
These formats have correctness requirements of their own, and are generally
stricter than tagged-value stream formats. However, TextFormat more closely
mimics the semantics of the wire format, and does, in certain cases, provide
similar semantics (for example, appending repeated name-value mappings to a
repeated field). In particular, similar to the wire format, TextFormat only
includes fields which are present.
JSON is a much stricter format, however, and cannot validly represent some
semantics of the wire format or TextFormat.
- Notably, JSON elements are semantically unordered, and each member must
have a unique name. This is different from TextFormat rules for repeated
fields. - JSON may include fields that are "not present," unlike the no presence
discipline for other formats:- JSON defines a
nullvalue, which may be used to represent a defined
but not-present field. - Repeated field values may be included in the formatted output, even if
they are equal to the default (an empty list).
- JSON defines a
- Because JSON elements are unordered, there is no way to unambiguously
interpret the "last one wins" rule.- In most cases, this is fine: JSON elements must have unique names:
repeated field values are not valid JSON, so they do not need to be
resolved as they are for TextFormat. - However, this means that it may not be possible to interpret
oneof
fields unambiguously: if multiple cases are present, they are unordered.
- In most cases, this is fine: JSON elements must have unique names:
In theory, JSON can represent presence in a semantic-preserving fashion. In
practice, however, presence correctness can vary depending upon implementation
choices, especially if JSON was chosen as a means to interoperate with clients
not using protobufs.
Presence in proto2 APIs
This table outlines whether presence is tracked for fields in proto2 APIs (both
for generated APIs and using dynamic reflection):
| Field type | Explicit Presence |
|---|---|
| Singular numeric (integer or floating point) | ✔️ |
| Singular enum | ✔️ |
| Singular string or bytes | ✔️ |
| Singular message | ✔️ |
| Repeated | |
| Oneofs | ✔️ |
| Maps |
Singular fields (of all types) track presence explicitly in the generated API.
The generated message interface includes methods to query presence of fields.
For example, the field foo has a corresponding has_foo method. (The specific
name follows the same language-specific naming convention as the field
accessors.) These methods are sometimes referred to as "hazzers" within the
protobuf implementation.
Similar to singular fields, oneof fields explicitly track which one of the
members, if any, contains a value. For example, consider this example oneof:
oneof foo {
int32 a = 1;
float b = 2;
}Depending on the target language, the generated API would generally include
several methods:
- A hazzer for the oneof:
has_foo - A oneof case method:
foo - Hazzers for the members:
has_a,has_b - Getters for the members:
a,b
Repeated fields and maps do not track presence: there is no distinction between
an empty and a not-present repeated field.
Presence in proto3 APIs
This table outlines whether presence is tracked for fields in proto3 APIs (both
for generated APIs and using dynamic reflection):
| Field type | optional |
Explicit Presence |
|---|---|---|
| Singular numeric (integer or floating point) | No | |
| Singular enum | No | |
| Singular string or bytes | No | |
| Singular numeric (integer or floating point) | Yes | ✔️ |
| Singular enum | Yes | ✔️ |
| Singular string or bytes | Yes | ✔️ |
| Singular message | Yes | ✔️ |
| Singular message | No | ✔️ |
| Repeated | N/A | |
| Oneofs | N/A | ✔️ |
| Maps | N/A |
Similar to proto2 APIs, proto3 does not track presence explicitly for repeated
fields. Without the optional label, proto3 APIs do not track presence for
basic types (numeric, string, bytes, and enums), either. Oneof fields
affirmatively expose presence, although the same set of hazzer methods may not
generated as in proto2 APIs.
Under the no presence discipline, the default value is synonymous with "not
present" for purposes of serialization. To notionally "clear" a field (so it
won't be serialized), an API user would set it to the default value.
The default value for enum-typed fields under no presence is the corresponding
0-valued enumerator. Under proto3 syntax rules, all enum types are required to
have an enumerator value which maps to 0. By convention, this is an UNKNOWN or
similarly-named enumerator. If the zero value is notionally outside the domain
of valid values for the application, this behavior can be thought of as
tantamount to explicit presence.
Semantic differences
The no presence serialization discipline results in visible differences from
the explicit presence tracking discipline, when the default value is set. For
a singular field with numeric, enum, or string type:
- No presence discipline:
- Default values are not serialized.
- Default values are not merged-from.
- To "clear" a field, it is set to its default value.
- The default value may mean:
- the field was explicitly set to its default value, which is valid in
the application-specific domain of values; - the field was notionally "cleared" by setting its default; or
- the field was never set.
- the field was explicitly set to its default value, which is valid in
- Explicit presence discipline:
- Explicitly set values are always serialized, including default values.
- Un-set fields are never merged-from.
- Explicitly set fields -- including default values -- are merged-from.
- A generated
has_foomethod indicates whether or not the fieldfoo
has been set (and not cleared). - A generated
clear_foomethod must be used to clear (i.e., un-set) the
value.
Considerations for merging
Under the no presence rules, it is effectively impossible for a target field
to merge-from its default value (using the protobuf's API merging functions).
This is because default values are skipped, similar to the no presence
serialization discipline. Merging only updates the target (merged-to) message
using the non-skipped values from the update (merged-from) message.
The difference in merging behavior has further implications for protocols which
rely on partial "patch" updates. If field presence is not tracked, then an
update patch alone cannot represent an update to the default value, because only
non-default values are merged-from.
Updating to set a default value in this case requires some external mechanism,
such as FieldMask. However, if presence is tracked, then all explicitly-set
values -- even default values -- will be merged into the target.
Considerations for change-compatibility
Changing a field between explicit presence and no presence is a
binary-compatible change for serialized values in wire format. However, the
serialized representation of the message may differ, depending on which version
of the message definition was used for serialization. Specifically, when a
"sender" explicitly sets a field to its default value:
- The serialized value following no presence discipline does not contain the
default value, even though it was explicitly set. - The serialized value following explicit presence discipline contains every
"present" field, even if it contains the default value.
This change may or may not be safe, depending on the application's semantics.
For example, consider two clients with different versions of a message
definition.
Client A uses this definition of the message, which follows the explicit
presence serialization discipline for field foo:
syntax = "proto3";
message Msg {
optional int32 foo = 1;
}Client B uses a definition of the same message, except that it follows the no
presence discipline:
syntax = "proto3";
message Msg {
int32 foo = 1;
}Now, consider a scenario where client A observes foo's presence as the clients
repeatedly exchange the "same" message by deserializing and reserializing:
// Client A:
Msg m_a;
m_a.set_foo(1); // non-default value
assert(m_a.has_foo()); // OK
Send(m_a.SerializeAsString()); // to client B
// Client B:
Msg m_b;
m_b.ParseFromString(Receive()); // from client A
assert(m_b.foo() == 1); // OK
Send(m_b.SerializeAsString()); // to client A
// Client A:
m_a.ParseFromString(Receive()); // from client B
assert(m_a.foo() == 1); // OK
assert(m_a.has_foo()); // OK
m_a.set_foo(0); // default value
Send(m_a.SerializeAsString()); // to client B
// Client B:
Msg m_b;
m_b.ParseFromString(Receive()); // from client A
assert(m_b.foo() == 0); // OK
Send(m_b.SerializeAsString()); // to client A
// Client A:
m_a.ParseFromString(Receive()); // from client B
assert(m_a.foo() == 0); // OK
assert(m_a.has_foo()); // FAILIf client A depends on explicit presence for foo, then a "round trip"
through client B will be lossy from the perspective of client A. In the example,
this is not a safe change: client A requires (by assert) that the field is
present; even without any modifications through the API, that requirement fails
in a value- and peer-dependent case.
How to enable *explicit presence* in proto3
These are the general steps to use field tracking support for proto3:
- Add an
optionalfield to a.protofile. - Run
protoc(at least v3.15, or v3.12 using--experimental_allow_proto3_optionalflag). - Use the generated "hazzer" methods and "clear" methods in application code,
instead of comparing or setting default values.
`.proto` file changes
This is an example of a proto3 message with fields which follow both no
presence and explicit presence semantics:
syntax = "proto3";
package example;
message MyMessage {
// No presence:
int32 not_tracked = 1;
// Explicit presence:
optional int32 tracked = 2;
}`protoc` invocation
Presence tracking for proto3 messages is enabled by default
since v3.15.0
release, formerly up until
v3.12.0 the--experimental_allow_proto3_optional flag was required when using presence
tracking with protoc.
Using the generated code
The generated code for proto3 fields with explicit presence (the optional
label) will be the same as it would be in a proto2 file.
This is the definition used in the "no presence" examples below:
syntax = "proto3";
package example;
message Msg {
int32 foo = 1;
}This is the definition used in the "explicit presence" examples below:
syntax = "proto3";
package example;
message Msg {
optional int32 foo = 1;
}In the examples, a function GetProto constructs and returns a message of typeMsg with unspecified contents.
C++ example
No presence:
Msg m = GetProto();
if (m.foo() != 0) {
// "Clear" the field:
m.set_foo(0);
} else {
// Default value: field may not have been present.
m.set_foo(1);
}Explicit presence:
Msg m = GetProto();
if (m.has_foo()) {
// Clear the field:
m.clear_foo();
} else {
// Field is not present, so set it.
m.set_foo(1);
}C# example
No presence:
var m = GetProto();
if (m.Foo != 0) {
// "Clear" the field:
m.Foo = 0;
} else {
// Default value: field may not have been present.
m.Foo = 1;
}Explicit presence:
var m = GetProto();
if (m.HasFoo) {
// Clear the field:
m.ClearFoo();
} else {
// Field is not present, so set it.
m.Foo = 1;
}Go example
No presence:
m := GetProto()
if m.Foo != 0 {
// "Clear" the field:
m.Foo = 0
} else {
// Default value: field may not have been present.
m.Foo = 1
}Explicit presence:
m := GetProto()
if m.Foo != nil {
// Clear the field:
m.Foo = nil
} else {
// Field is not present, so set it.
m.Foo = proto.Int32(1)
}Java example
These examples use a Builder to demonstrate clearing. Simply checking presence
and getting values from a Builder follows the same API as the message type.
No presence:
Msg.Builder m = GetProto().toBuilder();
if (m.getFoo() != 0) {
// "Clear" the field:
m.setFoo(0);
} else {
// Default value: field may not have been present.
m.setFoo(1);
}Explicit presence:
Msg.Builder m = GetProto().toBuilder();
if (m.hasFoo()) {
// Clear the field:
m.clearFoo()
} else {
// Field is not present, so set it.
m.setFoo(1);
}Python example
No presence:
m = example.Msg()
if m.foo != 0:
# "Clear" the field:
m.foo = 0
else:
# Default value: field may not have been present.
m.foo = 1Explicit presence:
m = example.Msg()
if m.HasField('foo'):
# Clear the field:
m.ClearField('foo')
else:
# Field is not present, so set it.
m.foo = 1Ruby example
No presence:
m = Msg.new
if m.foo != 0
# "Clear" the field:
m.foo = 0
else
# Default value: field may not have been present.
m.foo = 1
endExplicit presence:
m = Msg.new
if m.has_foo?
# Clear the field:
m.clear_foo
else
# Field is not present, so set it.
m.foo = 1
endJavascript example
No presence:
var m = new Msg();
if (m.getFoo() != 0) {
// "Clear" the field:
m.setFoo(0);
} else {
// Default value: field may not have been present.
m.setFoo(1);
}Explicit presence:
var m = new Msg();
if (m.hasFoo()) {
// Clear the field:
m.clearFoo()
} else {
// Field is not present, so set it.
m.setFoo(1);
}Objective C example
No presence:
Msg *m = [[Msg alloc] init];
if (m.foo != 0) {
// "Clear" the field:
m.foo = 0;
} else {
// Default value: field may not have been present.
m.foo = 1;
}Explicit presence:
Msg *m = [[Msg alloc] init];
if (m.hasFoo()) {
// Clear the field:
[m clearFoo];
} else {
// Field is not present, so set it.
[m setFoo:1];
}Implementing Proto3 Presence (`docs/implementing_proto3_presence.md`)
How To Implement Field Presence for Proto3
Protobuf release 3.12 adds experimental support for optional fields in proto3.
Proto3 optional fields track presence like in proto2. For background information
about what presence tracking means, please see
docs/field_presence.
Document Summary
This document is targeted at developers who own or maintain protobuf code
generators. All code generators will need to be updated to support proto3
optional fields. First-party code generators developed by Google are being
updated already. However third-party code generators will need to be updated
independently by their authors. This includes:
- implementations of Protocol Buffers for other languages.
- alternate implementations of Protocol Buffers that target specialized use
cases. - RPC code generators that create generated APIs for service calls.
- code generators that implement some utility code on top of protobuf
generated classes.
While this document speaks in terms of "code generators", these same principles
apply to implementations that dynamically generate a protocol buffer API "on the
fly", directly from a descriptor, in languages that support this kind of usage.
Background
Presence tracking was added to proto3 in response to user feedback, both from
inside Google and
from open-source users.
The
proto3 wrapper types
were previously the only supported presence mechanism for proto3. Users have
pointed to both efficiency and usability issues with the wrapper types.
Presence in proto3 uses exactly the same syntax and semantics as in proto2.
Proto3 Fields marked optional will track presence like proto2, while fields
without any label (known as "singular fields"), will continue to omit presence
information. The optional keyword was chosen to minimize differences with
proto2.
Unfortunately, for the current descriptor protos and Descriptor API (as of
3.11.4) it is not possible to use the same representation as proto2. Proto3
descriptors already use LABEL_OPTIONAL for proto3 singular fields, which do
not track presence. There is a lot of existing code that reflects over proto3
protos and assumes that LABEL_OPTIONAL in proto3 means "no presence." Changing
the semantics now would be risky, since old software would likely drop proto3
presence information, which would be a data loss bug.
To minimize this risk we chose a descriptor representation that is semantically
compatible with existing proto3 reflection. Every proto3 optional field is
placed into a one-field oneof. We call this a "synthetic" oneof, as it was not
present in the source .proto file.
Since oneof fields in proto3 already track presence, existing proto3
reflection-based algorithms should correctly preserve presence for proto3
optional fields with no code changes. For example, the JSON and TextFormat
parsers/serializers in C++ and Java did not require any changes to support
proto3 presence. This is the major benefit of synthetic oneofs.
This design does leave some cruft in descriptors. Synthetic oneofs are a
compatibility measure that we can hopefully clean up in the future. For now
though, it is important to preserve them across different descriptor formats and
APIs. It is never safe to drop synthetic oneofs from a proto schema. Code
generators can (and should) skip synthetic oneofs when generating a user-facing
API or user-facing documentation. But for any schema representation that is
consumed programmatically, it is important to keep the synthetic oneofs around.
In APIs it can be helpful to offer separate accessors that refer to "real"
oneofs (see API Changes below). This is a convenient way to omit
synthetic oneofs in code generators.
Updating a Code Generator
When a user adds an optional field to proto3, this is internally rewritten as
a one-field oneof, for backward-compatibility with reflection-based algorithms:
syntax = "proto3";
message Foo {
// Experimental feature, not generally supported yet!
optional int32 foo = 1;
// Internally rewritten to:
// oneof _foo {
// int32 foo = 1 [proto3_optional=true];
// }
//
// We call _foo a "synthetic" oneof, since it was not created by the user.
}As a result, the main two goals when updating a code generator are:
- Give
optionalfields likefoonormal field presence, as described in
docs/field_presence If your implementation already
supports proto2, a proto3optionalfield should use exactly the same API
and internal implementation as proto2optional. - Avoid generating any oneof-based accessors for the synthetic oneof. Its only
purpose is to make reflection-based algorithms work properly if they are not
aware of proto3 presence. The synthetic oneof should not appear anywhere in
the generated API.
Satisfying the Experimental Check
If you try to run protoc on a file with proto3 optional fields, you will get
an error because the feature is still experimental:
$ cat test.proto
syntax = "proto3";
message Foo {
// Experimental feature, not generally supported yet!
optional int32 a = 1;
}
$ protoc --cpp_out=. test.proto
test.proto: This file contains proto3 optional fields, but --experimental_allow_proto3_optional was not set.There are two options for getting around this error:
- Pass
--experimental_allow_proto3_optionalto protoc. - Make your filename (or a directory name) contain the string
test_proto3_optional. This indicates that the proto file is specifically
for testing proto3 optional support, so the check is suppressed.
These options are demonstrated below:
# One option:
$ protoc test.proto --cpp_out=. --experimental_allow_proto3_optional
# Another option:
$ cp test.proto test_proto3_optional.proto
$ protoc test_proto3_optional.proto --cpp_out=.
$The experimental check will be removed in a future release, once we are ready to
make this feature generally available. Ideally this will happen for the 3.13
release of protobuf, sometime in mid-2020, but there is not a specific date set
for this yet. Some of the timing will depend on feedback we get from the
community, so if you have questions or concerns please get in touch via a GitHub
issue.
Signaling That Your Code Generator Supports Proto3 Optional
If you now try to invoke your own code generator with the test proto, you will
run into a different error:
$ protoc test_proto3_optional.proto --my_codegen_out=.
test_proto3_optional.proto: is a proto3 file that contains optional fields, but
code generator --my_codegen_out hasn't been updated to support optional fields in
proto3. Please ask the owner of this code generator to support proto3 optional.This check exists to make sure that code generators get a chance to update
before they are used with proto3 optional fields. Without this check an old
code generator might emit obsolete generated APIs (like accessors for a
synthetic oneof) and users could start depending on these. That would create a
legacy migration burden once a code generator actually implements the feature.
To signal that your code generator supports optional fields in proto3, you
need to tell protoc what features you support. The method for doing this
depends on whether you are using the C++google::protobuf::compiler::CodeGenerator framework or not.
If you are using the CodeGenerator framework:
class MyCodeGenerator : public google::protobuf::compiler::CodeGenerator {
// Add this method.
uint64_t GetSupportedFeatures() const override {
// Indicate that this code generator supports proto3 optional fields.
// (Note: don't release your code generator with this flag set until you
// have actually added and tested your proto3 support!)
return FEATURE_PROTO3_OPTIONAL;
}
}If you are generating code using raw CodeGeneratorRequest andCodeGeneratorResponse messages from plugin.proto, the change will be very
similar:
void GenerateResponse() {
CodeGeneratorResponse response;
response.set_supported_features(CodeGeneratorResponse::FEATURE_PROTO3_OPTIONAL);
// Generate code...
}Once you have added this, you should now be able to successfully use your code
generator to generate a file containing proto3 optional fields:
$ protoc test_proto3_optional.proto --my_codegen_out=.Updating Your Code Generator
Now to actually add support for proto3 optional to your code generator. The goal
is to recognize proto3 optional fields as optional, and suppress any output from
synthetic oneofs.
If your code generator does not currently support proto2, you will need to
design an API and implementation for supporting presence in scalar fields.
Generally this means:
- allocating a bit inside the generated class to represent whether a given
field is present or not. - exposing a
has_foo()method for each field to return the value of this
bit. - make the parser set this bit when a value is parsed from the wire.
- make the serializer test this bit to decide whether to serialize.
If your code generator already supports proto2, then most of your work is
already done. All you need to do is make sure that proto3 optional fields have
exactly the same API and behave in exactly the same way as proto2 optional
fields.
From experience updating several of Google's code generators, most of the
updates that are required fall into one of several patterns. Here we will show
the patterns in terms of the C++ CodeGenerator framework. If you are usingCodeGeneratorRequest and CodeGeneratorReply directly, you can translate the
C++ examples to your own language, referencing the C++ implementation of these
methods where required.
To test whether a field should have presence
Old:
bool MessageHasPresence(const google::protobuf::FieldDescriptor* field) {
return field->has_presence();
}New:
// Presence is no longer a property of a message, it's a property of individual
// fields.
bool FieldHasPresence(const google::protobuf::FieldDescriptor* field) {
return field->has_presence();
// Note, the above will return true for fields in a oneof.
// If you want to filter out oneof fields, write this instead:
// return field->has_presence && !field->real_containing_oneof()
}To test whether a field is a member of a oneof
Old:
bool FieldIsInOneof(const google::protobuf::FieldDescriptor* field) {
return field->containing_oneof() != nullptr;
}New:
bool FieldIsInOneof(const google::protobuf::FieldDescriptor* field) {
// real_containing_oneof() returns nullptr for synthetic oneofs.
return field->real_containing_oneof() != nullptr;
}To iterate over all oneofs
Old:
bool IterateOverOneofs(const google::protobuf::Descriptor* message) {
for (int i = 0; i < message->oneof_decl_count(); i++) {
const google::protobuf::OneofDescriptor* oneof = message->oneof(i);
// ...
}
}New:
bool IterateOverOneofs(const google::protobuf::Descriptor* message) {
// Real oneofs are always first, and real_oneof_decl_count() will return the
// total number of oneofs, excluding synthetic oneofs.
for (int i = 0; i < message->real_oneof_decl_count(); i++) {
const google::protobuf::OneofDescriptor* oneof = message->oneof(i);
// ...
}
}Updating Reflection
If your implementation offers reflection, there are a few other changes to make:
API Changes
The API for reflecting over fields and oneofs should make the following changes.
These match the changes implemented in C++ reflection.
- Add a
FieldDescriptor::has_presence()method returningbool(adjusted to
your language's naming convention). This should return true for all fields
that have explicit presence, as documented in
docs/field_presence. In particular, this includes
fields in a oneof, proto2 scalar fields, and proto3optionalfields. This
accessor will allow users to query what fields have presence without
thinking about the difference between proto2 and proto3. - As a corollary of (1), please do not expose an accessor for the
FieldDescriptorProto.proto3_optionalfield. We want to avoid having users
implement any proto2/proto3-specific logic. Users should use thehas_presence()function instead. - You may also wish to add a
FieldDescriptor::has_optional_keyword()method
returningbool, which indicates whether theoptionalkeyword is present.
Message fields will always returntrueforhas_presence(), so this
method can allow a user to know whether the user wroteoptionalor not. It
can occasionally be useful to have this information, even though it does not
change the presence semantics of the field. - If your reflection API may be used for a code generator, you may wish to
implement methods to help users tell the difference between real and
synthetic oneofs. In particular:OneofDescriptor::is_synthetic(): returns true if this is a synthetic
oneof.FieldDescriptor::real_containing_oneof(): likecontaining_oneof(),
but returnsnullptrif the oneof is synthetic.Descriptor::real_oneof_decl_count(): likeoneof_decl_count(), but
returns the number of real oneofs only.
Implementation Changes
Proto3 optional fields and synthetic oneofs must work correctly when reflected
on. Specifically:
- Reflection for synthetic oneofs should work properly. Even though synthetic
oneofs do not really exist in the message, you can still make reflection
work as if they did. In particular, you can make a method likeReflection::HasOneof()orReflection::GetOneofFieldDescriptor()look at
the hasbit to determine if the oneof is present or not. - Reflection for proto3 optional fields should work properly. For example, a
method likeReflection::HasField()should know to look for the hasbit for
a proto3optionalfield. It should not be fooled by the synthetic oneof
into thinking that there is acasemember for the oneof.
Once you have updated reflection to work properly with proto3 optional and
synthetic oneofs, any code that uses your reflection interface should work
properly with no changes. This is the benefit of using synthetic oneofs.
In particular, if you have a reflection-based implementation of protobuf text
format or JSON, it should properly support proto3 optional fields without any
changes to the code. The fields will look like they all belong to a one-field
oneof, and existing proto3 reflection code should know how to test presence for
fields in a oneof.
So the best way to test your reflection changes is to try round-tripping a
message through text format, JSON, or some other reflection-based parser and
serializer, if you have one.
Validating Descriptors
If your reflection implementation supports loading descriptors at runtime, you
must verify that all synthetic oneofs are ordered after all "real" oneofs.
Here is the code that implements this validation step in C++, for inspiration:
// Validation that runs for each message.
// Synthetic oneofs must be last.
int first_synthetic = -1;
for (int i = 0; i < message->oneof_decl_count(); i++) {
const OneofDescriptor* oneof = message->oneof_decl(i);
if (oneof->is_synthetic()) {
if (first_synthetic == -1) {
first_synthetic = i;
}
} else {
if (first_synthetic != -1) {
AddError(message->full_name(), proto.oneof_decl(i),
DescriptorPool::ErrorCollector::OTHER,
"Synthetic oneofs must be after all other oneofs");
}
}
}
if (first_synthetic == -1) {
message->real_oneof_decl_count_ = message->oneof_decl_count_;
} else {
message->real_oneof_decl_count_ = first_synthetic;
}Jvm Aot (`docs/jvm_aot.md`)
Ahead Of Time (AOT) compilation for the Java Virtual Machine (JVM)
Ahead Of Time (AOT) compilation build tools such as those provided by
GraalVM's native-image
can require some configuration when using protobuf. Protobuf for the JVM uses
reflection, and some of its target classes are not possible to determine in
advance. This situation is unlikely to change.
The Lite version of protobuf for the JVM
avoids reflection and may be better suited for use with AOT compilation tooling.
This Lite version was originally targeted for use on Android which has similar
AOT compilation goals as GraalVM's native-image tool.
GraalVM native-image
The GraalVM native-image tool can be configured with options for the
Java Native Interface
(JNI), http proxying, reflection, and other resources. While these configuration
options can be manually specified in JSON files, we recommend that you exercise
the application with
the assisted configuration agent.
The agent will generate native-image.properties files that you add to the
project's META-INF/native-image/ directory. The Native Image builder uses
configuration options provided in META-INF/native-image/ to construct
native-image command line arguments.
Options (`docs/options.md`)
Protobuf Global Extension Registry
This file contains a global registry of known extensions for descriptor.proto,
so that any developer who wishes to use multiple 3rd party projects, each with
their own extensions, can be confident that there won't be collisions in
extension numbers.
If you need an extension number for your custom option (see
custom options),
please
send us a pull request to
add an entry to this doc, or
create an issue with info
about your project (name and website) so we can add an entry for you.
Existing Registered Extensions
C# port of protocol buffers
- Website: https://github.com/jskeet/protobuf-csharp-port
- Extensions: 1000
Perl/XS port of protocol buffers
- Website: http://code.google.com/p/protobuf-perlxs
- Extensions: 1001
Objective-C port of protocol buffers
- Website: http://code.google.com/p/protobuf-objc
- Extensions: 1002
Google Wave Federation Protocol open-source release (FedOne)
- Website: http://code.google.com/p/wave-protocol
- Extensions: 1003
PHP code generator plugin
- Website: ???
- Extensions: 1004
GWT code generator plugin (third-party!)
- Website: http://code.google.com/p/protobuf-gwt/
- Extensions: 1005
Unix Domain RPC code generator plugin
- Website: http://go/udrpc
- Extensions: 1006
Object-C generator plugin (Plausible Labs)
- Website: http://www.plausible.coop
- Extensions: 1007
TBD (code42.com)
- Website: ???
- Extensions: 1008
Goby Underwater Autonomy Project
- Website: https://github.com/GobySoft/goby
- Extensions: 1009
Nanopb
- Website: http://kapsi.fi/~jpa/nanopb
- Extensions: 1010
Bluefin AUV Communication Extensions
- Website: http://www.bluefinrobotics.com
- Extensions: 1011
Dynamic Compact Control Language
- Website: http://github.com/GobySoft/dccl
- Extensions: 1012
ScaleOut StateServer® Native C++ API
- Website: http://www.scaleoutsoftware.com
- Extensions: 1013
FoundationDB SQL Layer
- Website: https://github.com/FoundationDB/sql-layer
- Extensions: 1014
Fender
- Website: https://github.com/hassox/fender
- Extensions: 1015
Vortex
- Website: http://www.prismtech.com/vortex
- Extensions: 1016
tresorit
- Website: https://tresorit.com/
- Extensions: 1017
CRIU (Checkpoint Restore In Userspace)
- Website: http://criu.org/Main_Page
- Extensions: 1018
protobuf-c
- Website: https://github.com/protobuf-c/protobuf-c
- Extensions: 1019
ScalaPB
- Website: https://scalapb.github.io/
- Extensions: 1020
protoc-gen-bq-schema
- Website: https://github.com/GoogleCloudPlatform/protoc-gen-bq-schema
- Extensions: 1021
grpc-gateway
- Website: https://github.com/gengo/grpc-gateway
- Extensions: 1022
Certificate Transparency
- Website: https://github.com/google/certificate-transparency
- Extensions: 1023
JUNOS Router Telemetry
- Website: http://www.juniper.net
- Extensions: 1024
Spine Event Engine
- Website: https://github.com/SpineEventEngine/core-java
- Extensions: 1025
Aruba cloud platform
- Website: ???
- Extensions: 1026 -> 1030
Voltha
- Website: ???
- Extensions: 1031 -> 1033
gator
- Website: ???
- Extensions: 1034
protoc-gen-flowtypes
- Website:
https://github.com/tmc/grpcutil/tree/master/protoc-gen-flowtypes - Extensions: 1035
- Website:
ProfaneDB
- Website: https://gitlab.com/ProfaneDB/ProfaneDB
- Extensions: 1036
protobuf-net
- Website: https://github.com/mgravell/protobuf-net
- Extensions: 1037
FICO / StreamEngine
- Website: http://www.fico.com/
- Extensions: 1038
GopherJS
- Website: https://github.com/johanbrandhorst/protobuf
- Extensions: 1039
ygot
- Website: https://github.com/openconfig/ygot
- Extensions: 1040, 1179, 1180
go-grpcmw
- Website: https://github.com/MarquisIO/go-grpcmw
- Extensions: 1041
grpc-gateway protoc-gen-openapiv2
- Website: https://github.com/grpc-ecosystem/grpc-gateway
- Extensions: 1042
AN Message
- Website: TBD
- Extensions: 1043
protofire
- Website: https://github.com/ribrdb/protofire
- Extensions: 1044
Gravity
- Website: https://github.com/aphysci/gravity
- Extensions: 1045
SEMI Standards – I&C Technical Committee
Elixir plugin
- Website: https://github.com/tony612/grpc-elixir
- Extensions: 1047
API client generators
- Website: ???
- Extensions: 1048-1056
Netifi Proteus
- Website: https://github.com/netifi-proteus
- Extensions: 1057
CGSN Mooring Project
- Website: https://bitbucket.org/ooicgsn/cgsn-mooring
- Extensions: 1058
Container Storage Interface
- Website: https://github.com/container-storage-interface/spec
- Extensions: 1059-1069
TwirpQL Plugin
- Website: https://twirpql.dev
- Extensions: 1070
Protoc-gen-validate
- Website: https://github.com/bufbuild/protoc-gen-validate
- Extensions: 1071
Protokt (pre 1.0.0)
- Website: https://github.com/open-toast/protokt
- Extensions: 1072
Dart port of protocol buffers
- Website https://github.com/dart-lang/protobuf
- Extensions: 1073
Ocaml-protoc-plugin
- Website: https://github.com/issuu/ocaml-protoc-plugin
- Extensions: 1074
Analyze Re Graphene
- Website: https://analyzere.com
- Extensions: 1075
Wire since and until
- Website: https://square.github.io/wire/
- Extensions: 1076, 1077
Bazel, Failure Details
- Website: https://github.com/bazelbuild/bazel
- Extensions: 1078
grpc-graphql-gateway
- Website: https://github.com/ysugimoto/grpc-graphql-gateway
- Extensions: 1079
Cloudstate
- Website: https://cloudstate.io
- Extensions: 1080-1084
SummaFT protoc-plugins
- Website: https://summaft.com/
- Extensions: 1085
ADLINK EdgeSDK
- Website: https://www.adlinktech.com/en/Edge-SDK-IoT
- Extensions: 1086
Wire wire_package
- Website: https://square.github.io/wire/
- Extensions: 1087
Confluent Schema Registry
- Website: https://github.com/confluentinc/schema-registry
- Extensions: 1088
ScalaPB Validate
- Website: https://scalapb.github.io/docs/validation
- Extension: 1089
Astounding (Currently Private)
- Website: https://github.com/PbPipes/Astounding
- Extension: 1090
Protoc-gen-psql
- Website: https://github.com/Intrinsec/protoc-gen-psql
- Extension: 1091-1101
Protoc-gen-sanitize
- Website: https://github.com/Intrinsec/protoc-gen-sanitize
- Extension: 1102-1106
Coach Client Connect (planned release in March 2021)
- Website: https://www.coachclientconnect.com
- Extension: 1107
Kratos API Errors
- Website: https://go-kratos.dev
- Extension: 1108
Glitchdot (Currently Private)
- Website: https://go.glitchdot.com
- Extension: 1109
eigr/protocol
- Website: https://eigr.io
- Extension: 1110-1114
Container Object Storage Interface (COSI)
- Website:
https://github.com/kubernetes-sigs/container-object-storage-interface-spec - Extension: 1115-1124
- Website:
Protoc-gen-jsonschema
- Website: https://github.com/chrusty/protoc-gen-jsonschema
- Extension: 1125-1129
Protoc-gen-checker
- Website: https://github.com/Intrinsec/protoc-gen-checker
- Extension: 1130-1139
Protoc-gen-go-svc
- Website: https://github.com/dane/protoc-gen-go-svc
- Extension: 1140
Embedded Proto
- Website: https://EmbeddedProto.com
- Extension: 1141
Protoc-gen-fieldmask
- Website: https://github.com/yeqown/protoc-gen-fieldmask
- Extension: 1142
Google Gnostic
- Website: https://github.com/google/gnostic
- Extension: 1143
Protoc-gen-go-micro
- Website: https://github.com/unistack-org/protoc-gen-go-micro
- Extension: 1144
Protoc-gen-authz
- Website: https://github.com/Neakxs/protoc-gen-authz
- Extension: 1145
Protonium
- Website: https://github.com/zyp/protonium
- Extension: 1146
Protoc-gen-xo
- Website: https://github.com/xo/ecosystem
- Extension: 1147
Ballerina gRPC
- Website: https://github.com/ballerina-platform/module-ballerina-grpc
- Extension: 1148
Protoc-gen-referential-integrity
- Website:
https://github.com/ComponentCorp/protoc-gen-referential-integrity - Extension: 1149
- Website:
Oclea Service Layer RPC
- Website: https://oclea.com/
- Extension: 1150
mypy-protobuf
- Website: https://github.com/nipunn1313/mypy-protobuf
- Extension: 1151-1154
Pigweed protobuf compiler
- Website: https://pigweed.dev/pw_protobuf
- Extension: 1155
Perfetto
- Website: https://perfetto.dev
- Extension: 1156
Buf
- Website: http://buf.build/
- Extension: 1157-1166
Connect RPC
- Website: http://connectrpc.com/
- Extension: 1167-1176
protocel
- Website: https://github.com/Neakxs/protocel
- Extension: 1177-1178
Cybozu
- Website: https://github.com/cybozu/protobuf
- Extension: 1179
EngFlow
- Website: https://github.com/EngFlow/engflowapis
- Extensions: 1181
Proto-telemetry
- Website: https://github.com/clly/proto-telemetry
- Extensions: 1182
Digital Twins Definition Language (DTDL)
- Website: https://github.com/Azure/opendigitaltwins-dtdl
- Extensions: 1183
RabbitMQ
- Website: https://github.com/guihouchang/protoc-gen-go-event
- Extensions: 1184
Wire use_array
- Website: https://square.github.io/wire/
- Extensions: 1185
Protons
- Website: https://github.com/ipfs/protons
- Extensions: 1186
gRPC Federation
- Website: https://github.com/mercari/grpc-federation
- Extensions: 1187
protoc-gen-openapi
- Website: https://github.com/solo-io/protoc-gen-openapi
- Extensions: 1188-1189
Wire enumMode
- Website: https://square.github.io/wire/
- Extensions: 1190
Nebius AI
- Website: https://nebius.ai
- Extensions: 1191-1199
Buf
- Website: http://buf.build/
- Extension: 1200-1239
userver
- Website: https://userver.tech/
- Extensions: 1240-1249
protoc-gen-default / protoc-gen-env (confpb)
- Website: https://github.com/MarnixBouhuis/confpb
- Extensions: 1250-1251
JaiaBot
- Website: https://github.com/jaiarobotics/jaiabot
- Extensions: 1252
Protokt (1.0.0 and up)
- Website: https://github.com/open-toast/protokt
- Extensions: 1253-1263
aep.dev Extensions
- Website: https://github.com/aep-dev
- Extensions: 1264-1274
Thermo Fisher Scientific TEM Extensions
- Website: https://www.thermofisher.com/
- Extensions: 1275-1284
LiveKit
- Website: https://livekit.io
- Extensions: 1285-1294
grpc-gateway protoc-gen-openapiv3
- Website: https://github.com/grpc-ecosystem/grpc-gateway
- Extensions: 1295
Protosource
- Website: https://github.com/funinthecloud/protosource
- Extensions: 1296-1305
protosearch
- Website: https://github.com/benwebber/protosearch
- Extensions: 1306
proto2type
- Website: https://github.com/protocgen/proto2type
- Extensions: 1307
protoc-gen-openapi
- Website: https://github.com/engine-public/protoc-gen-openapi
- Extensions: 1308
Motif
- Website: https://motif.io
- Extensions: 1309
Teleport
- Website: https://goteleport.com
- Extensions: 1310
proto2mcp
- Website: https://github.com/protocgen/proto2mcp
- Extensions: 1311-1313
ProtoWire
- Website: https://protowire.org
- Extensions: 1314-1363
Flyte
- Website: https://github.com/flyteorg/flyte
- Extensions: 1364-1373
durable
- Website: https://github.com/dangra/durable
- Extensions: 1374-1375
firestore-proto-codec
- Website: https://github.com/willbinge/firestore-proto-codec
- Extensions: 1376-1380
buffa
- Website: https://github.com/anthropics/buffa
- Extensions: 1381-1390
Third Party (`docs/third_party.md`)
Third-Party Add-ons for Protocol Buffers
This page lists code related to Protocol Buffers which is developed and
maintained by third parties. You may find this code useful, but note that
these projects are not affiliated with or endorsed by Google (unless
explicitly marked); try them at your own risk. Also note that many projects
here are in the early stages of development and not production-ready.
If you have a project that should be listed here, please
send us a pull request to
update this page.
Programming Languages
These are projects we know about implementing Protocol Buffers for other
programming languages:
- Action Script: https://github.com/matrix3d/JProtoc
- Action Script: https://github.com/zhongfq/protobuf-as3/
- Ada: https://github.com/reznikmm/protobuf
- C: https://github.com/protobuf-c/protobuf-c
- C: https://koti.kapsi.fi/jpa/nanopb/
- C: https://github.com/cloudwu/pbc/
- C: https://github.com/haberman/upb/wiki
- C: https://github.com/squidfunk/protobluff
- C: https://github.com/eerimoq/pbtools
- C++: https://github.com/protocolbuffers/protobuf (Google-official
implementation) - C++: https://github.com/tonda-kriz/simple-protobuf
- C++: https://EmbeddedProto.com
- C++: https://github.com/yksten/struct2x
- C/C++: http://spbc.sf.net/
- C#: https://silentorbit.com/protobuf/
- Clojure: http://github.com/ninjudd/clojure-protobuf
- Clojure: https://github.com/clojusc/protobuf
- Clojure: https://protojure.readthedocs.io
- Clojure: https://github.com/s-expresso/clojobuf
- ClojureScript: https://github.com/s-expresso/clojobuf
- Common Lisp: http://github.com/brown/protobuf
- Common Lisp: http://github.com/qitab/cl-protobuf
- D: https://github.com/dcarp/protobuf-d
- D: https://github.com/msoucy/dproto
- D: https://github.com/opticron/ProtocolBuffer
- Dart: https://github.com/dart-lang/dart-protobuf (runtime)
https://github.com/dart-lang/dart-protoc-plugin (code generator) - Delphi: http://sourceforge.net/projects/protobuf-delphi/
- Delphi: http://fundementals.sourceforge.net/dl.html
- Elixir: https://github.com/jeremyong/exprotoc
- Elixir: https://github.com/tony612/protobuf-elixir
- Elixir: https://github.com/ahamez/protox
- Elm: https://github.com/tiziano88/elm-protobuf
- Erlang: https://github.com/tomas-abrahamsson/gpb
- Erlang: http://piqi.org/
- Erlang: https://github.com/basho/erlang_protobuffs (no longer maintained,
use gpb instead) - Hacklang/HHVM: https://github.com/y3llowcake/proto-hack
- GDScript: https://github.com/oniksan/godobuf (Godot v3 engine plugin)
- Go: https://github.com/golang/protobuf (Google-official implementation)
- Go: https://github.com/akunspy/gopbuf
- Go: https://github.com/gogo/protobuf
- GopherJS: https://github.com/johanbrandhorst/protobuf
- Haskell: https://hackage.haskell.org/package/hprotoc
- Haskell: https://github.com/google/proto-lens (Google-unofficial
implementation) - Haskell: https://github.com/awakesecurity/proto3-suite (code generator)
https://github.com/awakesecurity/proto3-wire (binary
serializer/deserializer) - Haxe: https://github.com/Atry/protoc-gen-haxe
- Java: https://github.com/protocolbuffers/protobuf (Google-official
implementation) - Java/Android: https://github.com/square/wire
- Java: https://github.com/HebiRobotics/QuickBuffers/
- Java ME: http://swingme.sourceforge.net/encode.shtml
- Javascript: http://github.com/bufbuild/protobuf-es
- Javascript: http://github.com/sirikata/protojs
- Javascript: https://github.com/dcodeIO/ProtoBuf.js
- Javascript: https://github.com/seishun/node-protoc-plugin (Node.js port of
plugin.h) - Javascript: https://github.com/seishun/node-protoc-gen-javascript (Node.js
port of the Google-official implementation) - Javascript: https://github.com/ButterCam/sisyphus-js
- Julia: https://github.com/JuliaIO/ProtoBuf.jl
- Kotlin: https://github.com/marcoferrer/kroto-plus
- Kotlin: https://github.com/Kotlin/kotlinx.serialization
- Kotlin: https://github.com/ButterCam/sisyphus
- Kotlin: https://github.com/open-toast/protokt
- Kotlin Multiplatform: https://github.com/streem/pbandk
- Lua: http://github.com/indygreg/lua-protobuf
- Lua: https://github.com/Neopallium/lua-pb
- Objective C: https://github.com/alexeyxo/protobuf-objc
- OCaml: http://piqi.org/
- Perl: http://groups.google.com/group/protobuf-perl
- Perl: https://metacpan.org/pod/Google::ProtocolBuffers
- Perl: https://metacpan.org/pod/Google::ProtocolBuffers::Dynamic
- PHP: https://github.com/allegro/php-protobuf/
- PHP: https://github.com/chobie/php-protocolbuffers
- Protobuf: https://github.com/mycroftjr/protoc-gen-proto
- Prolog: http://www.swi-prolog.org/pldoc/package/protobufs.html
- Purescript: https://github.com/xc-jp/purescript-protobuf
- Python: https://github.com/protocolbuffers/protobuf (Google-official
implementation) - Python: https://github.com/eigenein/protobuf
- Python: https://github.com/danielgtaylor/python-betterproto
- R: http://cran.r-project.org/package=RProtoBuf
- Ruby: http://github.com/mozy/ruby-protocol-buffers
- Ruby: https://github.com/bmizerany/beefcake/tree/master/lib/beefcake
- Ruby: https://github.com/localshred/protobuf
- Rust: https://github.com/tokio-rs/prost
- Rust: https://github.com/stepancheg/rust-protobuf/
- Rust: https://github.com/tafia/quick-protobuf
- Scala: http://github.com/jeffplaisance/scala-protobuf
- Scala: https://github.com/SandroGrzicic/ScalaBuff
- Scala: https://scalapb.github.io
- Solidity: https://github.com/celer-network/pb3-gen-sol
- Swift: https://github.com/alexeyxo/protobuf-swift
- Swift: https://github.com/apple/swift-protobuf/
- Typescript: http://github.com/bufbuild/protobuf-es
- Typescript: https://github.com/improbable-eng/ts-protoc-gen
- Typescript: https://github.com/thesayyn/protoc-gen-ts
- Typescript: https://github.com/pbkit/pbkit
- Vala: https://launchpad.net/protobuf-vala
- Zig: https://github.com/Arwalk/zig-protobuf
RPC Implementations
GRPC (http://www.grpc.io/) is Google's RPC implementation for Protocol Buffers.
There are other third-party RPC implementations as well. Some of these actually
work with Protocol Buffers service definitions (defined using the service
keyword in .proto files) while others just use Protocol Buffers message
objects.
- https://github.com/grpc/grpc (C++, Node.js, Python, Ruby, Objective-C, PHP,
C#, Google-official implementation) - https://connectrpc.com (Go, Javascript, Typescript, Python, Dart, Swift,
Kotlin) - https://zeroc.com/products/ice (Multiple languages)
- https://github.com/protobuf-net/protobuf-net (C#/.NET/WCF/VB)
- http://www.deltavsoft.com/doc/_external_serialization.html#Protobufs (C++)
- https://protojure.github.io (Clojure)
- https://github.com/baidu/sofa-pbrpc (C++)
- https://github.com/madhon/protobuf-csharp-rpc (C#)
- https://github.com/madwyn/libpbrpc (C++)
- https://github.com/SeriousMa/grpc-protobuf-validation (Java)
- https://github.com/elixir-grpc/grpc (Elixir)
- https://github.com/johanbrandhorst/protobuf (GopherJS)
- https://github.com/awakesecurity/gRPC-haskell (Haskell)
- https://github.com/Yeolar/raster (C++)
- https://github.com/jnordberg/wsrpc (JavaScript Node.js/Browser)
- https://github.com/pbkit/npm-packages/blob/main/frpc-test/src/index.spec.ts
(TypeScript Node.js/Browser) - https://github.com/ppissias/xsrpcj (Java)
- https://github.com/twitchtv/twirp (Multiple languages)
- https://github.com/icerpc/icerpc-csharp (C#)
- https://philips-software.github.io/amp-embedded-infra-lib (C++, C#, Java)
- https://github.com/tdaira/blerpc (C/nanopb, Python, Kotlin, Swift, Dart,
TypeScript; RPC over Bluetooth Low Energy)
Inactive:
- https://code.google.com/p/protobuf-actionscript3/ (Action Script)
- https://code.google.com/p/protoc-gen-as3/ (Action Script)
- https://code.google.com/p/protobuf-csharp-port (C#)
- https://code.google.com/p/protobuf-net/ (C#/.NET/WCF/VB)
- https://code.google.com/p/protobuf-javame/ (Java ME)
- https://code.google.com/p/protobuf-rpc-pro/ (Java)
- https://launchpad.net/txprotobuf/ (Python)
- https://github.com/modeswitch/protobuf-rpc (Python)
- https://github.com/sdeo/protobuf-socket-rpc (Java, Python)
- https://code.google.com/p/proto-streamer/ (Java)
- https://code.google.com/p/server1/ (C++)
- https://code.google.com/p/protobuf-mina-rpc/ (Python client, Java server)
- https://code.google.com/p/casocklib/ (C++)
- https://code.google.com/p/cxf-protobuf/ (Java)
- https://code.google.com/p/protobuf-remote/ (C++/C#)
- https://code.google.com/p/eneter-protobuf-serializer/ (Java/.NET)
- https://github.com/robbinfan/claire/tree/master/protorpc (C++)
- https://github.com/ebencheung/arab (C++)
- https://code.google.com/p/protobuf-csharp-rpc/ (C#)
- https://github.com/thesamet/rpcz (C++/Python, based on ZeroMQ)
- https://github.com/w359405949/libmaid (C++, Python)
- https://code.google.com/p/protobuf-js/ (Javascript)
- https://code.google.com/p/protobuf-for-node/ (Javascript)
- https://code.google.com/p/protostuff/ (Javascript)
- https://code.google.com/p/protoc-gen-lua/ (Lua)
- https://code.google.com/p/protobuf-matlab/ (Matlab)
- https://code.google.com/p/protobuf-mercury/ (Mercury)
- https://code.google.com/p/protobuf-objc/ (ObjC)
- https://code.google.com/p/protobuf-perlxs/ (Perl/XS)
- https://code.google.com/p/pb4php/ (PHP)
- https://code.google.com/p/ruby-protobuf/ (Ruby)
- https://code.google.com/p/protobuf-scala (Scala)
- https://code.google.com/p/protobuf-net/ (Visual Basic)
- https://code.google.com/p/protoc-gen-docbook/ (DocBook generator)
Build
- Bazel Build
- rules_closure
js-closure - rules_go
go - rules_protobuf
javac++c#gojs-closurejs-nodepythonruby
- rules_closure
- Maven Protobuf Compiler Plugin
- Protobuf Plugin for Gradle
- Sbt plugin for Protocol Buffers
IDE
- Buf for Visual Studio Code
- Visual Studio Code Support for Protocol Buffers
- Visual Studio Language Service support for Protocol Buffers
- IntelliJ IDEA plugin
- IntelliJ Protobuf Plugin
- TextMate syntax highlighting
- Notepad++ Syntax Highlighting for .proto files
- Eclipse editor for protobuf (from Google)
- NetBeans IDE plugin
- Editor for serialized protobufs
Documentation
Other Utilities
There are miscellaneous other things you may find useful as a Protocol Buffers
developer.
- Buf CLI
- Wireshark/Ethereal packet sniffer plugin
- Alternate encodings (JSON, XML, HTML) for Java protobufs
- Another JSON encoder/decoder for Java
- Oracle PL SQL plugin
- C++ Builder compatible protobuf
- Protobuf for nginx module
- RSpec matchers and Cucumber step defs for testing Protocol Buffers
- Multi-platform executable JAR and Java API for protoc
- Python scripts to convert between Protocol Buffers and JSON
- C++ library for serialization/de-serialization between Protocol Buffers and
JSON. - ProtoBuf with Java EE7 Expression Language 3.0; pure Java ProtoBuf Parser
and Builder. - Linter for .proto files
- Protocol Buffers Dynamic Schema - create protobuf schemas programmatically
(Java) - Make protoc plugins in NodeJS
- ProfaneDB - A Protocol Buffers database
- Protocol Buffer property-based testing utility and example message
generator (Python /
Hypothesis) - Protolock - CLI utility to prevent backward-incompatible changes to .proto
files - Optional GRPC - GRPC for testable microservices (Python)
- Protobuf Parser - Yet another Go package which parses a Protocol Buffer
file (proto2+proto3) - Protolint - A tool to enforce Protocol Buffer style and conventions.
- super-linter: Protocol Buffer lint as GitHub Action
- protoc-gen-fieldmask - A plugin to generate static type fieldmask paths
- protoc-gen-bq-schema - A protoc plugin to generate BigQuery schema files
- protobuf-decoder-explainer - Web page which decodes and explains pasted
binary Protocol
Buffers - rubberbuf - dynamically transpile protobuf into AST
clojureclojurescript - clojobuf-serdes - low level serialize/deserialize library for protobuf
binary formatclojureclojurescript - gRPC Federation - generates a gRPC server by writing a custom option in
Protocol Buffers - Proto Boiler - A plugin to generate boilerplate code from .proto files
using templates
Csharp Proto2 (`docs/csharp/proto2.md`)
As part of the 3.10 release of Google.Protobuf, experimental proto2 support has been released. This document outlines the new changes brought about to include proto2 support. This does not break existing proto3 support and users may continue to use proto3 features without changing their current code. Again the generated code and public API associated with proto2 is experimental and subject to change in the future. APIs for proto2 may be added, removed, or adjusted as feedback is received.
Generated code for proto2 may also be modified by adding, removing, or adjusting APIs as feedback is received.
Enabling proto2 features
For information about specific proto2 features, please read the proto2 language guide.
Much like other languages, proto2 features are used with proto2 files with the syntax declaration syntax = "proto2";. However, please note, proto3 is still the recommended version of protobuf and proto2 support is meant for legacy system interop and advanced uses.
Generated code
Messages
Messages in proto2 files are very similar to their proto3 counterparts. They expose the usual property for getting and setting, but they also include properties and methods to handle field presence.
For optional/required field XYZ, a HasXYZ property is included for checking presence and a ClearXYZ method is included for clearing the value.
message Foo {
optional Bar bar = 1;
required Baz baz = 2;
}var foo = new Foo();
Assert.IsNull(foo.Bar);
Assert.False(foo.HasBar);
foo.Bar = new Bar();
Assert.True(foo.HasBar);
foo.ClearBar();Messages with extension ranges
Messages which define extension ranges implement the IExtendableMessage interface as shown below.
See inline comments for more info.
public interface IExtendableMessage<T> : IMessage<T> where T : IExtendableMessage<T>
{
// Gets the value of a single value extension. If the extension isn't present, this returns the default value.
TValue GetExtension<TValue>(Extension<T, TValue> extension);
// Gets the value of a repeated extension. If the extension hasn't been set, this returns null to prevent unnecessary allocations.
RepeatedField<TValue> GetExtension<TValue>(RepeatedExtension<T, TValue> extension);
// Gets the value of a repeated extension. This will initialize the value of the repeated field and will never return null.
RepeatedField<TValue> GetOrInitializeExtension<TValue>(RepeatedExtension<T, TValue> extension);
// Sets the value of the extension
void SetExtension<TValue>(Extension<T, TValue> extension, TValue value);
// Returns whether the extension is present in the message
bool HasExtension<TValue>(Extension<T, TValue> extension);
// Clears the value of the extension, removing it from the message
void ClearExtension<TValue>(Extension<T, TValue> extension);
// Clears the value of the repeated extension, removing it from the message. Calling GetExtension after this will always return null.
void ClearExtension<TValue>(RepeatedExtension<T, TValue> extension);
}Extensions
Extensions are generated in static containers like reflection classes and type classes.
For example for a file called foo.proto containing extensions in the file scope, aFooExtensions class is created containing the extensions defined in the file scope.
For easy access, this class can be used with using static to bring all extensions into scope.
option csharp_namespace = "FooBar";
extend Foo {
optional Baz foo_ext = 124;
}
message Baz {
extend Foo {
repeated Baz repeated_foo_ext = 125;
}
}public static partial class FooExtensions {
public static readonly Extension<Foo, Baz> FooExt = /* initialization */;
}
public partial class Baz {
public partial static class Extensions {
public static readonly RepeatedExtension<Foo, Baz> RepeatedFooExt = /* initialization */;
}
}using static FooBar.FooExtensions;
using static FooBar.Baz.Extensions;
var foo = new Foo();
foo.SetExtension(FooExt, new Baz());
foo.GetOrInitializeExtension(RepeatedFooExt).Add(new Baz());APIs
Message initialization
Initialization refers to checking the status of required fields in a proto2 message. If a message is uninitialized, not all required fields are set in either the message itself or any of its submessages. In other languages, missing required fields throw errors depending on the merge method used. This could cause unforeseen errors at runtime if the incorrect method is used.
However, in this implementation, parsers and input streams don't check messages for initialization on their own and throw errors. Instead it's up to you to handle messages with missing required fields in whatever way you see fit.
Checking message initialization can be done manually via the IsInitialized extension method in MessageExtensions.
Extension registries
Just like in Java, extension registries can be constructed to parse extensions when reading new messages
from input streams. The API is fairly similar to the Java API with some added bonuses with C# syntax sugars.
message Baz {
extend Foo {
optional Baz foo_ext = 124;
}
}var registry = new ExtensionRegistry()
{
Baz.Extensions.FooExt
};
var foo = Foo.Factory.WithExtensionRegistry(registry).ParseFrom(input);
Assert.True(foo.HasExtension(Bas.Extensions.FooExt));
var fooNoRegistry = Foo.Factory.ParseFrom(input);
Assert.False(foo.HasExtension(Bas.Extensions.FooExt));Custom options
Due to their limited use and lack of type safety, the original CustomOptions APIs are now deprecated. Using the new generated extension identifiers, you can access extensions safely through the GetOption APIs. Note that cloneable values such as
repeated fields and messages will be deep cloned.
Example based on custom options usage example here.
foreach (var service in input.Services)
{
Console.WriteLine($" {service.Name}");
foreach (var method in service.Methods)
{
var rule = method.GetOption(AnnotationsExtensions.Http);
if (rule != null)
{
Console.WriteLine($" {method.Name}: {rule}");
}
else
{
Console.WriteLine($" {method.Name}: no HTTP binding");
}
}
}Reflection
Reflection APIs have been extended to enable accessing the new proto2 portions of the library and generated code.
- FieldDescriptor.Extension
- Gets the extension identifier behind an extension field, allowing it to be added to an ExtensionRegistry
- FieldDescriptor.IsExtension
- Returns whether a field is an extension of another type.
- FieldDescriptor.ExtendeeType
- Returns the extended type of an extension field
- IFieldAccessor.HasValue
- Returns whether a field's value is set. For proto3 fields, throws an InvalidOperationException.
- FileDescriptor.Syntax
- Gets the syntax of a file
- FileDescriptor.Extensions
- An immutable list of extensions defined in the file
- MessageDescriptor.Extensions
- An immutable list of extensions defined in the message
var extensions = Baz.Descriptor.Extensions.GetExtensionsInDeclarationOrder(Foo.Descriptor);
var registry = new ExtensionRegistry();
registry.AddRange(extensions.Select(f => f.Extension));
var baz = Foo.Descriptor.Parser.WithExtensionRegistry(registry).ParseFrom(input);
foreach (var field in extensions)
{
if (field.Accessor.HasValue(baz))
{
Console.WriteLine($"{field.Name}: {field.Accessor.GetValue(baz)}");
}
}Upb Arena Fusion (`docs/upb/arena_fusion.md`)
μpb Arena Fusion and References
μpb generally follows a thread-compatibility model where only operations on aconst pointer may occur concurrently from multiple threads; non-const
operations must not race with each other or operations on const pointers.
Fusion, via upb_Arena_Fuse, binds the lifetime of two arenas together, such
that none are freed until all of the transitively fused arenas reach refcount 0.
This is useful to avoid dangling pointers when a message in one arena references
a message in another - such as when a child message is constructed in isolation
then set onto a parent message - by fusing the parent's arena with the child's,
the child's lifetime is tied to the parent's without needing to copy it.
Both modifying the refcount and fusing are thread safe; if you want to
conceptually perform allocations concurrently from multiple threads with the
same arena lifetime, one way to achieve that would be to share a const upb_Arena* parent plus a dedicated arena per thread, and fuse the
thread-specific arena with the parent arena. By contrast, reference counting
cannot help to achieve concurrent allocations on multiple threads, it only helps
in synchronizing lifetimes from multiple things observing the same arena (which
may be a non-const pointer for multiple writers in a single threaded context,
or a const pointer for multiple readers in a multithreaded context).
To be usable everywhere, fusion has a lock-free implementation, documented
below.
Data Structure
Each arena has three pointer sized members used to track the relationship
between arenas. Together, they implement a hybrid disjoint set forest and
doubly-linked list.
// Tagged pointer - tracked as black arrows in diagrams
UPB_ATOMIC(uintptr_t) parent_or_count;
// Linked list - tracked as red arrows in diagrams
UPB_ATOMIC(struct upb_ArenaInternal*) next;
// Linked list - previous tracked as blue arrows in diagrams, tail as dashed
UPB_ATOMIC(uintptr_t) previous_or_tail;The disjoint set is used to check whether two arenas are already fused and to
provide agreement on a single reference count across all fused arenas. The
linked list is used to implement freeing arenas once the refcount reaches 0, and
to implement upb_Arena_SpaceAllocated.
Two arenas are considered fused when the disjoint set says they are; the linked
list is guaranteed to converge before the refcount reaches 0, but may only track
a subset of fused arenas while racing with fuse calls.
Finding the root
Each set of fused arenas is uniquely identified by the root node of its tree. To
find the root for a given arena, traverse the parent_or_count pointer until
you reach a node with a count instead of a parent pointer - that's the root. To
avoid expensive lookups if frequently repeated, this data structure uses path
splitting to halve the distance from the root for every node traversed.
Repeatedly finding the root for a series of nodes that are part of the same
fused set will converge to O(1) quickly. Let's find the root of D, starting
with:
digraph {
C [label="C (next)"]
D [label="D (current)"]
A -> B [dir=back]
B -> C [dir=back]
C -> D [dir=back]
}We traverse our parents; each time we pass one we link it to its grandparent, so
that future lookups are faster.
digraph {
B [label="B (next)"]
C [label="C (current)"]
A -> B [dir=back]
B -> C [dir=back]
B -> D [dir=back]
}Then:
digraph {
A [label="A (next)"]
B [label="B (current)"]
A -> B [dir=back]
A -> C [dir=back]
B -> D [dir=back]
}D and C's paths got shorter; if we query D's root again, all subsequent lookups
will only require one step.
Fusing
Fusing starts by identifying the roots of each arena to fuse - if they already
share a root, there's nothing to be done. We'll work through the example of
fusing C and D - which is the same as fusing A and B, as they're the roots.
digraph {
A [label="A (refs=2)"]
B [label="B (refs=3)"]
A -> C [dir=back]
A -> C [weight=0.001, color="red"]
C -> A [weight=0.001, color="blue"]
A -> C [style=dashed]
B -> D [dir=back]
B -> D [weight=0.001, color="red"]
D -> B [weight=0.001, color="blue"]
B -> D [style=dashed]
}A and B are roots, so they store a refcount in their parent_or_count and the
start of the linked list in their next pointers; C and D are not, so they
store pointers to their parent node in parent_or_count. C and D store NULL
in next as they happen to be the last nodes in their set.
Pass refcount
As soon as the parent changes, all future refcount operations will switch to the
new root. To avoid decrementing to 0 while the old root still has active
references, we add the lower-addressed node's refcount to the new root.
digraph {
A [label="A (refs=5)"]
B [label="B (refs=3)"]
A -> C [dir=back]
A -> C [weight=0.001, color="red"]
C -> A [weight=0.001, color="blue"]
A -> C [style=dashed]
B -> D [dir=back]
B -> D [weight=0.001, color="red"]
D -> B [weight=0.001, color="blue"]
B -> D [style=dashed]
}The total transferred refcount is tracked throughout the operation so it can be
fixed up at the end if retries resulted in over-transfer.
Union
Now the arena with a lower address becomes the new root (A in this case), by
compare-and-exchange the higher address arena's parent_or_count pointer.
digraph {
A [label="A (refs=5)"]
A -> C [dir=back]
A -> C [weight=0.001, color="red"]
C -> A [weight=0.001, color="blue"]
A -> C [style=dashed]
B -> D [dir=back]
B -> D [weight=0.001, color="red"]
D -> B [weight=0.001, color="blue"]
B -> D [style=dashed]
A -> B [dir=back]
}This atomically eliminates B's refcount and makes it one of A's children. Any
change to B's refcount (or fusing it to a different arena with a lower address)
will make the swap fail, and the process starts again from the beginning.
Refcount fixups
If B's refcount changed while performing the union, we increase or decrease A's
refcount by the difference between the initial refcount we added and B's final
refcount, observed as part of the compare-and-exchange of B's parent_or_count.
Fuse linked lists
To fuse the linked lists, we need to make A's tail point to B. The root node is
always the start of the linked list, so that the list can't contain cycles. From
A's tail pointer, we traverse until we hit the end (C in this case), and CAS itsnext to B.
digraph {
A [label="A (refs=5)"]
A -> C [dir=back]
A -> C [weight=0.001, color="red"]
C -> A [weight=0.001, color="blue"]
A -> C [style=dashed]
B -> D [dir=back]
B -> D [weight=0.001, color="red"]
D -> B [weight=0.001, color="blue"]
B -> D [style=dashed]
A -> B [dir=back]
C -> B [weight=0.001, color="red"]
}With this, B is now reachable from the root and the final free operation will be
able to see it, traversing A->C->B->D.
Update tail pointer
If we stopped at the previous step, it's easy to see how fusing many arenas
could result in O(n^2) behavior, as we'd spend all of our time traversing the
linked list from the root to find the tail. To avoid this, we update A's tail
pointer to point to B's tail - so the next fuse operation will not have to
traverse C or B.
digraph {
A [label="A (refs=5)"]
A -> C [dir=back]
A -> C [weight=0.001, color="red"]
C -> A [weight=0.001, color="blue"]
A -> D [style=dashed]
B -> D [dir=back]
B -> D [weight=0.001, color="red"]
D -> B [weight=0.001, color="blue"]
B -> D [style=dashed]
A -> B [dir=back]
C -> B [weight=0.001, color="red"]
}Update back links
To make it possible to always traverse the list in both directions forupb_Arena_SpaceAllocated, we need to make the matching back link for our
doubly-linked list. Since we linked C to B, we need to link B to C via B'sprevious_or_tail pointer.
digraph {
A [label="A (refs=5)"]
A -> C [dir=back]
A -> C [weight=0.001, color="red"]
C -> A [weight=0.001, color="blue"]
A -> D [style=dashed]
B -> D [dir=back]
B -> D [weight=0.001, color="red"]
D -> B [weight=0.001, color="blue"]
B -> C [weight=0.001, color="blue"]
A -> B [dir=back]
C -> B [weight=0.001, color="red"]
}One-Way References
Fusion creates a bi-directional lifetime dependency between arenas: fused arenas
will have the same lifetime. In some cases, only a one-way dependency is needed.
If messages in arena A contain pointers to messages in arena B but not the
reverse, then arena B only needs to live at least as long as arena A.
upb_Arena_RefArena(A, B) establishes such a one-way dependency by having arenaA increment B's refcount. When arena A is freed, it releases its reference
on B. This is implemented by allocating a special block in A that holds a
pointer to B; this block is processed during upb_Arena_Free(A). Care is
taken to ensure that these blocks are processed before the memory blocks
containing them are freed from the underlying allocator.
Unlike fusion, upb_Arena_RefArena(A, B) is not thread-safe when called
concurrently with other operations on arena A, but it is thread-safe on B.
It is an error to create cycles of references (e.g., upb_Arena_RefArena(A, B); upb_Arena_RefArena(B, A)), or to create a reference between arenas that are
fused. Since fusion creates a bi-directional dependency, fusing arenas A and B
and calling upb_Arena_RefArena(B, A) would create a cycle A <-> B -> A. In
debug builds, upb checks for such cycles, with the implementation documented
below.
Counting allocated space
Traversing the linked list while the arenas are still live can be tricky, as we
can't necessarily reach our own node via the linked list from the root. Instead,
we scan the linked list in both directions starting from the caller-specified
node. This makes space counting weakly consistent - it's possible to see that A
and B are fused, but not see A's space reflected in SpaceAllocated(B) if the
fuse operation that joined them is still in progress. But counting allocated
space is always consistent with itself and any fully complete fuses - nodes are
only appended or prepended to the list, so starting the traversal at the same
point each time guarantees that future scans see a superset of previous ones.
Referenced arenas are not included in the count of allocated space.
Detecting reference cycles
To help prevent memory leaks from uncollectable arenas, upb checks for cycles in
debug builds when creating a reference or fusing arenas. A cycle can be formed
purely by references (eg. A->B->A) or by a combination of references and
fusions (eg. Fuse(A, B), then RefArena(B, A) creates the cycle A<->B->A).
If such cycles were permitted, the arenas involved could never be freed.
After performing a fusion or adding a reference, the cycle detector runs. This
check is performed after the operation because cycle detection cannot be
performed atomically; if the check was done prior to the fusion or reference, we
could miss a case where two concurrent operations resulted in a cycle. Both
would check for a possible cycle, find none, and then proceed.
Consider arenas with references A->B->C.
digraph {
rankdir=LR;
A; B; C;
A -> B [label="ref", color=blue];
B -> C [label="ref", color=blue];
}If we attempt RefArena(C, A), the call will add ref C->A, then check thatC is not reachable from A.
digraph {
rankdir=LR;
A -> B [label="ref", color=blue];
B -> C [label="ref", color=blue];
C -> A [label="ref", color=blue];
}The traversal will reveal A -> B -> C and trigger an assertion failure.
If instead attempt Fuse(A, C), fusion will occur:
digraph {
rankdir=LR;
node [shape=box, style=rounded];
A -> B [label="ref", color=blue];
B -> C [label="ref", color=blue];
C -> A [label="fuse", dir=both, color=red];
}The traversal will find C <-> A -> B -> C, and trigger an assertion failure.
Cycle detection algorithm
Cycles are detected with a non-memoizing recursive depth-first search (DFS). A
path can traverse fusions bi-directionally and references uni-directionally to
find a cycle that contains at least one unidirectional edge. While this is not
asymptotically optimal as it may traverse the same set of nodes numerous times,
it doesn't require allocating memory, making it less intrusive as a debug-only
check. It also may result in infinite recursion if a cycle is created on one
thread while another thread is performing cycle checks, but that would have
caused an assertion failure anyway.
Fast check for fusion
If a directional ref was added and from and to are already fused
(upb_Arena_IsFused(from, to) returns true), they are mutually reachable, so a
path containing a directed edge exists and the assertion fails.
Traversing fusion group members
To check all references originating from from's fusion group, we must visit
every arena fused with from. Because fusion operations can race with this
check, we cannot rely on traversing from the fusion root, which might change.
Instead, like upb_Arena_SpaceAllocated, the algorithm finds members offrom's fusion group by first traversing backwards from from usingprevious_or_tail, then iterates forwards.
Traverse fusion group and DFS on references
From the head of the list segment, we traverse forwards using next to visit
all nodes in from's fusion group. For each arena X in this group, we iterate
through all of its outgoing references (the list of arenas Y such thatRefArena(X, Y) was called). For each such reference X -> Y:
- If
Y == to, thentois directly referenced, so a path exists. We returntrueand eventually trigger an assertion failure. - If
Y != to, we recurse to continue the depth-first search fromY,
finding members of its fusion group and inspecting their outgoing
references. If this recursive call returnstrue, thentois reachable
viaY, and we returntrueto trigger an assertion failure.
If we explore all arenas in the fusion group and all their transitive references
without finding a path to to, no path exists, and we return false.
Upb Design (`docs/upb/design.md`)
upb Design
[TOC]
upb is a protobuf kernel written in C. It is a fast and conformant
implementation of protobuf, with a low-level C API that is designed to be
wrapped in other languages.
upb is not designed to be used by applications directly. The C API is very
low-level, unsafe, and changes frequently. It is important that upb is able to
make breaking API changes as necessary, to avoid taking on technical debt that
would compromise upb's goals of small code size and fast performance.
Design goals
Goals:
- Full protobuf conformance
- Small code size
- Fast performance (without compromising code size)
- Easy to wrap in language runtimes
- Easy to adapt to different memory management schemes (refcounting, GC, etc)
Non-Goals:
- Stable API
- Safe API
- Ergonomic API for applications
Parameters:
- C99
- 32 or 64-bit CPU (assumes 4 or 8 byte pointers)
- Uses pointer tagging, but avoids other implementation-defined behavior
- Aims to never invoke undefined behavior (tests with ASAN, UBSAN, etc)
- No global state, fully re-entrant
Arenas
All memory management in upb uses arenas, using the type upb_Arena. Arenas are
an alternative to malloc() and free() that significantly reduces the costs
of memory allocation.
Arenas obtain blocks of memory using some underlying allocator (likelymalloc() and free()), and satisfy allocations using a simple bump allocator
that walks through each block in linear order. Allocations cannot be freed
individually: it is only possible to free the arena as a whole, which frees all
of the underlying blocks.
Here is an example of using the upb_Arena type:
upb_Arena* arena = upb_Arena_New();
// Perform some allocations.
int* x = upb_Arena_Malloc(arena, sizeof(*x));
int* y = upb_Arena_Malloc(arena, sizeof(*y));
// We cannot free `x` and `y` separately, we can only free the arena
// as a whole.
upb_Arena_Free(arena);upb uses arenas for all memory management, and this fact is reflected in the API
for all upb data structures. All upb functions that allocate take a upb_Arena*
parameter and perform allocations using that arena rather than callingmalloc() or free().
// upb API to create a message.
UPB_API upb_Message* upb_Message_New(const upb_MiniTable* mini_table,
upb_Arena* arena);
void MakeMessage(const upb_MiniTable* mini_table) {
upb_Arena* arena = upb_Arena_New();
// This message is allocated on our arena.
upb_Message* msg = upb_Message_New(mini_table, arena);
// We can free the arena whenever we want, but we cannot free the
// message separately from the arena.
upb_Arena_Free(arena);
// msg is now deleted.
}Arenas are a key part of upb's performance story. Parsing a large protobuf
payload usually involves rapidly creating a series of messages, arrays (repeated
fields), and maps. It is crucial for parsing performance that these allocations
are as fast as possible. Equally important, freeing the tree of messages should
be as fast as possible, and arenas can reduce this cost from O(n) to O(lg n).
Avoiding Dangling Pointers
Objects allocated on an arena will frequently contain pointers to other
arena-allocated objects. For example, a upb_Message will have pointers to
sub-messages that are also arena-allocated.
Unlike unique ownership schemes (such as unique_ptr<>), arenas cannot provide
automatic safety from dangling pointers. Instead, upb provides tools to help
bridge between higher-level memory management schemes (GC, refcounting, RAII,
borrow checkers) and arenas.
If there is only one arena, dangling pointers within the arena are impossible,
because all objects are freed at the same time. This is the simplest case. The
user must still be careful not to keep dangling pointers that point at arena
memory after it has been freed, but dangling pointers between the arena
objects will be impossible.
But what if there are multiple arenas? If we have a pointer from one arena to
another, how do we ensure that this will not become a dangling pointer?
To help with the multiple arena case, upb provides a primitive called "fuse".
// Fuses the lifetimes of `a` and `b`. None of the blocks from `a` or `b`
// will be freed until both arenas are freed.
UPB_API bool upb_Arena_Fuse(const upb_Arena* a, const upb_Arena* b);When two arenas are fused together, their lifetimes are irreversibly joined,
such that none of the arena blocks in either arena will be freed until both
arenas are freed with upb_Arena_Free(). This means that dangling pointers
between the two arenas will no longer be possible.
Fuse is useful when joining two messages from separate arenas (making one a
sub-message of the other). Fuse is a relatively cheap operation, on the order of
150ns, and is very nearly O(1) in the number of arenas being fused (the true
complexity is the inverse Ackermann function, which grows extremely slowly).
Each arena does consume some memory, so repeatedly creating and fusing an
additional arena is not free, but the CPU cost of fusing two arenas together is
modest.
Initial Block and Custom Allocators
upb_Arena normally uses malloc() and free() to allocate and return its
underlying blocks. But this default strategy can be customized to support the
needs of a particular language.
The lowest-level function for creating a upb_Arena is:
// Creates an arena from the given initial block (if any -- n may be 0).
// Additional blocks will be allocated from |alloc|. If |alloc| is NULL,
// this is a fixed-size arena and cannot grow.
UPB_API upb_Arena* upb_Arena_Init(void* mem, size_t n, upb_alloc* alloc);The buffer [mem, n] will be used as an "initial block", which is used to
satisfy allocations before calling any underlying allocation function. Note that
the upb_Arena itself will be allocated from the initial block if possible, so
the amount of memory available for allocation from the arena will be less thann.
The alloc parameter specifies a custom memory allocation function which will
be used once the initial block is exhausted. The user can pass NULL as the
allocation function, in which case the initial block is the only memory
available in the arena. This can allow upb to be used even in situations where
there is no heap.
It follows that upb_Arena_Malloc() is a fallible operation, and all allocating
operations like upb_Message_New() should be checked for failure if there is
any possibility that a fixed size arena is in use.
Schemas
Nearly all operations in upb require that you have a schema. A protobuf schema
is a data structure that contains all of the message, field, enum, etc.
definitions that are specified in a .proto file. To create, parse, serialize,
or access a message you must have a schema. For this reason, loading a schema is
generally the first thing you must do when you use upb. [^0]
[^0]: This requirement comes from the protobuf wire format itself, which is a
deep insight about the nature of protobuf (or at least the existing wire
format). Unlike JSON, protobuf cannot be parsed or manipulated in a
schema-less way. This is because the binary wire format does not
distinguish between strings and sub-messages, so a generic parser that is
oblivious to the schema is not possible. If a future version of the wire
format did distinguish between these, it could be possible to have a
schema-agnostic data representation, parser, and serializer.
upb has two main data structures that represent a protobuf schema:
- MiniTables are a compact, stripped down version of the schema that
contains only the information necessary for parsing and serializing the
binary wire format. - Reflection contains basically all of the data from a
.protofile,
including the original names of all messages/fields/etc., and all options.
The table below summarizes the main differences between these two:
| MiniTables | Reflection | |
|---|---|---|
| Contains | Field numbers and types | All data in .proto file, |
| : : only : including names of : | ||
| : : : everything : | ||
| Used to parse | binary format | JSON / TextFormat |
| Wire representation | MiniDescriptor | Descriptor |
| Type names | upb_MiniTable, |
upb_MessageDef, |
: : upb_MiniTableField, ... : upb_FieldDef, ... : |
||
| Registry | upb_ExtensionRegistry |
upb_DefPool |
| : : (for extensions) : : |
MiniTables are useful if you only need the binary wire format, because they are
much lighter weight than full reflection.
Reflection is useful if you need to parse JSON or TextFormat, or you need access
to options that were specified in the proto file. Note that reflection also
includes MiniTables, so if you have reflection, you also have MiniTables
available.
MiniTables
MiniTables are represented by a set of data structures with names likeupb_MiniTable (representing a message), upb_MiniTableField,upb_MiniTableFile, etc. Whenever you see one of these types in a function
signature, you know that this particular operation requires a MiniTable. For
example:
// Parses the wire format data in the given buffer `[buf, size]` and writes it
// to the message `msg`, which has the type `mt`.
UPB_API upb_DecodeStatus upb_Decode(const char* buf, size_t size,
upb_Message* msg, const upb_MiniTable* mt,
const upb_ExtensionRegistry* extreg,
int options, upb_Arena* arena);The subset of upb that requires only MiniTables can be thought of as "upb lite,"
because both the code size and the runtime memory overhead will be less than
"upb full" (the parts that use reflection).
Loading
There are three main ways of loading a MiniTable:
- From C generated code: The upb code generator can emit
.upb_minitable.cfiles that contain the MiniTables as global constant
variables. When the main program links against these, the MiniTable will be
placed into.rodata(or.data.rel.ro) in the binary. The MiniTable can
then be obtained from a generated function. In Blaze/Bazel these files can
be generated and linked using theupb_minitable_proto_library()rule. - From MiniDescriptors: The user can build MiniDescriptors into MiniTables
at runtime. MiniDescriptors are a compact upb-specific wire format designed
specially for this purpose. The user can callupb_MiniTable_Build()at
runtime to convert MiniDescriptors to MiniTables. - From reflection: If you have already built reflection data structures
for your type, then you can obtain theupb_MiniTablecorresponding to aupb_MessageDefusingupb_MessageDef_MiniTable().
For languages that are already using reflection, (3) is an obvious choice.
For languages that are avoiding reflection, here is a general guideline for
choosing between (1) and (2): if the language being wrapped participates in the
standard binary linking model on a given platform (in particular, if it is
generally linked using ld), then it is better to use (1), which is also known
as "static loading".
Static loading of MiniTables has the benefit of requiring no runtime
initialization[^2], leading to faster startup. Static loading of MiniTables also
facilitates cross-language sharing of proto messages, because sharing generally
requires that both languages are using exactly the same MiniTables.
The main downside of static loading is that it requires the user to generate one.upb.c file per .proto and link against the transitive closure of .upb.c
files. Blaze and Bazel make this reasonably easy, but for other build systems it
can be more of a challenge.
[^2]: aside from possible pointer relocations performed by the ELF/Mach-O loader
if the library or binary is position-independent
Loading from MiniDescriptors, as in option (2), has the advantage that it does
not require per-message linking of C code. For many language toolchains,
generating and linking some custom C code for every protobuf file or message
type would be a burdensome requirement. MiniDescriptors are a convenient way of
loading MiniTables without needing to cross the FFI boundary outside the core
runtime.
A common pattern when using dynamic loading is to embed strings containing
MiniDescriptors directly into generated code. For example, the generated code in
Dart for a message with only primitive fields currently looks something like:
const desc = r'$(+),*-#$%&! /10';
_accessor = $pb.instance.registry.newMessageAccessor(desc);The implementation of newMessageAccessor() is mainly just a wrapper aroundupb_MiniTable_Build(), which builds a MiniTable from a MiniDescriptor. In the
code generator, the MiniDescriptor can be obtained from theupb_MessageDef_MiniDescriptorEncode() API; users should never need to encode a
MiniDescriptor manually.
Linking
When building MiniTables dynamically, it is the user's responsibility to link
each message to the to the appropriate sub-messages and or enums. Each message
must have its message and closed enum fields linked usingupb_MiniTable_SetSubMessage() and upb_MiniTable_SetSubEnum(), respectively.
A higher-level function that links all fields at the same time is also
available, as upb_MiniTable_Link(). This function pairs well withupb_MiniTable_GetSubList() which can be used in a code generator to get a list
of all the messages and enums which must be passed to upb_MiniTable_Link().
A common pattern is to embed the link() calls directly into the generated
code. For example, here is an example from Dart of building a MiniTable that
contains sub-messages and enums:
const desc = r'$3334';
_accessor = $pb.instance.registry.newMessageAccessor(desc);
_accessor!.link(
[
M2.$_accessor,
M3.$_accessor,
M4.$_accessor,
],
[
E.$_accessor,
],
);In this case, upb_MiniTable_GetSubList() was used in the code generator to
discover the 3 sub-message fields and 1 sub-enum field that require linking. At
runtime, these lists of MiniTables are passed to the link() function, which
will internally call upb_MiniTable_Link().
Note that in some cases, the application may choose to delay or even skip the
registration of sub-message types, as part of a tree shaking strategy.
When using static MiniTables, a manual link step is not necessary, as linking is
performed automatically by ld.
Enums
MiniTables primarily carry data about messages, fields, and extensions. However
for closed enums, we must also have a upb_MiniTableEnum structure that stores
the set of all numbers that are defined in the enum. This is because closed
enums have the unfortunate behavior of putting unknown enum values into the
unknown field set.
Over time, closed enums will hopefully be phased out via editions, and the
relevance and overhead of upb_MiniTableEnum will shrink and eventually
disappear.
Reflection
Reflection uses types like upb_MessageDef and upb_FieldDef to represent the
full contents of a .proto file at runtime. These types are upb's direct
equivalents of google::protobuf::Descriptor, google::protobuf::FieldDescriptor, etc. [^1]
[^1]: upb consistently uses Def where C++ would use Descriptor in type
names. This introduces divergence with C++; the rationale was to conserve
horizontal line length, as Def is less than 1/3 the length of
Descriptor. This is more relevant in C, where the type name is repeated
in every function, eg. upb_FieldDef_Name() vs.
upb_FieldDescriptor_Name().
Whenever you see one of these types in a function signature, you know that the
given operation requires reflection. For example:
// Parses JSON format into a message object, using reflection.
UPB_API bool upb_JsonDecode(const char* buf, size_t size, upb_Message* msg,
const upb_MessageDef* m, const upb_DefPool* symtab,
int options, upb_Arena* arena, upb_Status* status);The part of upb that requires reflection can be thought of as "upb full." These
parts of the library cannot be used if a given application has only loaded
MiniTables. There is no way to convert a MiniTable into reflection.
The upb_DefPool type is the top-level container that builds and owns some set
of defs. This type is a close analogue of google::protobuf::DescriptorPool in C++. The
user must always ensure that the upb_DefPool outlives any def objects that it
owns.
Loading
As with MiniTable loading, we have multiple options for how to load full
reflection:
- From C generated code: The upb code generator can create
foo.upbdefs.c
files that embed the descriptors and exports generated C functions for
adding them to a user-providedupb_DefPool. - From descriptors: The user can make manual calls to
upb_DefPool_AddFile(), using descriptors obtained at runtime. Defs for
individual messages can then be obtained usingupb_DefPool_FindMessageByName().
Unlike MiniTables, loading from generated code requires runtime initialization,
as reflection data structures like upb_MessageDef are not capable of being
emitted directly into .rodata like upb_MiniTable is. Instead, the generated
code embeds serialized descriptor protos into .rodata which are then built
into heap objects at runtime.
From this you might conclude that option (1) is nothing but a convenience
wrapper around option (2), but that is not quite correct either. Option (1)
does link against the static .upb.c structures for the MiniTables, whereas
option (2) will build the MiniTables from scratch on the heap. So option (1)
will use marginally less CPU and RAM when the descriptors are loaded into aupb_DefPool. More importantly, the resulting descriptors will be capable of
reflecting over any messages built from the generated .upb.c MiniTables,
whereas descriptors built using option (2) will have distinct MiniTables that
cannot reflect over messages that use the generated MiniTables.
A common pattern for dynamic languages like PHP, Ruby, or Python, is to use
option (2) with descriptors that are embedded into the generated code. For
example, the generated code in Python currently looks something like:
from google.protobuf import descriptor_pool as _descriptor_pool
from google.protobuf.internal import builder as _builder
_desc = b'\n\x1aprotoc_explorer/main.proto\x12\x03pkg'
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(_desc)
_globals = globals()
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google3.protoc_explorer.main_pb2', _globals)The AddSerializedFile() API above is mainly just a thin wrapper aroundupb_DefPool_AddFile().
Upb Style Guide (`docs/upb/style-guide.md`)
C style guide
Since upb is written in pure C, we supplement the
Google C++ style guide with
some C-specific guidance.
Everything written here is intended to follow the spirit of the C++ style guide.
upb is currently inconsistent about following these conventions. It is intended
that all code will be updated to match these guidelines. The priority is
converting public interfaces as these are more difficult to change later.
Naming
Functions and Types
C does not have namespaces. Anywhere you would normally use a namespace
separator (::) in C++, we use an underscore (_) in C:
// C equivalent for upb::Arena::New()
upb_Arena* upb_Arena_New();Since we rely on _ to be our namespace separator, we never use it to merely
separate words in function or type names:
// BAD: this would be interpreted as upb::FieldDef::has::default().
bool upb_FieldDef_has_default(const upb_FieldDef* f);// GOOD: this is equivalent to upb::FieldDef::HasDefault().
bool upb_FieldDef_HasDefault(const upb_FieldDef* f);For multi-word namespaces, we use PascalCase:
// `PyUpb` is the namespace.
PyObject* PyUpb_CMessage_GetAttr(PyObject* _self, PyObject* attr);Private Functions and Members
Since we do not have private in C, we use the UPB_PRIVATE() macro to mark
internal functions and variables that should only be accessed from upb:
// Internal-only function.
int64_t UPB_PRIVATE(upb_Int64_FromLL)();
// Internal-only members. Underscore prefixes are only necessary when the
// structure is defined in a header file.
typedef struct {
const int32_t* UPB_PRIVATE(values);
uint64_t UPB_PRIVATE(mask);
int UPB_PRIVATE(value_count);
} upb_MiniTableEnum;
// Using these members in an internal function.
int upb_SomeFunction(const upb_MiniTableEnum* e) {
return e->UPB_PRIVATE(value_count);
}This prevents users from accessing these symbols, because UPB_PRIVATE()
is defined in port_def.inc and undefined in port_undef.inc, and these
textual headers are not accessible by users.
It is only necessary to use UPB_PRIVATE() for things defined in headers.
For symbols in .c files, it is sufficient to mark private functions withstatic.
Upb Vs Cpp Protos (`docs/upb/vs-cpp-protos.md`)
upb vs. C++ Protobuf Design
upb is a small C
protobuf library. While some of the design follows in the footsteps of the C++
Protobuf Library, upb departs from C++'s design in several key ways. This
document compares and contrasts the two libraries on several design points.
Design Goals
Before we begin, it is worth calling out that upb and C++ have different design
goals, and this motivates some of the differences we will see.
C++ protobuf is a user-level library: it is designed to be used directly by C++
applications. These applications will expect a full-featured C++ API surface
that uses C++ idioms. The C++ library is also willing to add features to
increase server performance, even if these features would add size or complexity
to the library. Because C++ protobuf is a user-level library, API stability is
of utmost importance: breaking API changes are rare and carefully managed when
they do occur. The focus on C++ also means that ABI compatibility with C is not
a priority.
upb, on the other hand, is designed primarily to be wrapped by other languages.
It is a C protobuf kernel that forms the basis on which a user-level protobuf
library can be built. This means we prefer to keep the API surface as small and
orthogonal as possible. While upb supports all protobuf features required for
full conformance, upb prioritizes simplicity and small code size, and avoids
adding features like lazy fields that can accelerate some use cases but at great
cost in terms of complexity. As upb is not aimed directly at users, there is
much more freedom to make API-breaking changes when necessary, which helps the
core to stay small and simple. We want to be compatible with all FFI
interfaces, so C ABI compatibility is a must.
Despite these differences, C++ protos and upb offer roughly the same core set
of
features.
Arenas
upb and C++ protos both offer arena allocation, but there are some key
differences.
C++
As a matter of history, when C++ protos were open-sourced in 2008, they did not
support arenas. Originally there was only unique ownership, whereby each
message uniquely owns all child messages and will free them when the parent is
freed.
Arena allocation was added as a feature in 2014 as a way of dramatically
reducing allocation and (especially) deallocation costs. But the library was
not at liberty to remove the unique ownership model, because it would break far
too many users. As a result, C++ has supported a hybrid allocation model
ever since, allowing users to allocate messages either directly from the
stack/heap or from an arena. The library attempts to ensure that there are
no dangling pointers by performing automatic copies in some cases (for examplea->set_allocated_b(b), where a and b are on different arenas).
C++'s arena object itself google::protobuf::Arena is thread-safe by
design, which allows users to allocate from multiple threads simultaneously
without external synchronization. The user can supply an initial block of
memory to the arena, and can choose some parameters to control the arena block
size. The user can also supply block alloc/dealloc functions, but the alloc
function is expected to always return some memory. The C++ library in general
does not attempt to handle out of memory conditions.
upb
upb uses arena allocation exclusively. All messages must be allocated from
an arena, and can only be freed by freeing the arena. It is entirely the user's
responsibility to ensure that there are no dangling pointers: when a user sets a
message field, this will always trivially overwrite the pointer and will never
perform an implicit copy.
upb's upb::Arena is thread-compatible, which means it cannot be used
concurrently without synchronization. The arena can be seeded with an initial
block of memory, but it does not explicitly support any parameters for choosing
block size. It supports a custom alloc/dealloc function, and this function is
allowed to return NULL if no dynamic memory is available. This allows upb
arenas to have a max/fixed size, and makes it possible in theory to write code
that is tolerant to out-of-memory errors.
upb's arena also supports a novel operation known as fuse, which joins two
arenas together into a single lifetime. Though both arenas must still be freed
separately, none of the memory will actually be freed until both arenas have
been freed. This is useful for avoiding dangling pointers when reparenting a
message with one that may be on a different arena.
Comparison
hybrid allocation vs. arena-only
- The C++ hybrid allocation model introduces a great deal of complexity and
unpredictability into the library. upb benefits from having a much simpler
and more predictable design. - Some of the complexity in C++'s hybrid model arises from the fact that arenas
were added after the fact. Designing for a hybrid model from the outset
would likely yield a simpler result. - Unique ownership does support some usage patterns that arenas cannot directly
accommodate. For example, you can reparent a message and the child will precisely
follow the lifetime of its new parent. An arena would require you to either
perform a deep copy or extend the lifetime.
thread-compatible vs. thread-safe arena
- A thread-safe arena (as in C++) is safer and easier to use. A thread-compatible
arena requires that the user prove that the arena cannot be used concurrently. - Thread Sanitizer
is far more accessible than it was in 2014 (when C++ introduced a thread-safe
arena). We now have more tools at our disposal to ensure that we do not trigger
data races in a thread-compatible arena like upb. - Thread-compatible arenas are more performant.
- Thread-compatible arenas have a far simpler implementation. The C++ thread-safe
arena relies on thread-local variables, which introduce complications on some
platforms. It also requires far more subtle reasoning for correctness and
performance.
fuse vs. no fuse
- The
upb_Arena_Fuse()operation is a key part of how upb supports reparenting
of messages when the parent may be on a different arena. Without this, upb has
no way of supportingfoo.bar = barin dynamic languages without performing a
deep copy. - A downside of
upb_Arena_Fuse()is that passing an arena to a function can allow
that function to extend the lifetime of the arena in potentially
unpredictable ways. This can be prevented if necessary, as fuse can fail, eg. if
one arena has an initial block. But this adds some complexity by requiring callers
to handle the case where fuse fails.
Code Generation vs. Tables
The C++ protobuf library has always been built around code generation, while upb
generates only tables. In other words, foo.pb.cc files contain functions,
whereas foo.upb.c files emit only data structures.
C++
C++ generated code emits a large number of functions into foo.pb.cc files.
An incomplete list:
FooMsg::FooMsg()(constructor): initializes all fields to their default value.FooMsg::~FooMsg()(destructor): frees any present child messages.FooMsg::Clear(): clears all fields back to their default/empty value.FooMsg::_InternalParse(): generated code for parsing a message.FooMsg::_InternalSerialize(): generated code for serializing a message.FooMsg::ByteSizeLong(): calculates serialized size, as a first pass before serializing.FooMsg::MergeFrom(): copies/appends present fields from another message.FooMsg::IsInitialized(): checks whether required fields are set.
This code lives in the .text section and contains function calls to the generated
classes for child messages.
upb
upb does not generate any code into foo.upb.c files, only data structures. upb uses a
compact data table known as a mini table to represent the schema and all fields.
upb uses mini tables to perform all of the operations that would traditionally be done
with generated code. Revisiting the list from the previous section:
FooMsg::FooMsg()(constructor): upb instead initializes all messages withmemset(msg, 0, size).
Non-zero defaults are injected in the accessors.FooMsg::~FooMsg()(destructor): upb messages are freed by freeing the arena.FooMsg::Clear(): can be performed withmemset(msg, 0, size).FooMsg::_InternalParse(): upb's parser uses mini tables as data, instead of generating code.FooMsg::_InternalSerialize(): upb's serializer also uses mini-tables instead of generated code.FooMsg::ByteSizeLong(): upb performs serialization in reverse so that an initial pass is not required.FooMsg::MergeFrom(): upb supports this via serialize+parse from the other message.FooMsg::IsInitialized(): upb's encoder and decoder have special flags to check for required fields.
A util libraryupb/util/required_fields.hhandles the corner cases.
Comparison
If we compare compiled code size, upb is far smaller. Here is a comparison of the code
size of a trivial binary that does nothing but a parse and serialize of descriptor.proto.
This means we are seeing both the overhead of the core library itself as well as the
generated code (or table) for descriptor.proto. (For extra clarity we should break this
down by generated code vs core library in the future).
| Library | .text |
.data |
.bss |
|---|---|---|---|
| upb | 26Ki | 0.6Ki | 0.01Ki |
| C++ (lite) | 187Ki | 2.8Ki | 1.25Ki |
| C++ (code size) | 904Ki | 6.1Ki | 1.88Ki |
| C++ (full) | 983Ki | 6.1Ki | 1.88Ki |
"C++ (code size)" refers to protos compiled with optimize_for = CODE_SIZE, a mode
in which generated code contains reflection only, in an attempt to make the
generated code size smaller (however it requires the full runtime instead
of the lite runtime).
Bifurcated vs. Optional Reflection
upb and C++ protos both offer reflection without making it mandatory. However
the models for enabling/disabling reflection are very different.
C++
C++ messages offer full reflection by default. Messages in C++ generally
derive from Message, and the base class provides a member functionReflection* Message::GetReflection() which returns the reflection object.
It follows that any message deriving from Message will always have reflection
linked into the binary, whether or not the reflection object is ever used.
Because GetReflection() is a function on the base class, it is not possible
to statically determine if a given message's reflection is used:
Reflection* GetReflection(const Message& message) {
// Can refer to any message in the whole binary.
return message.GetReflection();
}The C++ library does provide a way of omitting reflection: MessageLite. We can
cause a message to be lite in two different ways:
optimize_for = LITE_RUNTIMEin a.protofile will cause all messages in that
file to be lite.liteas a codegen param: this will force all messages to lite, even if the.protofile does not haveoptimize_for = LITE_RUNTIME.
A lite message will derive from MessageLite instead of Message. SinceMessageLite has no GetReflection() function, this means no reflection is
available, so we can avoid taking the code size hit.
upb
upb does not have the Message vs. MessageLite bifurcation. There is only one
kind of message type upb_Message, which means there is no need to configure in
a .proto file which messages will need reflection and which will not.
Every message has the option to link in reflection from a separate foo.upbdefs.o
file, without needing to change the message itself in any way.
upb does not provide the equivalent of Message::GetReflection(): there is no
facility for retrieving the reflection of a message whose type is not known statically.
It would be possible to layer such a facility on top of the upb core, though this
would probably require some kind of code generation.
Comparison
- Most messages in C++ will not bother to declare themselves as "lite". This means
that many C++ messages will link in reflection even when it is never used, bloating
binaries unnecessarily. optimize_for = LITE_RUNTIMEis difficult to use in practice, because it prevents
any non-lite protos fromimporting that file.- Forcing all protos to lite via a codegen parameter (for example, when building for
mobile) is more practical thanoptimize_for = LITE_RUNTIME. But this will break
the compile for any code that tries to upcast toMessage, or tries to use a
non-lite method. - The one major advantage of the C++ model is that it can support
msg.DebugString()
on a type-erased proto. For upb you have to explicitly pass theupb_MessageDef*
separately if you want to perform an operation like printing a proto to text format.
Explicit Registration vs. Globals
TODO
Upb Wrapping Upb (`docs/upb/wrapping-upb.md`)
Building a protobuf library on upb
This is a guide for creating a new protobuf implementation based on upb. It
starts from the beginning and walks you through the process, highlighting
some important design choices you will need to make.
Overview
A protobuf implementation consists of two main pieces:
- a code generator, run at compile time, to turn
.protofiles into source
files in your language (we will call this "zlang", assuming an extension of ".z"). - a runtime component, which implements the wire format and provides the data
structures for representing protobuf data and metadata.
digraph {
rankdir=LR;
newrank=true;
node [style="rounded,filled" shape=box]
"foo.proto" -> protoc;
"foo.proto" [shape=folder];
protoc [fillcolor=lightgrey];
protoc -> "protoc-gen-zlang";
"protoc-gen-zlang" -> "foo.z";
"protoc-gen-zlang" [fillcolor=palegreen3];
"foo.z" [shape=folder];
labelloc="b";
label="Compile Time";
}digraph {
newrank=true;
node [style="rounded,filled" shape=box fillcolor=lightgrey]
"foo.z" -> "zlang/upb glue (FFI)";
"zlang/upb glue (FFI)" -> "upb (C)";
"zlang/upb glue (FFI)" [fillcolor=palegreen3];
labelloc="b";
label="Runtime";
}The parts in green are what you will need to implement.
Note that your code generator (protoc-gen-zlang) does not need to generate
any C code (eg. foo.c). While upb itself is written in C, upb's parsers and
serializers are fully table-driven, which means there is never any need or even
benefit to generating C code for each proto. upb is capable of full-speed
parsing even when schema data is loaded at runtime from strings embedded intofoo.z. This is a key benefit of upb compared with C++ protos, which have
traditionally relied on generated parsers in foo.pb.cc files to achieve full
parsing speed, and suffered a ~10x speed penalty in the parser when the schema
data was loaded at runtime.
Prerequisites
There are a few things that the language runtime must provide in order to wrap
upb.
- FFI: To wrap upb, your language must be able to call into a C API
through a Foreign Function Interface (FFI). Most languages support FFI in
some form, either through "native extensions" (in which you write some C
code to implement new methods in your language) or through a direct FFI (in
which you can call into regular C functions directly from your language
using a special library). - Finalizers, Destructors, or Cleaners: The runtime must provide
finalizers or destructors of some sort. There must be a way of triggering a
call to a C function when the language garbage collects or otherwise
destroys an object. We don't care much whether it is a finalizer, a
destructor, or a cleaner, as long as it gets called eventually when the
object is destroyed. upb allocates memory in C space, and a finalizer is our
only way of making sure that memory is freed and does not leak. - HashMap with weak values: (optional) This is not a strong requirement,
but it is sometimes helpful to have a global hashmap with weak values to act
as aupb_msg* -> wrapperobject cache. We want the values to be weak (not
the keys). There is some question about whether we want to continue to use
this pattern going forward.
Reflection vs. MiniTables
The first key design decision you will need to make is whether your generated
code will access message data via reflection or minitables. Generally more
dynamic languages will want to use reflection and more static languages will
want to use minitables.
Reflection
Reflection-based data access makes the most sense in highly dynamic language
interpreters, where method dispatch is generally resolved via strings and hash
table lookups.
In such languages, you can often implement a special method like __getattr__
(Python) or method_missing (Ruby) that receives the method name as a string.
Using upb's reflection, you can look up a field name using the method name,
thereby using a hash table belonging to upb instead of one provided by the
language.
class FooMessage:
# Written in Python for illustration, but in practice we will want to
# implement this in C for speed.
def __getattr__(self, name):
field = FooMessage.descriptor.fields_by_name[name]
return field.get_value(self)Using this design, we only need to attach a single __getattr__ method to each
message class, instead of defining a getter/setter for each field. In this way
we can avoid duplicating hash tables between upb and the language interpreter,
reducing memory usage.
Reflection-based access requires loading full reflection at runtime. Your
generated code will need to embed serialized descriptors (ie. a serialized
message of descriptor.proto), which has some amount of size overhead and
exposes all message/field names to the binary. It also forces a hash table
lookup in the critical path of field access. If method calls in your language
already have this overhead, then this is no added burden, but for statically
dispatched languages it would cause extra overhead.
If we take this path to its logical conclusion, all class creation can be
performed fully dynamically, using only a binary descriptor as input. The
"generated code" becomes little more than an embedded descriptor plus a
library call to load it. Python has recently gone down this path. Generated
code now looks something like this:
# main_pb2.py
from google3.net.proto2.python.internal import builder as _builder
from google3.net.proto2.python.public import descriptor_pool as _descriptor_pool
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile("<...>")
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals())
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google3.main_pb2', globals())This is all the runtime needs to create all of the classes for messages defined
in that serialized descriptor. This code has no pretense of readability, but
a separate .pyi stub file provides a fully expanded and readable list of all
methods a user can expect to be available:
# main_pb2.pyi
from google3.net.proto2.python.public import descriptor as _descriptor
from google3.net.proto2.python.public import message as _message
from typing import ClassVar as _ClassVar, Optional as _Optional
DESCRIPTOR: _descriptor.FileDescriptor
class MyMessage(_message.Message):
__slots__ = ["my_field"]
MY_FIELD_FIELD_NUMBER: _ClassVar[int]
my_field: str
def __init__(self, my_field: _Optional[str] = ...) -> None: ...To use reflection-based access:
- Load and access descriptor data using the interfaces in upb/reflection/def.h.
- Access message data using the interfaces in upb/reflection/message.h.
MiniTables
MiniTables are a "lite" schema representation that are much smaller than
reflection. MiniTables omit names, options, and almost everything else from the.proto file, retaining only enough information to parse and serialize binary
format.
MiniTables can be loaded into upb through MiniDescriptors. MiniDescriptors are
a byte-oriented format that can be embedded into your generated code and passed
to upb to construct MiniTables. MiniDescriptors only use printable characters,
and therefore do not require escaping when embedding them into generated code
strings. Overall the size savings of MiniDescriptors are ~60x compared with
regular descriptors.
MiniTables and MiniDescriptors are a natural choice for compiled languages that
resolve method calls at compile time. For languages that are sometimes compiled
and sometimes interpreted, there might not be an obvious choice. When a method
call is statically bound, we want to remove as much overhead as possible,
especially from accessors. In the extreme case, we can use unsafe APIs to read
raw memory at a known offset:
// Example of a maximally-optimized generated accessor.
class FooMessage {
public long getBarField() {
// Using Unsafe should give us performance that is comparable to a
// native member access.
//
// The constant "24" is obtained from upb at compile time.
sun.misc.Unsafe.getLong(this.ptr, 24);
}
}This design is very low-level, and tightly couples the generated code to one
specific version of the schema and compiler. A slower but safer version would
look up a field by field number:
// Example of a more loosely-coupled accessor.
class FooMessage {
public long getBarField() {
// The constant "2" is the field number. Internally this will look
// up the number "2" in the MiniTable and use that to read the value
// from the message.
upb.glue.getLong(this.ptr, 2);
}
}One downside of MiniTables is that they cannot support parsing or serializing
to JSON or TextFormat, because they do not know the field names. It should be
possible to generate reflection data "on the side", into separate generated
code files, so that reflection is only pulled in if it is being used. However
APIs to do this do not exist yet.
To use MiniTable-based access:
- Load and access MiniDescriptors data using the interfaces in upb/mini_descriptor/decode.h.
- Access message data using the interfaces in upb/message/accessors.h.
Memory Management
One of the core design challenges when wrapping upb is memory management. Every
language runtime will have some memory management system, whether it is
garbage collection, reference counting, manual memory management, or some hybrid
of these. upb is written in C and uses arenas for memory management, but upb is
designed to integrate with a wide variety of memory management schemes, and it
provides a number of tools for making this integration as smooth as possible.
Arenas
upb defines data structures in C to represent messages, arrays (repeated
fields), and maps. A protobuf message is a hierarchical tree of these objects.
For example, a relatively simple protobuf tree might look something like this:
digraph G {
rankdir=LR;
newrank=true;
node [style="rounded,filled" shape=box colorscheme=accent8 fillcolor=1, ordering=out]
upb_msg -> upb_msg2;
upb_msg -> upb_array;
upb_msg [label="upb Message" fillcolor=1]
upb_msg2 [label="upb Message"];
upb_array [label="upb Array"]
}All upb objects are allocated from an arena. An arena lets you allocate objects
individually, but you cannot free individual objects; you can only free the arena
as a whole. When the arena is freed, all of the individual objects allocated
from that arena are freed together.
digraph G {
rankdir=LR;
newrank=true;
subgraph cluster_0 {
label = "upb Arena"
graph[style="rounded,filled" fillcolor=gray]
node [style="rounded,filled" shape=box colorscheme=accent8 fillcolor=1, ordering=out]
upb_msg -> upb_array;
upb_msg -> upb_msg2;
upb_msg [label="upb Message" fillcolor=1]
upb_msg2 [label="upb Message"];
upb_array [label="upb Array"];
}
}In simple cases, the entire tree of objects will all live in a single arena.
This has the nice property that there cannot be any dangling pointers between
objects, since all objects are freed at the same time.
However upb allows you to create links between any two objects, whether or
not they are in the same arena. The library does not know or care what arenas
the objects are in when you create links between them.
digraph G {
rankdir=LR;
newrank=true;
subgraph cluster_0 {
label = "upb Arena 1"
graph[style="rounded,filled" fillcolor=gray]
node [style="rounded,filled" shape=box colorscheme=accent8 fillcolor=1, ordering=out]
upb_msg -> upb_array;
upb_msg -> upb_msg2;
upb_msg [label="upb Message 1" fillcolor=1]
upb_msg2 [label="upb Message 2"];
upb_array [label="upb Array"];
}
subgraph cluster_1 {
label = "upb Arena 2"
graph[style="rounded,filled" fillcolor=gray]
node [style="rounded,filled" shape=box colorscheme=accent8 fillcolor=1]
upb_msg3;
}
upb_msg2 -> upb_msg3;
upb_msg3 [label="upb Message 3"];
}When objects are on separate arenas, it is the user's responsibility to ensure
that there are no dangling pointers. In the example above, this means Arena 2
must outlive Message 1 and Message 2.
Integrating GC with upb
In languages with automatic memory management, the goal is to handle all of the
arenas behind the scenes, so that the user does not have to manage them manually
or even know that they exist.
We can achieve this goal if we set up the object graph in a particular way. The
general strategy is to create wrapper objects around all of the C objects,
including the arena. Our key goal is to make sure the arena wrapper is not
GC'd until all of the C objects in that arena have become unreachable.
For this example, we will assume we are wrapping upb in Python:
/* Detailed source-code truncated for AI context efficiency. */In this example we have three different kinds of pointers:
- raw ptr: This is a pointer that carries no ownership.
- unique ptr: This is a pointer has unique ownership of the target. The owner
will free the target in its destructor (or finalizer, or cleaner). There can
only be a single unique pointer to a given object. - shared (GC) ptr: This is a pointer that has shared ownership of the
target. Many objects can point to the target, and the target will be deleted
only when all such references are gone. In a runtime with automatic memory
management (GC), this is a reference that participates in GC. In Python such
references use reference counting, but in other VMs they may use mark and
sweep or some other form of GC instead.
The Python Message wrappers have only raw pointers to the underlying message,
but they contain a shared pointer to the arena that will ensure that the raw
pointer remains valid. Only when all message wrapper objects are destroyed
will the Python Arena become unreachable, and the upb arena ultimately freed.
Links between arenas with "Fuse"
The design given above works well for objects that live in a single arena. But
what if a user wants to create a link between two objects in different arenas?
TODO
UTF-8 vs. UTF-16
TODO
Object Cache
TODO
Python Docs Environment.Yml (`python/docs/environment.yml`)
name: protobuf
channels:
- defaults
dependencies: - libprotobuf=3.11.4
- make=4.2.1
- pip=19.3.1
- python=3.7.6
- sphinx=2.4.0
- sphinx_rtd_theme=0.4.3
Python Docs Index (`python/docs/index.rst`)
.. Protocol Buffers documentation master file, created by
sphinx-quickstart on Thu Aug 15 13:56:43 2019.
You can adapt this file completely to your liking, but it should at least
contain the root toctree directive.
.. ifconfig:: build_env == 'readthedocs'
.. warning::
You are reading the documentation for the `latest committed changes
<https://github.com/protocolbuffers/protobuf/tree/master/python>`_ of
the `Protocol Buffers package for Python
<https://developers.google.com/protocol-buffers/docs/pythontutorial>`_.
Some features may not yet be released. Read the documentation for the
latest released package at `googleapis.dev
<https://googleapis.dev/python/protobuf/latest/>`_.Protocol Buffers Python API Reference
The complete documentation for Protocol Buffers is available via the web at:
https://developers.google.com/protocol-buffers/
Modules and Packages
.. START REFTOC, generated by generate_docs.py.
.. toctree::
google/protobuf
google/protobuf/any_pb2
google/protobuf/descriptor
google/protobuf/descriptor_database
google/protobuf/descriptor_pb2
google/protobuf/descriptor_pool
google/protobuf/duration_pb2
google/protobuf/empty_pb2
google/protobuf/field_mask_pb2
google/protobuf/internal/containers
google/protobuf/json_format
google/protobuf/message
google/protobuf/message_factory
google/protobuf/proto_builder
google/protobuf/reflection
google/protobuf/service
google/protobuf/service_reflection
google/protobuf/struct_pb2
google/protobuf/symbol_database
google/protobuf/text_encoding
google/protobuf/text_format
google/protobuf/timestamp_pb2
google/protobuf/type_pb2
google/protobuf/unknown_fields
google/protobuf/wrappers_pb2
.. END REFTOC.
Indices and tables
- :ref:
genindex - :ref:
modindex
Python Docs Requirements (`python/docs/requirements.txt`)
googleapis-common-protos==1.56.1
jinja2==3.1.6
sphinx==3.0.4
sphinx_rtd_theme==0.4.3
sphinxcontrib-napoleon==0.7
Design Editions Cpp Apis For Edition Zero (`docs/design/editions/cpp-apis-for-edition-zero.md`)
C++ APIs for Edition Zero
Author: @mcy
Approved: 2022-06-27
Overview
As recorded in FileDescriptor::syntaxAudit Report (not available externally),
there are significant uses of FileDescriptor::syntax() in internal Google
repositories that Edition Zero will break; for example, existing usage checkssyntax() == PROTO3 to determine if enums are open.
Because we expect to make everything that callers are querying be controlled by
individual edition-gated features, the cleanest way forward is to enrich theDescriptor types with focused APIs that serve current usages, followed by a
migration to them.
Tier 1 proposal:
Proposed API
This proposal adds the following code to descriptor.h:
class FileDescriptor {
// Copies package, syntax, edition, dependencies, and file-level options.
void CopyHeadingTo(FileDescriptorProto*) const;
};
class FieldDescriptor {
// Returns whether this field has a proto3-like zero default value.
bool has_zero_default_value() const;
// Returns whether this is a string field that enforces UTF-8 in the codec.
bool enforces_utf8() const;
};
class EnumDescriptor {
// Returns whether this enum is a proto2-style closed enum.
bool is_closed() const;
};This covers everything not already covered by existing accessors.CopyHeadingTo is intended to simplify the common, easy-to-get-wrong pattern of
copying the heading of a proto file before doing custom manipulation of
descriptors within.
Additionally, we propose generating unknown_fields() andmutable_unknown_fields() for all protos unconditionally.
Migration
In addition to the above functions, we'll use FieldDescriptor::has_presence()
and FieldDescriptor::is_packed() to migrate callers performing comparisons tosyntax(). Users can migrate to the new functions by:
- Searching for calls to
syntax(). - Identifying what proto2/proto3 distinction is being picked up on, among the
following:- UTF-8 verification on parse (use new API).
- Closed/open enum (use new API).
- Packed-ness (use existing API instead of guessing from
syntax). - Whether fields have hasbits (use
has_presence()).
- Migrate to the new API.
The volume of such changes in google3 is small enough that it's probably easiest
to create a giant CL that fixes every instance of a particular misuse and then
hand it to Rosie for splitting up.
Once all the "easy" usages are migrated, we will mark syntax() asABSL_DEPRECATED. syntax() will return a new special value,Syntax::EDITIONS, intended to explicitly break caller expectations about what
this function returns. Virtually all cases not covered by this proposal are
using syntax() to reject one of the two existing syntaxes, or produce an error
when encountering an unknown Syntax value, so they will continue to work as
expected (i.e. fail on editions protos).
A number of existing tools are hostile to proto2 or proto3. Once the migration
plan is approved, we should reach out to them individually to coordinate either
updating or deprecating their tool.
Design Editions Edition Evolution (`docs/design/editions/edition-evolution.md`)
Edition Evolution
Author: @mcy
Approved: 2022-07-06
Overview
Protobuf Editions give us a mechanism for
pulling protobuf files into the future by upgrading their edition. Features
are flags associated with syntax items of a .proto file; they can be either
set explicitly, or they can be implied by the edition. Features can either
correspond to behavior in protoc's frontend (e.g. proto:closed_enums), or to
a specific backend (cpp:string_view).
This document seeks to answer:
- When do we create an edition?
- How do backends inform protoc of their defaults?
Proposal
Total Ordering of Editions
NOTE: This topic is largely superseded by Edition Naming.
The FileDescriptorProto.edition field is a string, so that we can avoid nasty
surprises around needing to mint multiple editions per year: even if we mintedition = "2022";, we can mint edition = "2022b"; in a pinch.
However, we might not own some third-party backends, and they might be unaware
of when we decide to mint editions, and might want to mint editions on their
own. Suppose that I maintain the Haskell protobuf backend, and I decide to add
the haskell:more_monads feature. How do I get this into an edition?
We propose defining a total order on editions. This means that a backend can
pick the default not by looking at the edition, but by asking "is this proto
older than this edition, where I introduced this default?"
The total order is thus: the edition string is split on '.'. Each component is
then ordered by a.len < b.len && a < b. This ensures that 9 < 10,
for example.
By convention, we will make the edition be either the year, like 2022, or the
year followed by a revision, like 2022.1. Thus, we have the following total
ordering on editions:
2022 < 2022.0 < 2022.1 < ... < 2022.9 < 2022.10 < ... < 2023 < ... < 2024 < ...This means that backends (even though we don't particularly recommend it) can
change defaults as often as they like. Thus, if I decide thathaskell:more_monads becomes true in 2023, I simply askfile.EditionIsLaterThan("2023"). If it becomes false in 2023.1, a future
backend can ask file.EditionIsBetween("2023", "2023.1").
Creating an Edition
In a sense, every edition already exists; it's just a matter of defining
features on it.
If the feature is a proto: feature, protoc intrinsically knows it, and it is
implemented in the frontend.
If the feature is a backend feature, the backend must be able to produce some
kind of proto like message Edition { repeated Feature defaults = 1; } that
describes what a specific edition must look like, based on less-than/is-between
predicates like those above. That information can be used by protoc to display
the full set of features it knows about given its backends. (The backend must,
of course, use this information to make relevant codegen decisions.)
What about Editions "From the Future"?
Suppose that in version v5.0 of my Haskell backend I introducedhaskell:more_monads, and this has a runtime component to it; that is, the
feature must be present in the descriptor to be able to handle parsing the
message correctly.
However, suppose I have an older service running v4.2. It is compiled with a
proto that was already edition 2023 at build time (alternatively, it dynamically
loads a proto). My v5.0 client sends it an incompatible message from "the
future". Because a parse failure would be an unacceptable service degradation,
we have a couple of options:
- Editions cannot introduce a feature that requires readers to accept new
encodings.- Similarly, editions cannot add restrictions that constrain past parsers.
- Editions may introduce such features, but they must somehow fit into some
kind of build horizon.
The former is a reasonable-sounding but ultimately unacceptable position, since
it means we cannot use editions if we wanted to, say, make it so that message
fields are encoded as groups rather than length-prefixed chunks. The alternative
is to define some kind of build horizon.
Design Editions Edition Lifetimes (`docs/design/editions/edition-lifetimes.md`)
Edition Lifetimes
Now that Edition Zero is complete, we need to re-evaluate what the lifetimes of
features and editions look like going forward.
Background
The implementation of editions today was based largely on
Protobuf Editions Design: Features and
Life of an Edition (among other less-relevant docs).
Specifically, the latter one takes a strong stance on the lifetimes of both
editions and features. Many of the ideas around editions have since been
simplified in Edition Naming, where we opted for a stricter
naming scheme owned and defined by us. In the process of rolling out editions to
various protoc plugins and planning for edition 2024, it's become clear that we
may need to re-evaluate the feature lifetimes as well.
Editions: Life of a Feature (not available externally) is an alternate vision
to Life of an Edition, which tries to put tighter constraints on how features
and editions interact. It also predicted many of the problems we face now, and
proposes a possible solution.
Overview
Today, features and editions are largely disconnected from each other. We have a
set of features that govern various behaviors, and they can all be used in any
edition. Each edition is simply a distinct set of defaults for every
feature. Users can override the value of any released feature in any edition.
Each generator and protoc itself all advertise a range of editions they support,
and will reject any protos encountered outside that range.
This system does have the nice consequence that behavior-preserving editions
upgrades can always be performed (with respect to the most current proto
language). It separates editions from breaking changes, and means that we only
need to worry about one versioning scheme (our OSS release).
While this all works fine in Edition 2023 where we only have a single edition,
it poses a number of problems going forward. There are three relevant events in
the lifetime of both editions and features, introduction, deprecation, and
removal. Deprecation is essentially just a soft removal to give users adequate
warning, and the same problems apply to both.
Introducing an Edition
Because every generator plugin (and protoc) advertises its edition support
window, introducing a new Edition is well-handled today. We get to enjoy all the
same benefits we saw rolling out Edition 2023 in every subsequent edition (e.g.
we can make radical language changes gated on edition).
Dropping an Edition
Dropping support for an edition doesn't really mean that much today. We could
do it simply by bumping up the minimum supported edition of a binary in a
breaking release. However, that would have no relation to our feature support,
and at best would allow us to clean up some parser code branching on editions.
This code would always be tied to language changes we made in the introduction
of a new edition, where we could finalize them with the removal of an edition.
Introducing a Feature
Whenever we introduce a new feature, we need to make sure to specify its
defaults for every edition (protoc enforces that every edition has a known
set of defaults). It will also immediately be overridable in every edition. This
means that pre-existing binaries that have declared support for an old edition
may suddenly be presented with protos that override a feature they can't
possibly know about.
We faced this problem when introducing the new string_type feature as part of
New String APIs (not available externally). Our solution at the time was to
create some ad hoc validation that prohibited overriding this feature in Edition
2023 until we were ready to release it. This solution doesn't work in general
though, where in OSS we can have arbitrarily old binaries floating around (and
even in google3 we can easily have up to six-month-old binaries within the build
horizon). These old binaries wouldn't have that validation layer, and would
happily process Edition 2023 files with string_type overrides, despite not
knowing how to properly treat that feature.
Dropping a Feature
On the other end of the spectrum, we need a way to deprecate and then remove
support for features. Given that we expect most features to remain for many
years, we haven't been forced to consider this situation too much. The current
plan of record though, is that we would do this by first marking the feature
field definition deprecated, and then remove it entirely in a breaking release
(and total burndown in google3).
The problem with this plan is that it creates a lot of complexity for users
trying to understand our support guarantees. They'll need to track the lifetime
of every feature they use, and also difficult-to-predict interactions
between different versions of protoc and its plugins. If we drop a global
feature in protoc, some plugins may still expect to see that feature and become
broken, while others may not care and still work.
Recommendation
Feature Lifetimes
We recommend adding four new field options to be used in feature specifications:edition_introduced, edition_deprecated, deprecation_warning, andedition_removed. This will allow every feature to specify the edition it was
introduced in, the edition it became deprecated in, when we expect to remove it
(deprecation warnings), and the edition it actually becomes removed in.
We will also add a new special edition EDITION_LEGACY, to act as a placeholder
for "infinite past". For editions earlier than edition_introduced, the default
assigned to EDITION_LEGACY will be assigned and should always signal the noop
behavior that predated the feature. Proto files will not be allowed to override
this feature without upgrading to a newer edition. Deprecated features can get
special treatment beyond the regular deprecated option, and a custom warning
signaling that they should be migrated off of. For editions later thanedition_removed, the last edition default will continue to stay in place, but
overrides will be disallowed in proto files.
For example, a hypothetical feature might look like:
optional FeatureType do_something = 2 [
retention = RETENTION_RUNTIME,
targets = TARGET_TYPE_FIELD,
targets = TARGET_TYPE_FILE,
feature_support {
edition_introduced = EDITION_2023,
edition_deprecated = EDITION_2025,
deprecation_warning = "Feature do_something will be removed in edition 2027",
edition_removed = EDITION_2027,
}
edition_defaults = { edition: EDITION_LEGACY, value: "LEGACY" }
edition_defaults = { edition: EDITION_2023, value: "INTERMEDIATE" }
edition_defaults = { edition: EDITION_2024, value: "FUTURE" }
];Before edition 2023, this feature would always get a default of LEGACY, and
proto files would be prohibited from overriding it. In edition 2023, the default
would change to INTERMEDIATE and users could override it to the old default or
the future behavior. In edition 2024 the default would change again to FUTURE,
and in edition 2025 any overrides of that would start emitting warnings. In
edition 2027 we would prohibit overriding this feature, and the behavior would
always be FUTURE.
Edition Lifetimes
By tying feature lifetimes to specific editions, it gives editions a lot more
meaning. We will still limit this to breaking releases, but it means that
all of the editions-related breaking changes come from this process. When we
drop an edition, breaking changes will always come from the removal of
previously deprecated features. By regularly dropping support for editions, we
will be able to gradually clean up our codebase.
Edition Upgrades
A consequence of this design is that edition upgrades could now become
potentially breaking. Any proto files using deprecated features could be broken
by bumping its edition to one where the feature has been removed. Within
google3, we would need to completely burn down all deprecated uses before we can
remove the feature.
This is not a substantial change on our end from the existing situation though,
where we'd still need to remove all uses before removing it. The key difference
is that we have the option to allowlist some people to stay on an older
edition while still moving the rest of google3 forward. We would also be able to
continue testing removed features by allow-listing dedicated tests to stay on
old editions.
Garbage Collection
Another consequence of this is that we can't actually clean up feature-related
code until every edition before its edition_removed declaration has been
dropped. This ties feature support directly to edition support, especially in
OSS where we can't forcibly upgrade protos to the latest edition.
Predictability
The main win with this strategy is that it clarifies our guarantees and makes
our library more predictable. We can guarantee that a proto file at a specific
edition will not see any behavioral changes unless we:
- Make a breaking change outside the editions framework.
- Drop the edition the proto file uses.
We can also guarantee that as long as users stay away from deprecated features,
they will still be able to upgrade to the next edition without any changes.
Implementation
Fortunately, this design would be very easy to implement right now. We
simply need to add the new field options and the new placeholder edition, and
then implement new validation in protoc. Because the two error conditions (using
a feature outside its existence window) and the warning (using a deprecated
feature) only trigger on overridden features, protoc already has all the
information it needs. Generator feature extensions must be imported to be
overridden, so the problem of protoc not knowing feature defaults doesn't come
into play at all.
If we wait until edition 2024 has been released, the situation would be a bit
more difficult to unravel. Any new features added in 2024 would be usable from
2023, so we'd have to either intentionally backport support or remove all of
those uses before enabling the validation layer. Therefore, the recommendation
is to implement this ASAP, before we start rolling out 2024.
Runtimes with Dynamic Messages
None of the generators where editions have already been rolled out require any
changes. We likely will want to add validation layers to runtimes that support
dynamic messages though, to make sure there are no invalid descriptors floating
around. Since they all have access to protoc's compiled defaults IR, we can pack
as much information in there as possible to minimize duplication. Specifically,
we will add two new FeatureSet fields to FeatureSetEditionDefault in
addition to the existing features field.
- overridable_features - The default values that users are allowed to
override in a given edition - fixed_features - The default values that users are not allowed to
override in a given edition
We will keep the existing features field as a migration tool, to avoid
breaking plugins and runtimes that already use it to calculate defaults. We can
strip it from OSS prior to the 27.0 release though, and remove it once everyone
has been migrated.
In order to calculate the full defaults of any edition, each language will
simply need to merge the two FeatureSet objects. The advantage to splitting
them means that we can fairly easily implement validation checks in every
language that needs it for dynamic messages. The algorithm is as follows, for
some incoming unresolved FeatureSet user_features:
- Strip all unknown fields from user_features
- Strip all extensions from user_features that the runtime doesn't handle
- merged_features := user_features.Merge(overridable_defaults)
- assert merged_features == overridable_defaults
This will work as long as every feature is a scalar value (making merge a simple
override). We already ban oneof and repeated features, and we plan to ban
message features before the OSS release.
Note, that there is a slight gap here in that we perform no validation for
features owned by other languages. Dynamic messages in language A will naively
be allowed to specify whatever language B features they want. This isn't
optimal, but it is in line with our current situation where validation of
dynamic messages is substantially more permissive than descriptors processed by
protoc.
On the other hand, owners of language A will have the option of easily adding
validation for language B's features, without having to reimplement the
reflective inspection of imports that protoc does. This can be done by simply
adding those features to the compilation of the defaults IR, and then not
stripping those extensions during validation. This will have the effect of tying
the edition support window of A to that of B though, and A won't be able to
extend its maximum edition until B does (at least for dynamic messages). For
generators in a monorepo like Protobuf's this seems fine, but may not be
desirable elsewhere.
Patching Old Editions
In Edition Naming we decided to drop the idea of "patch"
editions, because editions were always forward and backward compatible. We would
only ever need multiple editions in a year if somehow we managed to speed up the
rollout process and wanted faster turnaround. This changes those assumptions
though, since now editions are neither forward-compatible (new features don't
work in old editions) or backward-compatible (old features may not work in new
editions).
Hypothetically, if there were a bug in the editions layer itself we may require
a "patch" edition to safely roll out a fix. For example, imagine we discover
that our calculation of edition defaults is broken in edition 2023 and we had
accidentally released it. If we've already fixed the issue and released edition
2024 as well, we can't just create a 2023A "patch" to fix the issue because
editions are represented as integers (and 2023 and 2024 are adjacent). We would
want to release some kind of fix for people still on edition 2023 though, so
that they can minimally upgrade before 2024 (which may be a breaking edition).
What we could do in this situation (if it ever arises) is introduce a new
integer field in FileDescriptorProto called edition_patch. It would take
some work to fit this into feature resolution and roll it out to every plugin,
but given that we've hidden the edition from most users
(Editions Feature Visibility) it shouldn't be
too bad. As long as patches never introduce or remove features or change their
defaults, protoc and plugins can always use the latest patch they know about to
represent that edition.
Documentation
As part of this change, we need to document all of this publicly for
plugin/runtime owners. We should create a new topic in
https://protobuf.dev/editions/ to cover all of this, along with other relevant
details they'd need to know.
Alternatives
Continue as usual
The only real alternative here is to make no change, which has all of the
problems listed in the overview of this topic.
Pros
- Requires no effort short-term
- Editions upgrades will never be breaking changes
Cons
- Likely to cause problems as soon as edition 2024
- Introducing new features is dangerous and unpredictable
- Dropping features affects all editions simultaneously
- The features supported in each edition can vary between protobuf releases
- High cognitive overhead for our users. They'd need to track the progress of
every feature individually across releases.
Full Validation for Dynamic Messages
None of the generators where editions have already been rolled out require any
changes. We will need to add validation layers to runtimes that support dynamic
messages though, to make sure there are no invalid descriptors floating around.
Any runtime that supports dynamic messages should have reflection, and the same
reflection-based algorithm will need to be duplicated everywhere. For eachFeatureSet specified on a descriptor:
/* Detailed source-code truncated for AI context efficiency. */Pros
- Prevents any feature lifetime violations for any language, in any language
- Easier to understand
- Less error-prone
- Easy to test with fake features
Cons
- Only works post-build, which requires a huge amount of code in every
language to walk the descriptor tree applying these checks - Performance concerns, especially in upb
- Duplicates protoc validation, even though most languages perform
significantly looser checks on dynamic messages
Pros
- Minimizes the amount of reflection needed
Cons
- Can't validate extensions for languages we don't know about, since they're
not built into the binary - Potential version skew between pool and runtime features
- Requires reflection stripping unexpected fields
- Difficult to understand the algorithm from the code
Design Editions Edition Naming (`docs/design/editions/edition-naming.md`)
Edition Naming
Author: @mkruskal-google
Approved: 2023-08-25
Background
One of the things Life of an Edition lays out is a very
loose scheme for naming editions. It defines an ordering rule and "." delimiter,
but otherwise imposes no constraints on the edition name. By convention, we
decided to use the year followed by an optional revision number.
One of the decisions in
Editions: Life of a FeatureSet was that
feature resolution would, at least partially, need to be duplicated across every
supported language.
Problem Description
As discussed in Life of a FeatureSet, the minimal operations we need to
duplicate are edition comparison and proto merging. While edition comparison
isn't very complicated today, it has a lot of edge cases we may miss due to
the loose constraints on edition names. It would also be really nice if we could
reduce it down to a lexicographical string comparison, which can be easily done
in any language.
There's also a very real Hyrum's law risk when we permit an infinite number of
edition names that we never expect to exist in practice. For example, 2023.a
is currently valid, and its relative ordering to 2023.10 (for example) is not
obvious. Notably, our initial editions tests used an edition very-cool, which
doesn't seem like something we need to support. Ideally, our edition names would
be as simple as possible, with enforceable and documented constraints.
There has been a lot of confusion when looking at the interaction between
editions and releases. Specifically, the fact that editions look like calver
numbers has led to us calling revisions "patch editions", which suggests that
they're bug fixes to an earlier one. This was not the original intention though,
which to summarize was:
Editions are strictly time-ordered. Revisions were simply a mechanism to
release more than one edition per calendar year, but new editions can't be
inserted into earlier slots.New editions can be added at any time. As long as they're ordered
after the pre-existing ones, this is a non-breaking change and can be done
in a patch release.New features can be added at any time without changing the edition.
Since they're a non-breaking change by definition, this can be done in a
patch release.Features can only be deleted in a breaking release. The editions model
doesn't support the deletion of features, which is always a breaking change.
This will only be done in a major version bump of protobuf.Feature defaults can only be changed in a new edition. Once the default
is chosen for a feature, we can only change it by releasing a new edition
with an updated default. This is a non-breaking change though, and can be
done in patch releases.
Recommended Solution
We want the following properties out of editions:
- Discrete allowable values controlled by us
- Easily comparable
- Cross-language support
- Relatively small set (<100 for the next century)
- Slow growth (~once per year)
The simplest solution here is to just make an Edition enum for specifying the
edition. We will continue to use strings in proto files, but the parser will
quickly convert them and all code from then on will treat them as enums. This
way, we will have a centralized cross-language list of all valid editions:
enum Edition {
EDITION_UNKNOWN = 0;
EDITION_2023 = 1;
EDITION_2024 = 2;
// ...
}We will document that these are intended to be comparable numerically for
finding the time ordering of editions. Proto files will specify the edition in
exactly the same way as in the original decision:
edition = "2023";Ideally, this would be an open enum to avoid ever having the edition thrown into
the unknown field set. However, since it needs to exist in descriptor.proto,
we won't be able to make it open until the end of our edition zero migration.
Until then, we can take advantage of the fact that syntax gets set toeditions by the parser when an edition is found. In this case, an unset
edition should be treated as an error. Once we migrate to an open enum we can
replace this with a simpler validity check.
Additionally, we've decided to punt on the question of revisions and just not
allow them for now. If we ever need more than that it means we've made a huge
mistake, and we can bikeshed on naming at that time (e.g. EDITION_2023_OOPS).
We will plan to have exactly one edition every year.
Pros
Edition comparison becomes even simpler, as it's just an integer
comparison- Feature resolution becomes trivial in every language
Automatic rejection of unknown editions.
- This includes both future and dropped editions, and also unknown
revisions - Other solutions would need custom logic in protoc to enforce these
- This includes both future and dropped editions, and also unknown
Doesn't look like calver, avoiding that confusion
Lack of revisions simplifies our documentation and makes editions easier to
understand/maintain
Cons
Might prove to be a challenge when we go to migrate
descriptor.prototo
editionsMight be a bit tricky to implement in the parser (but Prototiller does this
just fine)
Considered Alternatives
Edition Enums
Instead of using a string, editions could be represented as an enum. This would
give us a lot of enforcement for free and be maximally constraining. For
example:
enum Edition {
E2023 = 1;
E2023A = 2;
E2024 = 3;
}Pros
Edition comparison becomes even simpler, as it's just an integer
comparisonArbitrary number of revisions within a year
Automatic rejection of unknown editions (with closed enums).
- This includes both future and dropped editions, and also unknown
revisions - Other solutions would need custom logic in protoc to enforce these
- This includes both future and dropped editions, and also unknown
Doesn't look like calver, avoiding that confusion
Cons
Edition specification in every proto file becomes arguably less readable.
edition = "2023.1"is easier to read thanedition = E2023A.Might require parser changes to get
descriptor.protoonto editionsThis is a pretty big change and will require documentation/comms updates
Plugin owners can't pre-release upcoming features
- Do we even want to allow this? It could help coordinating editions
releases
- Do we even want to allow this? It could help coordinating editions
Neutral
Edition must be strictly time-ordered. We can't go back and add a revision
to an older revision, but the original plan didn't really allow for this
anyway.Edition ordering is not directly linked to the name at all. We'd have to
write a reflection test that enforces that they're strictly increasing.
Truncated Revisions
The simplest solution that's consistent with the original plan would be to limit
ourselves to no more than 9 intermediate editions per year. This means editions
would either be a simple year (e.g. 2023), or have a single digit revision
number (e.g. 2024.3). The revision number would be constrained to (0,9] and
the year would have to be an integer >=2023.
With these constraints in place, edition ordering becomes a simple
lexicographical string ordering.
Pros
- Tightly constrains edition names and avoids unexpected edge cases
- Edition comparison code is easily duplicated (basic string comparison)
- Removes ambiguity around revision 0 (e.g.
2023.0isn't allowed)
Cons
- Limits us to no more than 10 editions per year. This seems reasonable
today, and since protoc is the one enforcing this we can always revisit it
later. As long as the ordering stays lexicographical we can expand it
arbitrarily.
Fixed length editions
This is similar to the recommended solution, but instead of allowing year
editions we would always start with a .0 release. This means that Edition Zero
would become 2023.0. This has the same pros/cons as the solution above, with
the addition of:
Pros
- Makes revisions less jarring. Users won't be confused by
2023.1if they're
used to2023.0.
Cons
- We've already published comms and documentation calling the first edition
2023. We'd have to go update those and communicate the change. - We have no real use-case for revisions yet, and this adds complexity to the
"typical" case.
Edition Messages
Instead of using a string, editions could be represented as a message. This
would allow us to model the constraints better. For example:
message Edition {
uint32 year = 1;
uint32 revision = 2;
}Pros
- The schema itself would automatically enforce a lot of our constraints
- Arbitrary number of revisions would be allowed
- Can't accidentally use string comparison
Cons
- Custom comparison code would still be needed in every language (marginally
more expensive than string comparison) - We would either need to convert the edition string into this message during
parsing or radically change the edition specification
Do Nothing
Pros
- Easier today, since we don't have to do anything
Cons
- Harder tomorrow when we have to duplicate this comparison code into every
language - Leaves us open to Hyrum's law and unexpected abuse
Design Editions Edition Zero Converged Semantics (`docs/design/editions/edition-zero-converged-semantics.md`)
Edition Zero: Converged Semantics
Approved: 2021-10-07
Background
The Protobuf Team has been exploring potential facilities for introducing
breaking API and semantic changes. This document is an attempt to make use of
these facilities to unify proto semantics from this point forward, while giving
customers the ability to more-granularly manage their project's specific needs.
Objective
We want to reduce complications of API semantics that are coarsely managed
through the syntax keyword, and instead default to converged proto2/proto3 when
opting into editions. Where needed, customers will be able to opt out of
specific semantics that are incompatible with their existing usages, at a
fine-grained level, using the new capabilities provided by editions + features.
Why Now?
As we introduce new facilities for managing breaking changes, we have an
additional opportunity to cutover and rectify a long-standing vision of
converging proto 2/3 semantics as a natural extension of this.
Doing this in lockstep with the introduction of editions provides the protobuf
team with a few valuable outcomes:
Editions provides us with more granular specification of intent than the
existing coarse knob of "proto2" or "proto3." By opting into our first
edition, customers are upgrading to what we've referred to in the past as
"converged semantics," and if needed can reversibly downgrade back to proto2
or proto3 semantics respectively by opt-ing out of the specific features
that are incompatible with their existing needs.The protobuf team can avoid the n^2 complexity of considering how an
edition/feature will interplay with an explicit syntax designation of
"proto2" vs "proto3" for all impacted runtimes. This allows us to transition
our thinking/support model to be explicitly feature-centric.The introduction of editions will almost certainly cause a major version
bump and gives us ample justification to make breaking changes as we
transition to this granular specification.
Introduction of the `edition` keyword to proto IDL
The edition keyword is used to define which semantic version a particular file
and all of its contents will adhere to as a baseline. Whenever a proto file
declares an edition keyword, it automatically defaults to converged proto2/3
semantics.
An edition's value is represented as a string, encoded by convention as a year.
Introduction of `features` option to `descriptor.proto`
This option will be uniformly defined as a repeated set of strings which can be
used to encode the ability to opt-out of a specific feature (eg:"-string_view"), or to potentially opt-in to a future/experimental feature
(eg: "string_view"). The features option will be added to descriptor.proto
for the following descriptor options:
- File
- Message
- Field
- Enum
- Enum Value
- Oneof
- Service
- Method
- Stream (internal repositories only)
Features are only respected when used in conjunction with the edition keyword.
They are not validated for correctness to ensure they are forward/backward
compatible with releases.
Features may be declared at any descriptor level, however, a feature definition
may influence descendant types at the discretion of the protobuf team. (e.g., a
file-level feature opt-out could impact all fields within the file, if it was
desired).
A taxonomy of features
Features can be broken down into two main categories: language-specific and
semantic.
Language-specific features
Language-specific features pertain to the generated API for a given language.
Referring to the protobuf breaking changes backlog we can see some examples:
- (C++) Changing string fields to return
string_view. - (Java) Removing the confusing
Enum#valueOf(int)API. - (Java) Rename oneof enums to do appropriate camel casing.
Language-specific features have no meaning for any other language: they can be
ignored entirely. They are, in essence, a private (tunneled) interface between
protobuf IDL and the respective code generator. Each language's code generator
can independently decide what the "base" set of features is for any given
edition. Each language defines the migration path between editions
independently.
Semantic Features
Semantic features define behavior changes that apply to the protobuf data model,
independent of language. These can also have API implications, but their meaning
goes deeper than just a surface-level API. Some examples of semantic features:
- Open enums (enums are placed directly into the field instead of the
UnknownFieldSet). - Packed (whether repeated fields are packed on the wire)
Semantic features have significantly broader scope, since they must be respected
across languages, and each language must implement the semantic correctly. This
also implies that every language must either (1) know the canonical set of "base
features" for each edition, or (2) that the set of "default" features for the
edition must be resolved in protoc itself and propagated explicitly into the
descriptor.
Rev'ing the protobuf IDL vs. descriptor.proto
Changing descriptor.proto to reflect editions is a much more intrusive change
than changing just the protobuf IDL. The protobuf IDL is parsed and resolved in
protoc, and we have only a single implementation of that parser. Any change that
can be resolved in the parser alone is relatively unintrusive (though there are
build horizon issues since GCL parses protos in prod).
Rev'ing descriptor.proto is a far more intrusive change that affects many
downstream systems. Many systems access descriptors through either a descriptor
API (for example, google::protobuf::Descriptor in C++) or by directly accessing a proto
from descriptor.proto (eg. google.protobuf.DescriptorProto). Any changes
here need to be managed much more delicately.
Deprecation of the `syntax` keyword from proto IDL
The syntax keyword shall no longer be required/observed when an edition
keyword is present, as it is now considered redundant. If edition and syntax
are both present, edition takes precedence and syntax is ignored.
Migrating from `proto2` and `proto3` to Editions + Features
Today's usage of syntax opaquely bundles a collection of implied feature flags
that are set based on the presence of proto2 or proto3. This is often a
source of confusion for customers (eg: what am I gaining by moving to proto3?
What am I losing?).
By deciding that editions/features exist in a state of proto2/3 convergence,
this enables customers to decide for themselves what features are important to
their usage of protos.
Migrating existing users of proto2 and proto3 to editions w/converged semantics
would mean we'd need to execute a large-scale change to make their
implicit/implied behavior explicit. Here are examples of implied behavior.
today:
| Feature | proto2 implied behavior |
proto3 implied behavior |
|---|---|---|
| packed_repeated_primitives | 🚫 | ✅ |
| extensions | ✅ | 🚫 |
| required | ✅ | 🚫 |
| groups | ✅ | 🚫 |
| cpp_string_view | 🚫 | 🚫 |
| java_enum_no_value_of | 🚫 | 🚫 |
| open_enums | 🚫 | ✅ |
| MORE STUFF ... |
Managing Complexity of `features` for Large Deployments
A separate concept has been established to help mitigate the complexity of
editions and progressive feature rollouts and synchronizations for larger proto
projects.
This facility could be used to migrate existing usages of the syntax keyword
to use Editions + Features across google3, for example.
Prior Work
proto{2,3} Convergence Vision (not available externally)
Epochs for descriptor.proto (not available externally)
Rust editions: https://doc.rust-lang.org/edition-guide/editions/index.html
Design Editions Edition Zero Feature Enum Field Closedness (`docs/design/editions/edition-zero-feature-enum-field-closedness.md`)
Edition Zero Feature: Enum Field Closedness
Author: @mcy
Approved: 2023-02-13
Background
On 2023-02-10, a CL @mcy submitted to deletegoogle::protobuf::Reflection::SupportsUnknownEnumValue(). Oddly, this function used the
containing message's syntax, rather than the enum field's, to determine
whether the enum was open.
It turns out we misunderstood a critical corner-case of proto3 enums. Consider
the following proto files:
// enum.proto
syntax = "proto3";
package oh.no;
enum Enum {
A = 0;
B = 1;
}
// message.proto
syntax = "proto2";
package oh.no;
import "enum.proto";
message Msg {
optional Enum enum = 1;
}If we parse the Protoscope
value 1: 2 as an oh.no.Msg, and look at the value of oh.no.Msg.enum, we
will find that it is not present, and that there is a VARINT of value 2 in theUnknownFieldSet.
This is because Protobuf sometimes implements the openness of an enum by its
usage, not its definition.
This case is actually quite difficult to observe, because the converse doesn't
work: the proto compiler rejects proto2-enum-valued fields in proto3 messages,
because such enums can have nonzero defaults, which proto3 does not support due
to implicit presence.
Languages
| Language | Open/Closed handling |
|---|---|
| C++ | Determined by the field using the enum's file |
| Java | Determined by the field using the enum's file |
| UPB (non-ruby) | Determined by the enum's definition file |
| UPB (ruby) | All enums treated as open |
| C# | All enums treated as open |
| Obj-C | by usage (< 22.x) |
by definition (>= 22.x)
It looks like this was handled by the field's usage, but in Nov as part of the syntax cleanup, we stopped looking at syntax and captured things on the enum definition, so it's now defined by the enum. |
| Swift | Determined by the enum's definition file
Swift uses the ability for enums to have associated values, so an enum defined in a proto3 syntax file gets a value that holds all unknown values. So a proto2 syntax defined message will still end up with the enum using that to hold unknown values. |
| Go | All enums treated as open |
| Apps JSPB | All enums treated as open |
| ImmutableJs | All enums treated as open |
| JsProto | All enums treated as open |
Impact
Approximately 2.99% of enum fields import enums across syntaxes and 1.77% of
enums are imported across syntaxes.
6.14% of fields being enum fields, meaning 0.18% of fields are affected when
used by affected languages.
Overview
This document proposes adding an additional feature to
Edition Zero Features, specified as the following
.proto fragment:
message Features {
// ...
optional bool legacy_treat_enum_as_closed = ??? [
retention = RUNTIME,
target = FILE,
target = FIELD
];
}The name of this field captures the desired intent: this is a bad legacy
behavior that we believe is rare and want to stamp out. Edition 2023 would set
this to false by default, and proto2 would treat it as implicitly true. It
also does not permit the converse: you cannot force a field to be open, because
that is currently not possible and we don't want to add more special cases.
Additionally, we would like to make special dispensation in migration tooling
for this field: it should not be set unconditionally when migrating from proto2
-> editions, but only on proto2 fields that are of proto3 enum type. We should
also want to build an allowlist for this, like we do for required.
This option can also help in migrating enums from closed to open, since we can
use it to migrate individual use-sites by marking the enum as open and all of
its uses as treat-as-closed in one CL, and then deleting the treat-as-closed
annotations one by one.
An open (lol) question is whether we should move is_closed fromEnumDescriptor to FieldDescriptor.
Recommendation
Use the "define official behavior" alternative below. Given the wide variety of
behavior in different languages, a singular global setting will always leave
some of our languages in the lurch. As such, we will use per language features
to allow each language to control its own evolution while we define the
"correct" behavior.
For example, in C++ we will define:
// Determines if the given enum field is treated as closed based on legacy
// non-conformant behavior.
//
// Conformant behavior determines closedness based on the enum and
// can be queried using EnumDescriptor::is_closed().
//
// Some runtimes currently have a quirk where non-closed enums are
// treated as closed when used as the type of fields defined in a
// `syntax = proto2;` file. This quirk is not present in all runtimes; as of
// writing, we know that:
//
// - C++, Java, and C++-based Python share this quirk.
// - UPB and UPB-based Python do not.
// - PHP and Ruby treat all enums as open regardless of declaration.
//
// Care should be taken when using this function to respect the target
// runtime's enum handling quirks.
bool FieldDescriptor::legacy_enum_field_treated_as_closed() const {
return type() == TYPE_ENUM && file().syntax() == FileDescriptor::SYNTAX_PROTO2;
}In Java, FileDescriptor.supportsUnknownEnumValue() will need to be deprecated
and replaced with the above.
Alternatives
Define official behavior
Define the official behavior to be "Enums open-ness should be defined by the
definition of the enum." Add a conformance test for this behavior. Use per
language features to eventually converge implementations that are out of
conformance. We choose to define this as "enum openness is defined by the
definition" because that matches the model for almost all other proto3/proto2
properties.
Pros
- Clarifies desired behavior
- Existing implementations can change incrementally using editions
- Avoids complicating global features for something that is a per-language
issue
Cons
- When migrating from syntax to edition zero, Prototiller will need to know
all used languages to make the upgrade a trivial change (this is already the
case for other edition upgrades).
Make `Features.enum` a field-level feature
Here, we don't add legacy_treat_enum_as_closed and instead make closeness a
bona fide property of fields, not enums.
Pros
- Reflects the current behavior of Protobuf for our largest languages
(C++/Java). - Removes the possibility of making a mistake in reflective code that checks
is_closed()onEnumDescriptorrather thanFieldDescriptor.
Cons
- Doesn't handle the case for languages other than C++/Java
- Harder to migrate individual enums to open, since the property is not in
control of the owner of the type. - Conceptually unpleasant, since it gives locality to the meaning of
IsValid, unless we want to believe thatIsValidmerely states whether
the value has a name we know of.
Allow `Features.enum` on both enums and fields
This allows enum owners some more control without needing to introduce a
strictly "legacy do not use" feature.
Pros
- We don't introduce a "legacy do not use" option, and don't need to play the
allowlist game.
Cons
- We need to support closed-enums-treated-as-open, which is a functionality
Protobuf does not offer today.
Name the feature `Features.treat_as_closed_for_migration`
This is an aesthetic choice if we feel this is a useful knob for migration, that
still highlights its temporary nature.
Pros
- We don't introduce a "legacy do not use" option, and don't need to play the
allowlist game. - Clearly underscores that this is for migration in a specific desirable
direction (closed -> open).
Cons
- People may use it because they like closed enums for some reason and don't
fully appreciate the ramifications.
Do Nothing
We can simply keep the current editions enum semantics.
Pros
- No extra work.
Cons
- proto2 -> editions is not a no-op in some cases. This breaks a lot of the
draw of moving to editions, even though it is possible to detect the no-ops
in advance. - This would immediately add a blocker to our syntax reflection large-scale
change
Design Editions Edition Zero Features (`docs/design/editions/edition-zero-features.md`)
Edition Zero Features
Authors: @mcy,
@zhangskz,
@mkruskal-google
Approved: 2022-07-22
Feature flags, and their defaults, that we will introduce to define the
converged semantics of Edition Zero.
NOTE: This document is largely replaced by the topic,
Feature Settings for Editions (to be
released soon).
Overview
Edition Zero Features defines the "first edition" of the brave new world of
no-syntax Protobuf. This document defines the actual mechanics of the features
(in the narrow sense of editions) we need to implement in protoc, as well as the
chosen defaults.
This document will require careful review from various stakeholders, because it
is essentially defining a new Protobuf syntax, even if it isn't spelled that
way. In particular, we need to ensure that there is a way to rewrite existingproto2 and proto3 files as editions files, and the behavior of "mixed
syntax" messages, without any nasty surprises.
Note that it is an explicit goal that it be possible to take an arbitrary
proto2/proto3 file and convert it to editions without semantic changes, via
appropriate application of features.
Existing Non-Conformance
We must keep in mind that the status quo is messy. Many languages have some
areas where they currently diverge from the correct proto2/proto3 semantics. For
edition zero, we must preserve these idiosyncratic behaviors, because that is
the only way for a proto2/proto3 -> editions LSC to be a no-op.
For example, in this document we define a feature features.enum = {CLOSED,OPEN}. But currently Go does not implement closed enum semantics forsyntax=proto2 as it should. This behavior is out of conformance, but we must
preserve this out-of-conformance behavior for edition zero.
In other words, defining features and their semantics is in scope for edition
zero, but fixing code generators to perfectly match those semantics is
explicitly out-of-scope.
Glossary
Because we need to speak of two proto syntaxes, proto2 and proto3, that have
disagreeing terminology in some places, we'll define the following terms to aid
discussion. When a term appears in code font, it refers to the Protobuf
language keyword.
- A presence discipline is a handling for the presence (or hasbit) of a
field. Every field notionally has a hasbit: whether it has been explicitly
set via the API or whether a record for it was present on deserialization.
See
Application Note: Field Presence
for more on this topic. The discipline specifies how this bit is surfaced to
the user:- No presence means that the API does not expose the hasbit. The
default value for the field behaves somewhat like a special sentinel
value, which is not serialized and not merged-from. The hasbit may still
exist in the implementation (C++ accidentally leaks this via HasField,
for example). Note that repeated fields sort-of behave like no presence
fields. - Explicit presence means that the API exposes the hasbit through a
hasmethod and aClearmethod; default values are always serialized
if the hasbit is set.
- No presence means that the API does not expose the hasbit. The
- A closed enum is an enum where parsing requires validating that a parsed
int32representing a field of this type matches one of the known set of
valid values. - An open enum does not have this restriction, and is just an
int32
field with well-known values.
For the purposes of this document, we will use the syntax described in Features
as Custom Options, since it is the prevailing consensus among those working on
editions, and allows us to have enum-typed features. The exact names for the
features are a matter of bikeshedding.
Proposed Converged Semantics
There are two kinds of syntax behaviors we need to capture: those that are
turned on by a keyword, like required, and those that are implicit, like open
enums. The differences between proto2 and proto3 today are:
- Required. Proto2 has
requiredbut notdefaulted; Proto3 hasdefaulted
but notrequired. Proto3 also does not allow custom defaults ondefaultedfields, and on message-typed fields,defaultedis a synonym
foroptional. - Groups. Proto2 has groups, proto3 does not.
- Enums. In Proto2, enums are closed: messages that have an enum not in
the known set are stored in the unknown field set. In Proto3, enums are
open. - String validation. Proto2 is a bit wobbly on whether strings must be UTF-8
when serialized; Proto3 enforces this (sometimes). - Extensions. Proto2 has extensions, while Proto3 does not (
Anyis the
canonical workaround).
We propose defining the following features as part of edition zero:
features.field_presence
This feature is enum-typed and controls the presence discipline of a singular
field:
EXPLICIT(default) - the field has explicit presence discipline. Any
explicitly set value will be serialized onto the wire (even if it is the
same as the default value).IMPLICIT- the field has no presence discipline. The default value is
not serialized onto the wire (even if it is explicitly set).LEGACY_REQUIRED- the field is wire-required and API-optional. Setting
this will require being in therequiredallowlist. Any explicitly set
value will be serialized onto the wire (even if it is the same as the
default value).
The syntax for singular fields is a much debated question. After discussing the
tradeoffs, we have chosen to eliminate both the optional and required
keywords, making them parse errors. Singular fields are spelled as in proto3
(no label), and will take on the presence discipline given byfeatures.:presence. Migration will require deleting every instance ofoptional in proto files in google3, of which there are 385,236.
It is important to observe that proto2 users are much likelier to care about
presence than proto3 users, since the design of proto3 discourages thinking
about presence as an interesting feature of protos, so arguably introducing
proto2-style presence will not register on most users' mental radars. This is
difficult to prove concretely.
IMPLICIT fields behave much like proto3 implicit fields: they cannot have
custom defaults and are ignored on submessage fields. Also, if it is an
enum-typed field, that enum must be open (i.e., it is either defined in asyntax = proto3; file or it specifies option features.enum = OPEN;
transitively).
We also make some semantic changes:
IMPLICIT``fields may have a custom default value, unlike inproto3. Whether anIMPLICIT` field containing its default value is serialized
becomes an implementation choice (implementations are encouraged to try to
avoid serializing too much, though).has_optional_keyword()andhas_presence()now check forEXPLICIT, and
are effectively synonyms.proto3_optionalis rejected as a parse error (use the feature instead).
Migrating from proto2/3 involves deleting all optional/required labels and
adding IMPLICIT and LEGACY_REQUIURED annotations where necessary.
Alternatives
- For syntax:
- Require
optional. This may confuse proto3 users who are used tooptionalnot being a default they reach for. Will result in
significant (trivial, but noisy) churn in proto3 files. The keyword is
effectively line noise, since it does not indicate anything other than
"this is a singular field". - Invent a new label, like
singular. This results in more churn but
avoids breaking peoples' priors. - Allow
optionaland no label to coexist in a file, which take on their
original meanings unless overridden byfeatures.field_presence. The
fact that a top-levelfeatures.field_presence = IMPLICITbreaks the
proto3 expectation thatoptionalmeansEXPLICITmay be a source of
confusion.
- Require
proto:allow_required, which must be present forrequiredto not be a
syntax error.- Allow
required/optionaland introducedefaultedas a real keyword. We
will not have another easy chance to introduce such syntax (which we do,
becauseedition = ...is a breaking change). - Reject custom defaults for
IMPLICITfields. This is technically not really
needed for converged semantics, but trying to remove the Proto3-ness fromIMPLICITfields seems useful for consistency.
Future Work
In the future, we can introduce something like features.always_serialize or a
similar new enumerator (ALWAYS_SERIALIZE) to the when_missing enum, which
makes EXPLICIT_PRESENCE fields unconditionally serialized, allowingLEGACY_REQUIRED fields to become EXPLICIT_PRESENCE in a future large-scale
change. The details of such a migration are out-of-scope for this document.
Migration Examples
Given the following files:
// foo.proto
syntax = "proto2"
message Foo {
required int32 x = 1;
optional int32 y = 2;
repeated int32 z = 3;
}
// bar.proto
syntax = "proto3"
message Bar {
int32 x = 1;
optional int32 y = 2;
repeated int32 z = 3;
}post-editions, they will look like this:
// foo.proto
edition = "tbd"
message Foo {
int32 x = 1 [features.field_presence = LEGACY_REQUIRED];
int32 y = 2;
repeated int32 z = 3;
}
// bar.proto
edition = "tbd"
option features.field_presence = NO_PRESENCE;
message Bar {
int32 x = 1;
int32 y = 2 [features.field_presence = EXPLICIT_PRESENCE];
repeated int32 z = 3;
}features.enum_type
Enum types come in two distinct flavors: closed and open.
closed enums will store enum values that are out of range in the unknown
field set.open enums will parse out of range values into their fields directly.
NOTE: Closed enums can cause confusion for parallel arrays (two repeated
fields that expect to have index i refer to the same logical concept in both
fields) because an unknown enum value from a parallel array will be placed
in the unknown field set and the arrays will cease being parallel. Similarly
parsing and serializing can change the order of a repeated closed enum by
moving unknown values to the end.NOTE: Some runtimes (C++ and Java, in particular) currently do not use
the declaration site of enums to determine whether an enum field is treated
as open; rather, they use the syntax of the message the field is defined in,
instead. To preserve this proto2 quirk until we can migrate users off of it,
Java and C++ (and runtimes with the same quirk) will use the value offeatures.enumas set at the file level of messages (so, if a file setsfeatures.enum = CLOSEDat the file level, enum fields defined in it behave
as if the enum was closed, regardless of declaration). IMPLICIT singular
fields in Java and C++ ignore this and are always treated as open, because
they used to only be possible to define in proto3 files, which can't use
proto2 enums.
In proto2, enum values are closed and no requirements are placed upon the
first enum value. The first enum value will be used as the default value.
In proto3, enum values are open and the first enum value must be zero. The
first enum value is used as the default value, but that value is required to
be zero.
In edition zero, We will add a feature features.enum_type = {CLOSED,OPEN}. The
default will be OPEN. Upgraded proto2 files will explicitly setfeatures.enum_type = CLOSED. The requirement of having the first enum value be
zero will be dropped.
NOTE: Nominally this exposes a new state in the configuration space, OPEN
enums with a non-zero default. We decided that excluding this option simply
because it was previously inexpressible was a false economy.
Alternatives
- We could add a property for requiring a zero first value for an enum. This
feels needlessly complicated. - We could drop the ability to have
CLOSEDenums, but that is a semantic
change.
Migration Examples
Given the following files:
// foo.proto
syntax = "proto2"
enum Foo {
A = 2, B = 4, C = 6,
}
// bar.proto
syntax = "proto3"
enum Bar {
A = 0, B = 1, C = 5,
}post-editions, they will look like this:
// foo.proto
edition = "tbd"
option features.enum_type = CLOSED;
enum Foo {
A = 2, B = 4, C = 6,
}
// bar.proto
edition = "tbd"
enum Bar {
A = 0, B = 1, C = 5,
}If we wanted to merge them into one file, it would look like this:
// foo.proto
edition = "tbd"
enum Foo {
option features.enum_type = CLOSED;
A = 2, B = 4, C = 6,
}
enum Bar {
A = 0, B = 1, C = 5,
}features.repeated_field_encoding
In proto3, the repeated_field_encoding attribute defaults to PACKED. In
proto2, the repeated_field_encoding attribute defaults to EXPANDED. Users
explicitly enabled packed fields 12.3k times, but only explicitly disable it 200
times. Thus we can see a clear preference for repeated_field_encoding = PACKED
emerge. This data matches best practices. As such, the default value forfeatures.repeated_field_encoding will be PACKED.
The existing [packed = …] syntax will be made an alias for setting the feature
in edition zero. This alias will eventually be removed. Whether that removal
happens during the initial large-scale change to enable edition zero or as a
follow on will be decided at the time.
In the long term, we would like to remove explicit usages offeatures.repeated_field_encoding = EXPANDED, but we would prefer to separate
that large-scale change from the landing of edition zero. So we will explicitly
set features.repeated_field_encoding to EXPANDED at the file level when we
migrate proto2 files to edition zero.
Alternatives
- Force everyone to use packed fields. This is a semantic change, which we're
trying to avoid in edition zero. - Don’t add
features.repeated_field_encodingand instead specify[packed = false]when converting from proto2. This will be incredibly noisy,
syntax-wise and diff-wise.
Migration Examples
Given the following files:
// foo.proto
syntax = "proto2"
message Foo {
repeated int32 x = 1;
repeated int32 y = 2 [packed = true];
repeated int32 z = 3;
}
// bar.proto
syntax = "proto3"
message Foo {
repeated int32 x = 1;
repeated int32 y = 2 [packed = false];
repeated int32 z = 3;
}post-editions, they will look like this:
// foo.proto
edition = "tbd"
options features.repeated_field_encoding = EXPANDED;
message Foo {
repeated int32 x = 1;
repeated int32 y = 2 [packed = true];
repeated int32 z = 3;
}
// bar.proto
edition = "tbd"
message Foo {
repeated int32 x = 1;
repeated int32 y = 2 [packed = false];
repeated int32 z = 3;
}Note that post migration, we have not changed packed tofeatures.repeated_field_encoding = PACKED, although we could choose to do so
if the diff cost is not monumental. We prefer to defer to an LSC after editions
are shipped, if possible.
features.string_field_validation
WARNING: UTF8 validation is actually messier than originally believed. This
feature is being reconsidered in Editions Zero Feature: utf8_validation.
This feature is a tristate:
MANDATORY- this means that a runtime MUST verify UTF-8.HINT- this means that a runtime may refuse to parse invalid UTF-8, but it
can also simply skip the check for performance in some build modes.NONE- this field behaves like abytesfield on the wire, but parsers
may mangle the string in an unspecified way (for example, Java may insert
spaces as replacement characters).
The default will be MANDATORY.
Long term, we would like to remove this feature and make all string fieldsMANDATORY.
Alternatives
- Drop the UTF-8 requirements completely. This seems like it will create more
problems than it will solve (e.g., random things relying on validation need
to be fixed) and it will be a lot of work. This is also counter to the
vision of string being a UTF-8 type, and bytes being its unchecked sibling. - Make opt-in verification a hard requirement instead of a hint, so that users
have a nice performance needle they can play with.
Future Work
In the infinite future, we would like to remove this feature and force allstring fields to be UTF-8 validated. To do this, we need to recognize that
what many callers want from their string fields is a bytes field with astring-like API. To ease the transition, we would add per-codegen backend
features, like java.bytes_as_string, that give a bytes field a generated API
resembling that of a string field (with caveats about replacement characters
forced by the host language's string type).
The migration would take HINT or SKIP string fields and convert them intobytes fields with the appropriate API modifiers, depending on which languages
use that proto; C++-only protos, for example, are a no-op.
There is an argument to be made for "I want a string type, and I explicitly want
replacement U+FFFD characters if I get something that isn't UTF-8." It is
unclear if this is something users want and we would need to investigate it
before making a decision.
features.json_format
This feature is dual state in edition zero:
ALLOW- this means that a runtime must allow JSON parsing and
serialization. Checks will be applied at the proto level to make sure that
there is a well-defined mapping to JSON.LEGACY_BEST_EFFORT- this means that a runtime will do the best it can to
parse and serialize JSON. Certain protos will be allowed that can result in
undefined behavior at runtime (e.g. many:1 or 1:many mappings).
The default will be ALLOW, which maps the to the current proto3 behavior.LEGACY_BEST_EFFORT will be used for proto2 files that require it (e.g. they’ve
set deprecated_legacy_json_field_conflicts)
Alternatives
- Keep the proto2 behavior - this will regress proto3 files by removing
validation for JSON mappings, and lead to more undefined runtime behavior - Only use
ALLOW- there are ~30 cases internally where protos have invalid
JSON mappings and rely on unspecified (but luckily well defined) runtime
behavior.
Future Work
Long term, we would like to either remove this feature entirely or add aDISALLOW option instead of LEGACY_BEST_EFFORT. This will more strictly
enforce that protos without a valid JSON mapping can’t be serialized or parsed
to JSON. DISALLOW will be enforced at the proto-language level, where no
message marked ALLOW can contain any message/enum marked DISALLOW (e.g.
through extensions or fields)
Migration Examples
Extensions are Always Allowed
Extensions may be used on all messages. This lifts a restriction from proto3.
Extensions do not play nicely with TypeResolver. This is actually fixable, but
probably only worth it if someone complains.
Alternatives
- Add
features.allow_extensions, default true. This feels unnecessary since
utteringextendandextensionsis required to use extensions in the
first place.
features.message_encoding
This feature defaults to LENGTH_PREFIXED. The group syntax does not exist
under editions. Instead, message-typed fields that havefeatures.message_encoding = DELIMITED set will be encoded as groups (wire type
3/4) rather than byte blobs (wire type 2). This reflects the existing API
(groups are funny message fields) and simplifies the parser.
A proto2 group field will be converted into a nested message type of the same
name, and a singular submessage field that is features.message_encoding = DELIMITED with the message type's name in snake_case.
This could be used in the future to switch new message fields to use group
encoding, which suggested previously as an efficiency direction.
Alternatives
- Allow groups in
editionswith no changes.groupsyntax is deprecated, so
we may as well take the opportunity to knock it out. - Add a sidecar allowlist like we do for
required. This is mostly
orthogonal.
Migration Examples
Given the following file
// foo.proto
syntax = "proto2"
message Foo {
group Bar = 1 {
optional int32 x = 1;
repeated int32 y = 2;
}
}post-editions, it will look like this:
// foo.proto
edition = "tbd"
message Foo {
message Bar {
optional int32 x = 1;
repeated int32 y = 2;
}
Bar bar = 1 [features.message_encoding = DELIMITED];
}Proposed Features Message
Putting together all of the above, we propose the following Features message,
including retention and target rules associated with fields.
message Features {
enum FieldPresence {
EXPLICIT = 0;
IMPLICIT = 1;
LEGACY_REQUIRED = 2;
}
optional FieldPresence field_presence = 1 [
retention = RUNTIME,
target = FILE,
target = FIELD
];
enum EnumType {
OPEN = 0;
CLOSED = 1;
}
optional EnumType enum = 2 [
retention = RUNTIME,
target = FILE,
target = ENUM
];
enum RepeatedFieldEncoding {
PACKED = 0;
UNPACKED = 1;
}
optional RepeatedFieldEncoding repeated_field_encoding = 3 [
retention = RUNTIME,
target = FILE,
target = FIELD
];
enum StringFieldValidation {
MANDATORY = 0;
HINT = 1;
NONE = 2;
}
optional StringFieldValidation string_field_validation = 4 [
retention = RUNTIME,
target = FILE,
target = FIELD
];
enum MessageEncoding {
LENGTH_PREFIXED = 0;
DELIMITED = 1;
}
optional MessageEncoding message_encoding = 5 [
retention = RUNTIME,
target = FILE,
target = FIELD
];
extensions 1000; // for features_cpp.proto
extensions 1001; // for features_java.proto
}Design Editions Edition Zero Json Handling (`docs/design/editions/edition-zero-json-handling.md`)
Edition Zero: JSON Handling
Author: @mkruskal-google
Approved: 2023-05-10
Background
Today, proto3 fully validates JSON mappings for uniqueness during parsing, while
proto2 takes a best-effort approach and allows cases that don't have a 1:1
mapping. This is laid out in more detail by JSON Field Name Conflicts (not
available externally). While we had hoped to unify these before
Protobuf editions launched, we ended up blocked
by some internal use-cases. This issue is now blocking the editions launch,
since we can't represent this behavior with the current set of
Edition Zero features.
Overview
Today, by default, we transform each field name to a CamelCase name that will
always be valid, but not necessarily unique in JSON. We also support ajson_name field option to override this for JSON parsing/serialization. This
allows conflicts to potentially arise where many proto fields map to the same
JSON field. Our JSON handling has the following behaviors:
- All proto messages can be serialized to JSON
- Conflicting mappings will produce JSON with duplicate keys
- All proto messages can be parsed from JSON
- Conflicting mappings lead to undefined behavior. While the behavior is
deterministic in all of the cases we've encountered, it's inconsistent
across runtimes and unexpected.
- Conflicting mappings lead to undefined behavior. While the behavior is
- The Protobuf compiler will fail to parse any proto3 files if any JSON
conflicts are detected by default- Disabled by
deprecated_legacy_json_field_conflictsoption
- Disabled by
- Proto2 files will only fail to parse if both of the conflicts fields have
json_nameset- We will still warn for default json mapping conflicts if
deprecated_legacy_json_field_conflictsisn't set
- We will still warn for default json mapping conflicts if
The goal here is to unify these behaviors into a future-facing feature as part
of edition zero.
Recommendation
We recommend adding a new json_format feature as part of
Edition Zero features. The doc will be updated to
reflect the following details.
JSON format can have three possible states:
ALLOW- By default, fields will be fully validated during proto parsing.
Any conflicting JSON mappings will trigger protoc errors, guaranteeing
uniqueness. This will be consistent with the current proto3 behavior. No
runtime changes are needed, since we allow JSON parsing/serialization.DISALLOW- Alternatively, we will ban JSON encoding and disable all
validation related to JSON mappings. All runtimes will fail to parse or
serialize any messages to/from JSON when this feature is set on the
top-level messages. This is a new mode which provides an alternative toLEGACY_BEST_EFFORTthat doesn't involve any schema changes.LEGACY_BEST_EFFORT- Fields will be validated for correctness, but not for
uniqueness. Any conflicting JSON mappings will trigger protoc warnings, but
no errors. This will be consistent with the current proto2 behavior, or
proto3 wheredeprecated_legacy_json_field_conflictsis set. Since this is
undefined behavior we want to get rid of, a parallel effort will attempt to
remove this later. No runtime changes are needed, since we allow JSON
parsing/serialization.
Long-term, we want JSON support to be specified at the proto level. For the
migration from proto2/proto3, we will just migrate everything to ALLOW andLEGACY_BEST_EFFORT depending on the syntax and the value ofdeprecated_legacy_json_field_conflicts.
We will additionally ban any ALLOW message from containing a DISALLOW type
anywhere in its tree (including extensions, which will fail to compile).
Attempting to add this will result in a compiler error. This has the following
benefits:
The implementation is a lot simpler, since most of the work is done in
protoc and parsers only need to check the top level messageRuntime failures aren't dependent on the contents of the message being
serialized/parsedAvoids messy blurring of ownership. If a bug occurs because a
DISALLOW
field is sometimes set, is the owner of the child type required to change it
toALLOW? Or is the owner of the parent type responsible because they
added the dependency?LEGACY_BEST_EFFORTwill continue to allow serialization/parsing of types
withDISALLOWset.
This feature will target messages and enums, but we will also provide it at the
file level for convenience.
Example use-cases for DISALLOW:
- https://github.com/protocolbuffers/protobuf/issues/12525
- Some projects generate proto descriptors at runtime and uses underscores to
disambiguate field names. They never use JSON format with these protos, but
currently have to work around our conflict checks
Alternatives
Dual State
Instead of a tri-state feature, we could have a simple allow/disallow feature
for JSON format.
Pros
- Simpler conceptually
Cons
- We would end up blocked by many of the protos that we were unable to migrate
as part of JSON Field Name Conflicts (not available externally). While
some of them could be migrated toDISALLOW, others are actually
depending on our current behavior under JSON mapping conflicts (as a
hack around some limitations in JSON customization).
Default to DISALLOW
Instead of defaulting to ALLOW, we could default to DISALLOW.
Pros
The majority of internal Google protos are used for binary/text encoding and
don't care about JSON, so this would:
- Be less noisy for teams who forget to explicitly set
DISALLOWand may have
fields with conflicting JSON mappings - Decrease our support surface
Cons
- We would need to figure out where
DISALLOWcan be added
Do Nothing
Pros
- Short-term it saves some trouble and keeps edition zero simpler
Cons
- We'll eventually hit the same issues we hit in JSON Field Name Conflicts
(not available externally) - The current proto2/proto3 behaviors are mutually exclusive. There's nothing
we can migrate to in today's edition zero that won't risk breaking one of
them.
Design Editions Editions Feature Extension Layout (`docs/design/editions/editions-feature-extension-layout.md`)
Editions: Feature Extension Layout
Author: @mkruskal-google,
@zhangskz
Approved: 2023-08-23
Background
"What are Protobuf Editions" lays out a plan
for allowing for more targeted features not owned by the protobuf team. It uses
extensions of the global features proto to implement this. One thing that was
left a bit ambiguous was who should own these extensions. Language, code
generator, and runtime implementations are all similar but not identical
distinctions.
"Editions Zero Feature: utf8_validation" (not available externally, though a
later version,
"Editions Zero: utf8_validation Without Problematic Options"
is) is a recent plan to add a new set of generator features for utf8 validation.
While the sole feature we had originally created (legacy_closed_enum in Java
and C++) didn't have any ambiguity here, this one did. Specifically in Python,
the current behaviors across proto2/proto3 are distinct for all 3
implementations: pure python, Python/C++, Python/upb.
Overview
In meetings, we've discussed various alternatives, captured below. The original
plan was to make feature extensions runtime implementation-specific (e.g. C++,
Java, Python, upb). There are some notable complications that came up though:
Polyglot - it's not clear how upb or C++ runtimes should behave in
multi-language situations. Which feature sets do they consider for runtime
behaviors? Note: this is already a serious issue today, where all proto2
strings and many proto3 strings are completely unsafe across languages.Shared Implementations - Runtimes like upb and C++ are used as backing
implementations of multiple other languages (e.g. Python, Rust, Ruby, PHP).
If we have a single set ofupborcppfeatures, migrating to those
shared implementations would be more difficult (since there's no independent
switches per-language). Note: this is already the situation we're in today,
where switching the runtime implementation can cause subtle and dangerous
behavior changes.
Given that we only have two behaviors, and one of them is unambiguous, it seems
reasonable to punt on this decision until we have more information. We may
encounter more edge cases that require feature extensions (and give us more
information) during the rollout of edition zero. We also have a lot of freedom
to re-model features in later editions, so keeping the initial implementation as
simple as possible seems best (i.e. Alternative 2).
Alternatives
Alternative 1: Runtime Implementation Features
Features would be per-runtime implementation as originally described in
"Editions Zero Feature: utf8_validation." For example, Protobuf Python users
would set different features depending on the backing implementation (e.g.features.(pb.cpp).<feature>, features.(pb.upb).<feature>).
Pros
- Most consistent with range of behaviors expressible pre-Editions
Cons
- Implementation may / should not be obvious to users.
- Lack of levers specifically for language / implementation combos. For
example, there is no way to set Python-C++ behavior independently of C++
behavior which may make migration harder from other Python implementations.
Alternative 2: Generator Features
Features would be per-generator only (i.e. each protoc plugin would own one set
of features). This was the second decision we made in later discussions, and
while very similar to the above alternative, it's more inline with our goal of
making features primarily for codegen.
For example, all Python implementations would share the same set of features
(e.g. features.(pb.python).<feature>). However, certain features could be
targeted to specific implementations (e.g.features.(pb.python).upb_utf8_validation would only be used by Python/upb).
Pros
- Allows independent controls of shared implementations in different target
languages (e.g. Python's upb feature won't affect PHP).
Cons
- Possible complexity in upb to understand which language's features to
respect. UPB is not currently aware of what language it is being used for. - Limits in-process sharing across languages with shared implementations (e.g.
Python upb, PHP upb) in the case of conflicting behaviors.- Additional checks may be needed.
Alternative 3: Migrate to bytes
Since this whole discussion revolves around the utf8 validation feature, one
option would be to just remove it from edition zero. Instead of adding a new
toggle for UTF8 behavior, we could simply migrate everyone who doesn't enforce
utf8 today to bytes. This would likely need another new codegen feature for
generating byte getters/setters as strings, but that wouldn't have any of the
ambiguity we're seeing today.
Unfortunately, this doesn't seem feasible because of all the different behaviors
laid out in "Editions Zero Feature: utf8_validation." UTF8 validation isn't
really a binary on/off decision, and it can vary widely between languages. There
are many cases where UTF8 is validated in some languages but not others, and
there's also the C++ "hint" behavior that logs errors but allows invalid UTF8.
Note: This could still be partially done in a follow-up LSC by targeting
specific combinations of the new feature that disable validation in all relevant
languages.
Pros
- Punts on the issue, we wouldn't need any upb features and C++ features would
all be code-gen only - Simplifies the situation, avoids adding a very complicated feature in
edition zero
Cons
- Not really possible given the current complexity
- There are O(10M) proto2 string fields that would be blindly changed to bytes
Alternative 4: Nested Features
Another option is to allow for shared feature set messages. For example, upb
would define a feature message, but not make it an extension of the globalFeatureSet. Instead, languages with upb implementations would have a field of
this type to allow for finer-grained controls. C++ would both extend the globalFeatureSet and also be allowed as a field in other languages.
For example, python utf8 validation could be specified as:
We could have checks during feature validation that enforce that impossible
combinations aren't specified. For example, with our current implementationfeatures.(pb.python).cpp should always be identical to features.(pb.cpp),
since we don't have any mechanism for distinguishing them.
Pros
- Much more explicit than options 1 and 2
Cons
- Maybe too explicit? Proto owners would be forced to duplicate a lot of
features
Design Editions Editions Feature Visibility (`docs/design/editions/editions-feature-visibility.md`)
Editions Feature Visibility
Authors: @mkruskal-google
Approved: 2023-09-08
Background
While Editions: Life of a FeatureSet handles
how we propagate features to runtimes, what's left under-specified is how the
runtimes should expose features to their users. Exposing Editions Feature Sets
(not available externally) was an initial attempt to cover both these topics
(specifically the C++ API section), but much of it has been redesigned since.
This is a much more targeted document laying out how features should be treated
by runtimes.
Problem Description
There are two main concerns from a runtime's perspective:
Direct access to resolved features protos - While runtime decisions
should be made based on the data in these protos, their struct-like nature
makes them very rigid. Once users start to depend on the proto API, it makes
it very difficult for us to do internal refactoring. These protos are also
naturally structured based on how feature specification is done in proto
files, rather than the actual behaviors they represent. This makes it
difficult to guarantee that complex relationships between features and other
conditions are being uniformly handled.Accidental use of unresolved features - Unresolved features represent a
clear foot-gun for users, that could also cause issues for us. Since they
share the same type as resolved features, it's not always easy to tell the
two apart. If runtime decisions are made using unresolved features, it's
very plausible that everything will work as expected in a given edition by
coincidence. However, when the proto's edition is bumped, it will very
likely break this code unexpectedly.
Some concrete examples to help illustrate these concerns:
Remodeling features - We've bounced back and forth on how UTF8
validation should be modeled as a feature. None of the proposals resulted in
any functional changes, since edition zero preserves all proto2/proto3
behavior, the question was just about what features should be used to
control them. While the.protofile large-scale change to bump them to the
next edition containing these changes is unavoidable, we'd like to avoid
having to update any code simultaneously. If everyone is directly inspecting
theutf8_validationfeature, we would need to do both.Incomplete features - Looking at a feature like
packed, it's really
more of a contextual suggestion than a strict rule. If it's set at the
file level, all fields will have the feature even though only packable
ones will actually respect it. Giving users direct access to this feature
would be problematic, because they would also need to check if it's
packable before making decisions based on it. Field presence is an even more
complicated example, where the logic we want people making runtime decisions
based on is distinct from what's specified in the proto file.Optimizations - One of the major considerations in Exposing Editions
Feature Sets (not available externally) was whether or not it would be
possible to reduce the cost of editions later. Every descriptor is going to
contain two separate features protos, and it's likely this will end up
getting expensive as we roll out edition zero. We could decide to optimize
this by storing them as a custom class with a much more compact memory
layout. This is similar to other optimizations we've done to descriptor
classes, where we have the freedom to because we don't generally expose
them as protos.Bumpy Edition Large-scale Change - The proto team is going to be
responsible for rolling out the next edition to internal Google repositories
every year (at least 80% of it per our churn policy). We expect that
people are only making decisions based on resolved features, and therefore
that Prototiller transformations are behavior-preserving (despite changing
the unresolved features). If people have easy access to unresolved features
though, we can expect a lot of Hyrum's law issues slowing down these
large-scale changes.
Recommended Solution
We recommend a conservative approach of hiding all FeatureSet protos from
public APIs whenever possible. This means that there should be no publicfeatures() getter, and that features should be stripped from any descriptor
options. All options() getters should have an unset features field. Instead,
helper methods should be provided on the relevant descriptors to encapsulate the
behaviors users care about. This has already been done for edition zero features
(e.g. has_presence, requires_utf8_validation, etc), and we should continue
this model.
The one notable place where we can't completely hide features is in
reflection. Most of our runtimes provide APIs for converting descriptors back to
their original state at runtime (e.g. CopyTo and DebugString in C++). In
order to give a faithful representation of the original proto file in these
cases, we should include the unresolved features here. Given how inefficient
these methods are and how hard the resulting protos are to work with, we expect
misuse of these unresolved features to be rare.
Note: While we may need to adjust this approach in the future, this is the
one that gives us the most flexibility to do so. Adding a new API when we have
solid use-cases for it is easy to do. Removing an existing one when we decide we
don't want it has proven to be very difficult.
Enforcement
While we make the recommendation above, ultimately this decision should be up to
the runtime owners. Outside of Google we can't enforce it, and the cost would be
a worse experience for their users (not the entire protobuf ecosystem). Inside
of Google, we should be more diligent about this, since the cost mostly falls on
us.
μpb
One notable standout here is μpb, which is a runtime implementation, but not a
full runtime. Since μpb only provides APIs to the wrapping runtime in a target
language, it's free to expose features anywhere it wants. The wrapping language
should be responsible for stripping them out where appropriate.
Pros
Prevents any direct access to resolved feature protos
- Gives us freedom to do internal refactoring
- Allows us to encapsulate more complex relationships
- Users don't have to distinguish between resolved/unresolved features
Limits access to unresolved features
- Accidental usage of these is less likely (especially considering the
above)
- Accidental usage of these is less likely (especially considering the
This should be easy to loosen in the future if we find a real use-case for
features()getters.More inline with our descriptor APIs, which wrap descriptor protos but
aren't strictly 1:1 with them. Options are more an exception here, mostly
due to the need to expose extensions.
Cons
There's no precedent for modifying
options()like this. Up until now it
represented a faithful clone of what was specified in the proto file.Deciding to loosen this in the future would be a bit awkward for
options(). If we stop stripping it, people will suddenly start seeing a
new field and Hyrum's law might result in breakages.Requires duplicating high-level feature behaviors across every language. For
example,has_presencewill need to be implemented identically in every
language. We will likely need some kind of conformance test to make sure
these all agree.
Considered Alternatives
Expose Features
This is the simplest implementation, and was the initial approach taken in
prototypes. We would just have public features() getters in our descriptor
APIs, and keep the unresolved features in options().
Pros
- Very easy to implement
Cons
Doesn't solve any of the problems laid out above
Difficult to reverse later
Hide Features in Generated Options
This is a tweak of the recommended solution where we add a hack to the generated
options messages. Instead of just stripping the features out and leaving an
empty field, we could give the features fields "package-scoped" visibility
(e.g. access tokens in C++). We would still strip them, but nobody outside of
our runtimes could even access them to see that they're empty. This eliminates
the Hyrum's law concern above.
Pros
- Resolves one of the cons in the recommended approach.
Cons
We'd have to do this separately for each runtime, meaning specific hacks in
every code generatorNo clear benefit. This only helps if we decide to expose features and
if a bunch of people start depending on the fact thatfeaturesare
always empty.
ClangTidy warning Options Features
Similar to the above alternative, but leverages ClangTidy to warn users against
checking options().features().
Pros
- Resolves one of the cons in the recommended approach.
Cons
Doesn't work in every language
Doesn't work in OSS
Design Editions Editions Life Of A Featureset (`docs/design/editions/editions-life-of-a-featureset.md`)
Editions: Life of a FeatureSet
Author: @mkruskal-google
Approved: 2023-08-17
Background
Outside of some minor spelling tweaks, our current implementation of features
has very closely followed the original design laid out in
Protobuf Editions Design: Features. This
approach led to the creation of four different feature sets for each descriptor
though, and it's left under-specified who is responsible for generating these
(protoc, plugins, runtimes), who has access to them, and where they need to be
propagated to.
Exposing Editions Feature Sets (not available externally) was a first attempt
to try to define some of these concepts. It locks down feature visibility to
protoc, generators, and runtimes. Users will only be exposed to them indirectly,
via codegen changes or runtime helper functions, in order to avoid Hyrum's law
cementing every decision we make about them. We (incorrectly) assumed that the
protoc frontend would be able to calculate all the feature sets and then
propagate all four sets to the generators, who would then forward the fully
resolved runtime features to the runtime. This had the added benefit that we
could treat our C++ feature resolution logic as a source-of-truth and didn't
have to reimplement it identically in every language we support.
Editions: Runtime Feature Set Defaults (not available externally) was a
follow-up attempt to specifically handle the default feature sets of an edition.
We had realized that we would need proto2/proto3 default features in each
language to safely roll out editions, and that languages supporting descriptor
pools would have cases that bypass protoc entirely. The solution we arrived at
was that we should continue using the protoc frontend as the source-of-truth,
and propagate these defaults down to the necessary runtimes. This would fix the
proto2/proto3 issue, and at least provide some utilities to make the situation
easier for descriptor pool users.
Protobuf Editions Design: Features
defines the feature resolution algorithm, which can be summarized by the
following diagram:
Feature resolution for a given descriptor starts by using the proto file's
edition and the feature schemas to generate the default feature set. It then
merges all of the parent features from top to bottom, merging the descriptor's
features last.
Glossary
We will be discussing features a lot in this document, but the meaning
behind the word can vary in some subtle ways depending on context. Whenever it's
ambiguous, we will stick to qualifying these according to the following
definitions:
Global features - The features contained directly in
FeatureSetas
fields. These apply to the protobuf language itself, rather than any
particular runtime or generator.Generator features - Extensions of
FeatureSetowned by a specific
runtime or generator.Feature resolution - The process of applying the algorithm laid out in
Protobuf Editions Design: Features.
This means that edition defaults, parent features, and overrides have all
been merged together. After resolution, every feature should have an
explicit value.Unresolved features - The features a user has explicitly set on
their descriptors in the.protofile. These have not gone through
feature resolution and are a minimal representation that require more
knowledge to be useful.Resolved features - Features that have gone through feature
resolution, with defaults and inheritance applied. These are the only
feature sets that should be used to make decisions.
Option Retention - We support a retention specification on all options
(see
here),
including featuresSource features - The features available to protoc and generators,
before option retention has been applied. These can be either resolved
or unresolved.Runtime features - The features available to runtimes after option
retention has been applied. These can be either resolved or unresolved.
Problem Description
The flaw that all of these design documents suffer from is that protoc can't
be the universal source-of-truth for feature resolution under the original
design. For global features, there's of course no issue (protoc has a
bootstrapping setup for `descriptor.proto`` and always knows the global feature
set). For generator features though, we depend on imports to make them
discoverable.
If a user is actually overriding one of these features, there will necessarily
be an import and therefore protoc will be able to discover generator features
and handle resolution. However, if the user is ok with the edition defaults
there's no need for an import. Without the import, protoc has no way of
knowing that those generator features exist in general. We could hardcode the
ones we own, but that just pushes the problem off to third-party plugins. We
could also force proto owners to include imports for every (transitive)
language they generate code to, even if they're unused, but that would be very
disruptive and isn't practical or idiomatic.
Pushing the source-of-truth to the generators makes things a little better,
since they each know exactly what feature file needs to be included. There's no
longer any knowledge gap, and we don't need to rely on imports to discover the
feature extension. Additionally, many of our generators are written in C++ (even
non-built-in plugins), so we could at least reuse our existing feature
resolution utility for all of those and limit the amount of duplication
necessary. However, there's still a code-size issue with this approach. As
described in the previous documents, we would need to send four feature sets for
every descriptor to the runtime (i.e. in the generator request and embedded
as a serialized string). We wouldn't be able to use inheritance or references to
minimize the cost, and every generator that embeds a FileDescriptorProto into
its gencode would see a massive code-size increase.
There's also still the issue of descriptor pools that need to be able to build
descriptors at runtime. These are typically power users (and our own unit-tests)
doing very atypical things and bypassing protoc entirely. In previous documents
we've attempted to push some of the cost onto them by explicitly not giving them
feature resolution. They would have to specify every feature on every
descriptor, and would not be able to use edition defaults or inheritance.
However, this cost is fairly high and it also makes the edition field
meaningless. Any missing feature would be a runtime error, and there would be no
concept of "edition". This creates an inconsistent experience for developers,
where they think in terms of editions in one context and then throw it out in
another. Also, it would mean that we have two distinct ways of specifying a
`FileDescriptorProto``: with unresolved features meant to only go through
protoc, and with fully resolved features meant to always bypass protoc.
Round-tripping descriptors would become difficult or impossible.
The following image attempts illustrates the issue:
Here, a proto file is used in both A and B runtimes. The schema itself only
overrides features for A though, and doesn't declare an import on B's features.
This means that protoc doesn't know about B's features, and Generator B will
need to resolve them. Additionally, dynamic messages in both A and B runtimes
have issues because they've bypassed protoc and don't have any way to follow the
feature resolution spec.
Requirements
The following minimal feature sets are required by protoc:
- Resolved global source features - to make proto-level decisions
- Unresolved global source features - for validation
For each generator:
- Resolved generator source features - to make language-specific codegen
decisions - Unresolved generator source features - for validation
- Resolved global source features - to make more complex decisions
For each runtime:
- All resolved runtime features - for making runtime decisions
- All unresolved runtime features - for round-trip behavior and debugging
With some additional requirements on an ideal solution:
Minimal code-size costs - code size bloat can easily block the rollout
of editions, and once those limits are hit we don't have great solutionsMinimal performance costs - we want a solution that avoids any
unnecessary CPU or RAM regressionsMinimal code duplication - obviously we want to minimize this, but where
we can't, we need a suitable test strategy to keep the duplication in syncRuntime support for dynamic messages - while dynamic messages are a
less-frequently-used feature, they are a critical feature used by a lot of
important systems. Our solution should avoid making them harder to use in
any runtime that supports them.
Recommended Solution
Our long-term recommendation here is to support and use feature resolution in
every stage in the life of a FeatureSet. Every runtime, generator, and protoc
itself will all handle feature resolution independently, only sharing unresolved
features between each other. This will necessarily mean duplication across
nearly every language we support, and the following sections will go into detail
about strategies for managing this.
The main justification for this duplication is the simple fact that edition
defaults will be needed almost everywhere. The generators need defaults for
their features to get fully resolved generator features to make decisions on,
and can't get them from protoc in every case. The runtimes need defaults for
both global and generator features in order to honor editions in dynamic
messages and to keep RAM costs down (e.g. the absence of feature overrides
should result in a reference to some shared default object). Since the
calculation of edition defaults is by far the most complicated piece of feature
resolution, with the remainder just being proto merges, it makes everything
simpler to understand if we just duplicate the entire algorithm.
Pros
Resolved feature sets will never be publicly exposed
Our APIs will be significantly simpler, cutting the number of different
types of feature sets by a factor of 2There will be no ambiguity about what a
FeatureSetobject means. It
will always either be unresolved (outside of protobuf code) or fully
resolved on all accessible features (inside protobuf code).RAM and code-size costs will be minimal, since we'll only be storing and
propagating the minimal amount of information (unresolved features)Combats Hyrum's law by allowing us to provide wrappers around resolved
features everywhere, instead of letting people depend on them directly
Minimal duplication on top of what's already necessary (edition
defaults).Dynamic messages will be treated on equal footing to proto files
The necessary feature dependencies will always be available in the
appropriate contextWe can simplify the current implementation since protoc won't need to handle
resolution of imported features.
Cons
Requires duplication of feature resolution in every runtime and every unique
generator language- This means building out additional infrastructure to enforce
cross-language conformance
- This means building out additional infrastructure to enforce
Runtimes Without Reflection
There are various runtimes that do not support reflection or dynamic messages at
all (e.g. Java lite, ObjC). They typically embed the "feature-like" information
they need directly into custom objects in the gencode. In these cases, the
problem becomes a lot simpler because they don't need the full FeatureSet
objects. We don't need to duplicate feature resolution in the runtime, and
the generator can just directly embed the fully resolved features values needed
by the runtime (of course, the generator might still need duplicate logic to get
those).
Staged Rollout for Dynamic Messages
Long-term, we want to be able to handle feature resolution at run-time for any
runtime that supports reflection (and therefore needs FeatureSet objects) to
reduce code-size/RAM costs and support dynamic messages. However, in any
language where these costs are less critical, a staged rollout could be
appropriate. Here, the generator would embed the serialized resolved source
features into the gencode along with the rest of the options. We would use theraw_features field (which should eventually be deleted) to also include the
unresolved features for reflection.
This would allow us to implement and test editions, and unblock the migration of
all non-dynamic cases. A follow-up optimization at a later stage could push this
down the runtime, and only embed unresolved features in the gencode.
Under this scenario, dynamic messages could still allow editions, as long as
fully-resolved features were provided on every descriptor. When we do implement
feature resolution, it will just be a matter of deleting redundant/unnecessary
features, but there should always be a valid transformation from fully-resolved
features to unresolved ones.
C++ Generators
Generators written in C++ are in a better position since they don't require any
code duplication. They could be given visibility to our existing feature
resolution utility to resolve the features themselves. However, a better
alternative is to make improvements to this utility so that some helpers like
the ones we proposed in Exposing Editions Feature Sets can be used to access
the resolved features that already exist.
Protoc works by first parsing the input protofiles and building them into a
descriptor pool. This is the frontend pass, where only the global features are
needed. For built-in languages, the resulting descriptors are passed directly to
the generator for codegen. For plugins, they're serialized into descriptor
protos, rebuilt in a new descriptor pool (in the generator process), and then
sent to the generator code for codegen. In both of these cases, aDescriptorPool build of the protos is done from a binary that necessarily
links in the relevant generator features.
Today, we discover features in the pool which are imported by the protos being
built. This has the hole we mentioned above where non-imported features can't be
discovered. Instead, we will pivot to a more explicit strategy for discovering
features. By default, DescriptorPool will only resolve the global features and
the C++ features (since this is the C++ runtime). A new method will be added toDescriptorPool that allows new feature sets to replace the C++ features for
feature resolution. Generators will register their features via a virtual method
in CodeGenerator and the generator's pool build will take those into account
during feature resolution.
There are a few ways to actually define this registration, which we'll leave as
implementation details. Some examples that we're considering include:
- Have the generator provide its own
DescriptorPoolcontaining the relevant
feature sets - Have the generator provide a mapping of edition -> default
FeatureSet
objects
Expanding on previous designs, we will provide the following API to C++
generators via the CodeGenerator class:
They will have access to all the fully-resolved feature sets of any descriptor
for making codegen decisions, and they will have access to their own unresolved
generator features for validation. The FileDescriptor::CopyTo method will
continue to output unresolved runtime features, which will become unresolved
source features after option retention stripping (which generators should
already be doing), for embedding in the gencode for runtime use.
Example
As an example, let's look at some hypothetical language lang and how it would
introduce its own features. First, if it needs features at runtime it would
create a lang_features.proto file in its runtime directory and bootstrap the
gencode the same as it does for descriptor.proto. It would then also
bootstrap C++ gencode using a special C++-only build of protoc. This can be
illustrated in the following diagram:
This illustrates the bootstrapping setup for a built-in C++ generator. If
generator features weren't needed in the runtime, that red box would disappear.
If this were a separate plugin, the "plugin" box would simply be moved out ofprotoc and protoc could also serve as protoc_cpp.
If lang didn't need runtime features, we would simply put the features proto
in the lang generator and only generate C++ code (using the same bootstrapping
technique as above).
After the generator registers lang_features.proto with the DescriptorPool, theFeatureSet objects returned by GetFeatures will always have fully resolvedlang features.
Non-C++ Generators
As we've shown above, non-C++ generators are already in a situation where they'd
need to duplicate some of the feature resolution logic. With this solution,
they'd need to duplicate much more of it. The GeneratorRequest from protoc
will provide the full set of unresolved features, which they will need to
resolve and apply retention stripping to.
Note: If we're able to implement bidirectional plugin communication, the
Bidirectional Plugins alternative may be a simpler
solution for non-C++ generators that don't need features at runtime. Ones that
need it at runtime will need to reimplement feature resolution anyway, so it may
be less useful.
One of the trickier pieces of the resolution logic is the calculation of edition
defaults, which requires a lot of reflection. One of the ideas mentioned above
in C++ Generators could actually be repurposed to avoid
duplication of this in non-C++ generators as well. The basic idea is that we
start by defining a proto:
message EditionFeatureDefaults {
message FeatureDefaults {
string edition = 1;
FeatureSet defaults = 2;
}
repeated FeatureDefaults defaults = 1;
string minimum_edition = 2;
string maximum_edition = 3;
}This can be filled from any feature set extension to provide a much more usable
specification of defaults. We can package a genrule that converts from feature
protos to a serialized EditionFeatureDefaults string, and embed this anywhere
we want. Both C++ and non-C++ generators/runtimes could embed this into their
code. Once this is known, feature resolution becomes a lot simpler. The hardest
part is creating a comparator for edition strings. After that, it's a simple
search for the lower bound in the defaults, followed by some proto merges.
Bootstrapping
One major complication we're likely to hit revolves around our bootstrapping ofdescriptor.proto. In languages that have dynamic messages, one codegen
strategy is to embed the FileDescriptorProto of the file and then parse and
build it at the beginning of runtime. For descriptor.proto in particular,
handling options can be very challenging. For example, in Python, we
intentionally strip all options from this file and then assume that the options
descriptors always exist during build (in the presence of serialized options).
Since features are options, this poses a challenge that's likely to vary
language by language.
We will likely need to special-case descriptor.proto in a number of ways.
Notably, this file will never have any generator feature overrides, since it
can't import those files. In every other case, we can safely assume that
generator features exist in a fully resolved feature set. But fordescriptor.proto, at least at the time it's first being built by the runtime,
this extension won't be present. We also can't figure out edition defaults at
that point since we don't have the generator features proto to reflect over.
One possible solution would be to codegen extra information specifically for
this bootstrapped proto, similar to what we suggested in Editions: Runtime
Feature Set Defaults for edition defaults. That would allow the generator to
provide enough information to build descriptor.proto during runtime. As long
as these special cases are limited to descriptor.proto though, it can be left
to a more isolated language-specific discussion.
Conformance Testing
Code duplication means that we need a test strategy for making sure everyone
stays conformant. We will need to implement a conformance testing framework for
validating that all the different implementations of feature resolution agree.
Our current conformance tests provide a good model for accomplishing this, even
though they don't quite fit the problem (they're designed for
parsing/serialization). There's a runner binary that can be hooked up to another
binary built in any language. It sends a ConformanceRequest proto with a
serialized payload and set of instructions, and then receives aConformanceResponse with the result. In the runner, we just loop over a number
of fixed test suites to validate that the supplied binary is conformant.
We would want a similar setup here for language-agnostic testing. While we could
write a highly focused framework just for feature resolution, a more general
approach may set us up better in the future (e.g. option retention isn't
duplicated now but could have been implemented that way). This will allow us to
test any kind of transformation to descriptor protos, such as: proto3_optional,
group/DELIMITED, required/LEGACY_REQUIRED. The following request/response protos
describe the API:
message DescriptorConformanceRequest {
// The file under test, pre-transformation.
FileDescriptorProto file = 1;
// The pool of dependencies and feature files required for build.
FileDescriptorSet dependencies = 2;
}
message DescriptorConformanceResponse {
// The transformed file.
FileDescriptorProto file = 1;
// Any additional features added during build.
FileDescriptorSet added_features = 2;
}Each test point would construct a proto file, its dependencies, and any feature
files to include in feature resolution. The conformance binary would use this to
fully decorate the proto file with resolved features, and send the result back
for comparison against our C++ source-of-truth. Any generator features added by
the binary will also need to be sent back to get matching results.
Documentation
Because we're now asking third-party generator owners to handle feature
resolution on their own, we will need to document this. Specifically, we need to
open-source documentation for:
- The algorithm described in
Protobuf Editions Design: Features - The conformance test framework and how to use it (once it's implemented)
On the other hand, we will have significantly less documentation to write about
which feature sets to use where. Descriptor protos will always contain
unresolved features, and C++ generators will have a simple API for getting the
fully-resolved features.
Considered Alternatives
Use Generated Pool for C++ Generators
Note: this was part of the original proposal, but has been refactored (see
cons)
Generators written in C++ are in a better position since they don't require any
code duplication. They could be given visibility to our existing feature
resolution utility to resolve the features themselves. However, a better
alternative is to make improvements to this utility so that some helpers like
the ones we proposed in Exposing Editions Feature Sets can be used to access
the resolved features that already exist.
Protoc works by first parsing the input protofiles and building them into a
descriptor pool. This is the frontend pass, where only the global features are
needed. For built-in languages, the resulting descriptors are passed directly to
the generator for codegen. For plugins, they're serialized into descriptor
protos, rebuilt in a new descriptor pool (in the generator process), and then
sent to the generator code for codegen. In both of these cases, aDescriptorPool build of the protos is done from a binary that necessarily
links in the relevant generator features.
However, the FeatureSets we supply to generators are transformed to the
generated pool (i.e. FeatureSet objects rather than Message) where the
generator features will always exist. We've decided that there's no longer any
reason to scrape the imports for features, but we could scrape the generated
pool for them. This essentially means that when you call MergeFeatures to get
a FeatureSet, the returned set is fully resolved with respect to the current
generated pool. This is a much clearer contract, and has the benefit that the
features visible to every C++ generator would automatically be populated with
the correct generator features for them to use.
Expanding on previous designs, we will provide the following API to C++
generators via the CodeGenerator class:
They will have access to all the fully-resolved feature set of any descriptor
for making codegen decisions, and they will have access to their own unresolved
generator features for validation. The FileDescriptor::CopyTo method will
continue to output unresolved runtime features, which will become unresolved
source features after option retention stripping (which generators should
already be doing), for embedding in the gencode for runtime use.
Pros
- Automatic inclusion of any features used in a binary
- Features will never be partially resolved
Cons
- Implicit action at a distance could cause unexpected behaviors
- Uses globals, making testing awkward
- Not friendly to
DescriptorPoolcases who wouldn't necessarily want every
linked-in feature to go through feature resolution.
Default Placeholders
Protoc continues to propagate and resolve core features and imported language
level features. For language level features that protoc does not know about
(that is, not imported), a core placeholder feature indicating that the default
for a given edition should be respected can be propagated.
message FeatureSet {
optional string unknown_feature_edition_default = N; // e.g. 2023
}Instead of duplicating the entire feature resolution algorithm, plugins must
only provide a utility mapping editions to their default FeatureSet using the
generator feature files and optionally caching them.
For example:
if features.hasUtf8Validation():
return features.getUtf8Validation()
else:
default_features = getDefaultFeatures(features.getUnknownFeatureEditionDefault())
return default_features.getUtf8Validation()Pros
- Less duplicate logic for propagating features
Cons
- Descriptor proto bloat that is technically redundant with
FileDescriptorProtoedition. - Confusing that some but not all features are fully resolved
- Duplicated logic to resolve edition default from edition #
- Code-size and memory costs associated with the original approach still exist
- Still doesn't help with the descriptor pool case, which may require
duplicate logic.
Bidirectional Plugins
Since the generators know the features they care about, we could have some kind
of bidirectional communication between protoc and the plugins. The plugin would
start by telling protoc the features it wants added, and then protoc would be
able to fully resolve all feature sets before sending them off. This has the
added benefit that it would allow us to do more interesting enhancements in the
future. For example, the plugin could send its minimum required edition and
other requirements before actually starting the build.
Note: Bidirectional plugins could still be implemented for other purposes.
This "alternative" is specifically for using that communication to pass
missing feature specs.
Pros
- Eliminates code duplication problem
- Provides infrastructure to enable future enhancements
Cons
- Doesn't address the confusing API we have now where it's unclear what kind
of features are contained in thefeaturesfield - Doesn't address the code-size and memory costs during runtime
- Doesn't address the descriptor pool case
Central Feature Registry
Instead of relying on generators and imports to supply feature specs, we could
pivot to a central registry of all known features. Instead of simply claiming an
extension number, generator owners could be required to submit all the feature
protos to a central repository of feature protos. This would give protoc access
to all features. There would be two ways to implement this:
If it were built into protoc, we could avoid requiring any import
statements. We would probably still want an extension point to avoid adding
a dependency todescriptor.proto, but instead offeatures.(pb.cpp)they
would be something more likefeatures.(pb).cpp.We could keep the current extension and import scheme. Proto files would
still need to import the features they override, but protoc would depend on
all of them and populate defaults for unspecified ones.
Pros
- Makes all features easily discoverable wherever they're needed
- Eliminates the code duplication problem
- Gives us an option to remove the import statements, which are likely to
cause future headaches (in the edition zero LSC, in maintenance afterward,
and also for proto files that need to support a lot of third-party
runtimes).
Cons
- Doesn't address the code-size and memory costs
- Creates version skew problems
- Confusing ownership semantics
Do Nothing
Doing nothing would basically mean abandoning editions. The current design
doesn't (and can't) work for third party generators. They'd be left to duplicate
the logic themselves with no guidance or support from us. We would also see
code-size and RAM bloat (except in C++) that would be very difficult to resolve.
Pros
- Less work
Cons
- Worse in every other way
Design Editions Group Migration Issues (`docs/design/editions/group-migration-issues.md`)
Editions: Group Migration Issues
Authors: @mkruskal-google
Summary
Address some unexpected issues in delimited encoding in edition 2023 before its
OSS release.
Background
Joshua Humphries reported some well-timed
issues discovered
while experimenting with our early release of Edition 2023. He discovered that
our new message encoding feature piggybacked a bit too much on the old group
logic, and actually ended up being virtually useless in general.
None of our testing or migrations caught this because they were heavily focused
on preserving old behavior (which is the primary goal of edition 2023).
Delimited messages structured exactly like proto2 groups (e.g. message and field
in the same scope with matching names) continued to work exactly as before,
making it seem like everything was fine.
All of this is especially problematic in light of Submessages: In Pursuit of a
More Perfect Encoding (not available externally yet), which intends to migrate the
ecosystem to use delimited encoding everywhere. Releasing a semi-broken feature
as a migration tool to eliminate a deprecated syntax is one thing, but trying to
push the ecosystem to it is especially bad.
Overview
The problems here stem from the fact that before edition 2023, the field and
type name of group fields was guaranteed to always be unique and intuitive.
Proto2 splits groups into a synthetic nested message with a type name equivalent
to the group specification (required to be capitalized), and a field name that's
fully lowercased. For example,
optional group MyGroup = 1 { ... }would become:
message MyGroup { ... }
optional MyGroup mygroup = 1;The casing here is very important, since the transformation is irreversible. We
can't recover the group name from the field name in general, only if the group
is a single word.
The problem under edition 2023 is that we've removed the generation of
synchronized synthetic messages from the language. Users now explicitly define
messages, and any message field can be marked DELIMITED. This means that
anyone assuming that the type and field name are synchronized could now be
broken.
Codegen
While using the field name for generated APIs required less special-casing in
the generators, the field name ends up producing slightly-less-readable APIs for
multi-word camelcased groups. The result is that we see a fairly random-seeming
mix in different generators. Using protoc-explorer (not available externally),
we find the following:
| Language | Generated APIs | Example proto2 getter |
|---|---|---|
| C++ | field | MyGroup mygroup() |
| Java (all) | message | MyGroup getMyGroup() |
| Python | field | mygroup |
| Go (all) | field | GetMygroup() *Foo_MyGroup |
| Dart V1 | field/message* | get mygroup |
| upb ** | field | Foo_mygroup() |
| Objective-c | message | MyGroup* myGroup |
| Swift | message | MyGroup myGroup |
| C# | field/message* | MyGroup Mygroup |
* This codegen difference was caught during the implementation
and intentionally "fixed" in Edition 2023.
** This includes all upb-based runtimes as well (e.g. Ruby, Rust, etc.)
† Extensions use field
In the Dart V1 implementation, we decided to intentionally introduce a behavior
change on editions upgrades. It was determined that this only affected a handful
of protos in google3, and could probably be manually fixed as-needed. Java's
handling changes the story significantly, since over 50% of protos in google3
produce generated Java code. Objective-C is also noteworthy since we open-source
it, and Swift because it's widely used in OSS and we don't own it.
While the editions upgrade is still non-breaking, it means that the generated
APIs could have very surprising spellings and may not be unique. For example,
using the same type for two delimited fields in the same containing message will
create two sets of generated APIs with the same name in some languages!
Text Format
Our "official"
draft specification
of text-format explicitly states that group messages are encoded by the message
name, rather than the lowercases field name. A group MyGroup will be
serialized as:
MyGroup {
...
}In C++, we always serialize the message name and have special handling to only
accept the message name in parsing. We also have conformance tests locking down
the positive path here (i.e. using the message name round-trip). The negative
path (i.e. failing to accept the field name) doesn't have a conformance test,
but C++/Java/Python all agree and there's no known case that doesn't.
To make things even stranger, for extensions (group fields extending other
messages), we always use the field name for groups. So as far as group
extensions are concerned, there's no problem for editions.
There are a few problems with non-extension group fields in editions:
- Refactoring the message name will change any text-format output
- New delimited fields will have unexpected text-format output, that could
conflict with other fields - Text parsers will expect the message name, which is surprising and could be
impossible to specify uniquely
Recommendation
Clearly the end-state we want is for the field name to be used in all generated
APIs, and for text-format serialization/parsing. The only questions are: how do
we get there and can/should we do it in time for the 2023 release in 27.0 next
month?
We propose a combination of the alternatives listed below.
Smooth Extension seems like the best short-term path
forward to unblock the delimited migration. It mostly solves the problem and
doesn't require any new features. The necessary changes for this approach have
already been prepared, along with new conformance tests to lock down the
behavior changes.
Global Feature is a good long-term mitigation for tech debt
we're leaving behind with Smooth Extension. Ultimately we would like to remove
any labeling of fields by their type, and editions provides a good mechanism to
do this. Alternatively, we could implement aliases and use that to
unify this old behavior and avoid a new feature. Either of these options will be
the next step after the release of 2023, with aliases being preferred as long as
the timing works out.
If we hit any unexpected delays, Nerf Delimited Encoding in 2023 (not available
externally) is the quickest path forward to unblock the release of edition 2023.
It has a lot of downsides though, and will block any migration towards delimited
encoding until edition 2024 has started rolling out.
Alternatives
Smooth Extension {#smooth-extension}
Instead of trying to change the existing behavior, we could expand the current
spec to try to cover both proto2 and editions. We would define a "group-like"
concept, which applies to all fields which:
- Have
DELIMITEDencoding - Have a type corresponding to a nested message directly under its containing
message - Have a name corresponding to its lowercased type name.
Note that proto2 groups will always be "group-like."
For any group-like field we will use the old proto2 semantics, whatever they are
today. Otherwise, we will treat them as regular fields for both codegen and
text-format. This means that most new cases of delimited encoding will have
the desired behavior, while all old groups will continue to function. The main
exception here is that users will see the unexpected proto2 behavior if they
have message/field names that happen to match.
While the old behavior will result in some unexpected capitalization when it's
hit, it's mostly safe. Because of 2 and 3 (and the fact that we disallow
duplicate field names), we can guarantee that in both codegen and text encoding
there will never be any conflicting symbols. There can never be two delimited
fields of the same type using the old behavior, and no other messages or fields
will exist with either spelling.
Additionally, we will update the text parsers to accept both the old
message-based spelling and the new field-based spelling for group-like fields.
This will at least prevent parsing failures if users hit this unexpected change
in behavior.
Pros
- Fully supports old proto2 behavior
- Treats most new editions fields correctly
- Doesn't allow for any of the problematic cases we see today
- By updating the parsers to accept both, we have a migration path to change
the "wire"-format - Decoupled from editions launch (since it's a non-breaking change w/o a
feature)
Cons
- Requires coordinated changes in every editions-compatible runtime (and many
generators) - Keeps the old proto2 behavior around indefinitely, with no path to remove it
- Plants surprising edge case for users if they happen to name their
message/fields a certain way
Global Feature {#global-feature}
The simplest answer here is to introduce a new global message featurelegacy_group_handling to control all the changes we'd like. This will only be
applicable to group-like fields (see
Smooth Extension). With this feature enabled,
these fields will always use their message name for text-format. Each
non-conformant language could also use this feature to gate the codegen rules.
Pros
- Simple boolean to gate all the behavior changes
- Doesn't require adding language features to a bunch of languages that don't
have them yet - Uses editions to ratchet down the bad behavior
Cons
- It's a little late in the game to be introducing new features to 2023
(go/edition-lifetimes) - Requires coordinated changes in every editions-compatible runtime (and many
generators) - The migration story for users is unclear. Overriding the value of this
feature is both a "wire"-breaking and API-breaking change they may not be
able to do easily. - With the feature set, users will still see all of the problems we have today
Feature Suite
An extension of Global feature would be to
split the codegen changes out into separate per-language features.
Pros
- Simple booleans to gate all the distinct behavior changes
- Uses editions to ratchet down the bad behavior
- Better migration story for users, since it separates API and "wire" breaking
changes
Cons
- Requires a whole slew of new language features, which typically have a
difficult first-time setup - Requires coordinated changes in every editions-compatible runtime (and many
generators) - Increases the complexity of edition 2023 significantly
- With the features set, users will still see all of the problems we have
today
Nerf Delimited Encoding in 2023
A quick fix to avoid releasing a bad feature would be to simply ban the case
where the message and field names don't match. Adding this validation to protoc
would cover the majority of cases, although we might want additional checks in
every language that supports dynamic messages.
This is a good fallback option if we can't implement anything better before 27.0
is released. It allows us to release editions in a reasonable state, where we
can fix these issues and release a more functional DELIMITED feature in 2024.
Pros
- Unblocks editions rollout
- Easy and safe to implement
- Avoids rushed implementation of a proper fix
- Avoids runtime issues with text format
- Avoids unexpected build breakages post-editions (e.g. renaming the nested
message)
Cons
- We'd still be releasing a really bad feature. Instead of opening up new
possibilities, it's just "like groups but worse" - We couldn't fix this in 2023 without potential version skew from third party
plugins. We'd likely have to wait until edition 2024 - Might requires coordinated changes in a lot of runtimes
- Doesn't unblock our effort to roll out delimited
Rename Fields in Editions
While it might be tempting to leverage the edition 2023 upgrade as a place we
can just rename the group field, that doesn't actually work (e.g. renamemygroup to my_group). Because so many runtimes already use the field name
in generated APIs, they would break under this transformation.
Pros
- Works really well for text-format and some languages
Cons
- Turns 2023 upgrade into a breaking change for many languages
Aliases {#aliases}
We've discussed aliases a lot mostly in the context of Any, but they would be
useful for any encoding scheme that locks down field/message names. If we had a
fully implemented alias system in place, it would be the perfect mitigation
here. Unfortunately, we don't yet and the timeline here is probably too tight to
implement one.
Pros
- Fixes all of the problems mentioned above
- Allows us to specify the old behavior using the proto language, which allows
it to be handled by Prototiller
Cons
- We want this to be a real fully thought-out feature, not a hack rushed into
a tight timeline
Do Nothing
Doing nothing doesn't actually break anyone, but it is embarrassing.
Pros
- Easy to do
Cons
- Releases a horrible feature full of foot-guns in our first edition
- Doesn't unblock our effort to roll out delimited
Design Editions Java Lite For Editions (`docs/design/editions/java-lite-for-editions.md`)
Java Lite For Editions
Author: @zhangskz
Approved: 2023-05-26
Background
The "Lite" implementation for Java utilizes a custom format for embedding
descriptors motivated by critical code-size and performance requirements for
Android.
The code generator for Java Lite encodes an descriptor-like info string which is
stored into RawMessageInfo. This is decoded into MessageSchema which serves
as the descriptor-like schema for Java lite for parsing and serialization.
The current implementation makes significant use of an is_proto3 bit in the
encoding, which is problematic for editions. Note that any parser changes to the
format would also need to maintain backwards compatibility, due to our
guarantees for parsers to remain backwards compatible within a major version.
Overview
Fortunately, we already have corresponding bits for most
Editions Zero Features in the correspondingMessageInfo field entry encoding.
We will move existing remaining syntax usages reading is_proto3 to use these
bits. Several other syntax usages need to be made to be editions compatible by
merging implementations.
As new editions features are added that must be represented in MessageInfo, we
will eventually need to revamp MessageInfo encoding to support these changes.
However, this should be avoidable for Editions Zero.
Recommendation
Encoding: Add Is Edition Bit
RawMessageInfo should be augmented with an additional is_edition bit in
flags' unused bits.
[0]: flags, flags & 0x1 = is proto2?, flags & 0x2 = is message?, flags &
0x4 = is edition?
The decoded ProtoSyntax should add a corresponding Editions option based on
this bit.
public enum ProtoSyntax
PROTO2;
PROTO3;
EDITIONS;For now, there is no need to explicitly encode the raw editions string or
feature options. These resolved features will be encoded directly in their
corresponding field entries.
Encoding: Editions Zero Features
Field entries in RawMessageInfo already encode bits corresponding to most
resolved Editions Zero features in GetExperimentalJavaFieldType. This is
decoded in fieldTypeWithExtraBits by reading the corresponding bits.
| Edition Zero Feature | **Existing Encoding ** | Changes |
|---|---|---|
| features.field_presence | kHasHasBit (0x1000) |
Keep as-is. |
| java.legacy_closed_enum | kMapWithProto2EnumValue (0x800) |
Replace with kLegacyEnumIsClosedBit |
This will now be set for all enum fields, instead of just enum map values.
We will still need to check syntax in the interim in case of gencode. |
| features.enum_type | EnumLiteGenerator writes UNRECOGNIZED(-1) value for open enums in gencode.
This is not encoded in MessageInfo since this is an enum feature. | This is not needed in Editions Zero since enum closedness in Java Lite's runtime is dictated per-field by java.legacy_closed_enum. (Edition Zero Feature: Enum Field Closedness), but should be used when Java non-conformance is fixed.
Note, this is implicitly encoded in kLegacyEnumIsClosedBit if java.legacy_closed_enum is unset since the corresponding FieldDescriptor helper should fall back on the EnumDescriptor. |
| features.repeated_field_encoding | GetExperimentalJavaFieldTypeForPacked | Keep as-is. |
| features.string_field_validation | kUtf8CheckBit (0x200) | Keep as-is.
HINT does not apply to Java and will have the same behavior as MANDATORY or NONE |
| features.message_encoding | Not present. | Encode as type group.
See below. |
Several places already use these bits properly, but there are a few syntax
usages in the decoding that should be replaced by checking the corresponding
feature bit.
There are several unused bits that we could use for future field-level features
before breaking the encoding format, but we should not need these for editions
zero.
The results of the is_proto3 and feature bits only seem to be used within
protobuf, and don't seem to be publicly exposed.
features.message_encoding
In the compiler, message fields with features.message_encoding = DELIMITED
should be treated as a group before encoding message info.
This means that GetExperimentalJavaFieldTypeForSingular, should encode the
field's type GROUP (17), instead of its actual type MESSAGE (9), e.g.
int GetExperimentalJavaFieldTypeForSingular(const FieldDescriptor* field) {
int result = field->type();
if (result == FieldDescriptor::TYPE_MESSAGE) {
if (field->isDelimited()) {
return 17; // GROUP
}
}
}ImmutableMessageFieldLiteGenerator::GenerateFieldInfo calls this when
generating the message field's field info.
The nested message's MessageInfo encoding does not need to be changed as this
is already identical for group and message.
Since each message field will be handled separately, this means that the
post-editions proto file below
// foo.proto
edition = "tbd"
message Foo {
message Bar {
int32 x = 1;
repeated int32 y = 2;
}
Bar bar = 1 [features.message_encoding = DELIMITED];
Bar baz = 2; // not DELIMITED
}will be encoded and treated by MessageSchema like its pre-editions equivalent
below.
message Foo {
group Bar = 1 {
int32 x = 1;
repeated int32 y = 2;
}
Bar baz = 2; // not DELIMITED
}We recommended this alternative to minimize changes to the encoding and how
groups are treated.
In a future breaking change, we could consider renaming FieldType.GROUP toFieldType.MESSAGE_DELIMITED while preserving the same number and encoding for
clarity. For now, we will leave the naming for this enum as-is.
Alternative: Add kIsMessageEncodingDelimitedBit
Alternatively, we could encode features.message_encoding = DELIMITED as-is as
type MESSAGE. The MessageInfo encoding would encode these as a normal
message field, using an unused (0x1100) bit as kIsMessageEncodingDelimitedBit.
This could be used to indicate that the message should be parsed/serialized from
the wire-format as if it were a group. This would need to be passed along toMessageSchema which would then handle treating Messages with this bit set as
groups e.g. in case Message.
This is less ideal, since it would require handling this in multiple places.
Unify non-feature syntax usages
There are several places that branch on syntax into separate proto2/proto3
codepaths. These generally duplicate a lot of code and should be unified into a
single syntax-agnostic code path branching on the relevant feature bits.
This code tends to be pretty opaque, so we should document this with comments or
add helpers (e.g. isEnforceUtf8) to indicate what feature bits are used as we
make changes here.
ManifestSchemaFactory.newSchema() |
MessageInfo -> Schema | Allow extensions for editions. |
|---|---|---|
MessageSchema.getSerializedSize() |
Message -> Serialized Size | Unify getSerializedSizeProto2/3 |
MessageSchema.writeTo() |
Serialize Message | Unify writeFieldsInAscendingOrderProto2/3 |
MessageSchema.mergeFrom() |
Parse Message | Unify parseProto2/3Message |
DescriptorMessageInfoFactory.convert() |
Descriptor -> MessageInfo | Unify convertProto2/3 |
There is a lot of dead code in Java Lite so several syntax usages can also be
deleted or merged where possible.
Alternatives
Alternative 1: Introduce New Backwards-compatible MessageInfo Encoding
Add a new backwards-compatible MessageInfo encoding for editions.
The is_edition bit could toggle the encoding format being used, whereis_edition == true indicates the new encoding format but is_edition == false
indicates the old encoding.
This would allow us to encode additional information that the current encoding
format does not currently have available bits to support, such as the editions
string or additional features.
For example, the current encoding format only has a fixed number of available
field entry bits where we could encode new feature bits. We will need to
introduce a new encoding format once we exceed these, or if we want to encode
features at the message level.
In a future major version bump when support for proto2/3 is officially dropped,
we could drop support for the previous encoding format.
The recommendation is to revisit alternative 1 along with alternative 2
post-Editions zero as we need to support additional feature bits.
Pros
- Future-proof for future editions and features
Cons
- Blocks editions zero on more complex encoding changes that won't be used
yet. - Requires more invasive updates to all MessageInfo decodings
Alternative 2: Move to MiniDescriptor encoding
We could switch Java Lite to use the MiniDescriptor encoding specification.
Like Java Lite, this encoding seems to be optimized to be lightweight and with
minimal descriptor information.
MiniDescriptors do not encode proto2/proto3 syntax currently, which makes it
mostly editions-compatible. MiniDescriptors encode FieldModifier/MessageModifier
bits that correspond to some editions zero similarly to the Java Lite field
feature bits, and can be augmented to support additional features.
Supposedly, this encoding format should support an arbitrary number of
modifier bits, but this needs to be double-checked to verify there isn't a
similar hard limit to the number of features.
It is unclear whether this is sufficiently optimized for Android's needs and how
compatible this would be with Java Lite's Schemas.
The recommendation is to revisit alternative 2 along with alternative 1
post-Editions zero as we need to support additional feature bits.
Pros
Unify implementations for lower long-term maintenance cost
MiniDescriptor encoding will eventually need to be updated for editions
anyways.
Cons
Blocks editions zero on more complex encoding changes that aren't necessary.
Requires even more invasive updates to all MessageInfo decodings
Probably requires major version bumps to break compatibility
Unknown code size /schema compatibility constraints that would need to be
explored.There are a few possible changes to MiniDescriptors on the table that we
should wait to settle before bringing on additional implementations.
Alternative 3: Do Nothing
Doing nothing is always an alternative. Describe the pros and cons of it.
Pros
- No work
Cons
- Editions is blocked since Java Lite protos are stuck in the past
Design Editions Legacy Syntax Editions (`docs/design/editions/legacy-syntax-editions.md`)
Legacy Syntax Editions
Author: @mkruskal-google
Approved: 2023-09-08
Should proto2/proto3 be treated as editions?
Background
Edition Zero Features lays out our plan for edition
2023, which will unify proto2 and proto3. Since early in the design process,
we've discussed the possibility of making proto2 and proto3 "special" editions,
but never laid out what exactly it would look like or determined if it was
necessary.
We recently redesigned editions to be represented as enums
(Edition Naming), and also how edition defaults are
propagated to generators and runtimes
(Editions: Life of a FeatureSet). With these
changes, there could be an opportunity to special-case proto2 and proto3 in a
beneficial way.
Problem Description
While the original plan was to keep editions and syntax orthogonal, that naively
means we'd be supporting two very different codebases. This has some serious
maintenance costs though, especially when it comes to test coverage. We could
expect to have sub-optimal test coverage of editions initially, which would
gradually become poor coverage of syntax later. Since we need to support both
syntax and editions long-term, this isn't ideal.
In the implementation of editions in C++, we decided to unify a lot of the
infrastructure to avoid this issue. We define global feature sets for proto2 and
proto3, and try to use those internally instead of checking syntax directly. By
pushing the syntax/editions branch earlier in the stack, it gives us a lot of
indirect test coverage for editions much earlier.
A separate issue is how Prototiller will support the conversion of syntax to
edition 2023. For features it knows about, we can hardcode defaults into the
transforms. However, third party feature owners will have no way of signaling
what the old proto2/proto3 behavior was, so Prototiller won't be able to provide
any transformations by default. They'd need to provide custom Prototiller
transforms hardcoding all of their features.
Recommended Solution
We recommend adding two new special editions to our current set:
enum Edition {
EDITION_UNKNOWN = 0;
EDITION_PROTO2 = 998;
EDITION_PROTO3 = 999;
EDITION_2023 = 1000;
}These will be treated the same as any other edition, except in our parser which
will reject edition = "proto2" and edition = "proto3" in proto files. The
real benefit here is that this allows features to specify what their
proto2/proto3 defaults are, making it easier for Prototiller to handle
migration. It also allows generators and runtimes to unify their internals more
completely, treating proto2/proto3 files exactly the same as editions.
Serialized Descriptors
As we now know, there are a lot of serialized descriptor.proto descriptor sets
out there that need to continue working for O(months). In order to avoid
blocking edition zero for that long, we may need fallbacks in protoc for the
case where feature resolution fails. If the file is proto2/proto3, failure
should result in a fallback to the existing hardcoded defaults. We can remove
these later once we're willing to break stale descriptor.proto snapshots that
predate the changes in this doc.
Bootstrapping
In order to get feature resolution running in proto2 and proto3, we need to be
able to support bootstrapped protos. For these builds, we can't use any
reflection without deadlocking, which means feature defaults can't be compiled
during runtime. We would have had to solve this problem anyway when it came time
to migrate these protos to editions, but this proposal forces our hand early.
Luckily, "Editions: Life of a FeatureSet" already set us up for this scenario,
and we have Blaze rules for embedding these defaults into code. For C++
specifically, this will need to be checked in alongside the other bootstrapped
protos. Other languages will be able to do this more dynamically via genrules.
Feature Inference
While we can calculate defaults using the same logic as in editions, actually
inferring "features" from proto2/proto3 needs some custom code. For example:
- The
requiredkeyword setsLEGACY_REQUIREDfeature - The
optionalkeyword in proto3 setsEXPLICITpresence - The
groupkeyword impliesDELIMITEDencoding - The
enforce_utf8options flips betweenPACKEDandEXPANDEDencoding
This logic needs to be written in code, and will need to be duplicated in every
language we support. Any language-specific feature transformations will also
need to be included in that language. To make this as portable as possible, we
will define functions like:
Each type of descriptor will have its own set of transformations that should be
applied to its features for legacy editions.
Pros
Makes it clearer that proto2/proto3 are "like" editions
Gives Prototiller a little more information in the transformation from
proto2/proto3 to editions (not necessarily 2023)Allows proto2/proto3 defaults to be specified in a single location
Makes unification of syntax/edition code easier to implement in runtimes
Allows cross-language proto2/proto3 testing with the conformance framework
mentioned in "Editions: Life of a FeatureSet"
Cons
Adds special-case legacy editions, which may be somewhat confusing
We will need to port feature inference logic across all languages. This is
arguably cheaper than maintaining branched proto2/proto3 code in all
languages though
Considered Alternatives
Do Nothing
If we do nothing, there will be no built-in unification of syntax and editions.
Runtimes could choose any point to split the logic.
Pros
- Requires no changes to editions code
Cons
- Likely results in lower test coverage
- May hide issues until we start rolling out edition 2023
- Prototiller would have to hard-code proto2/proto3 defaults of features it
knows, and couldn't even try to migrate runtimes it doesn't
Design Editions Life Of An Edition (`docs/design/editions/life-of-an-edition.md`)
Life of an Edition
Author: @mcy
How to use Protobuf Editions to construct a large-scale change that modifies the
semantics of Protobuf in some way.
Overview
This document describes how to use the Protobuf Editions mechanism (both
editions, themselves, and features) for
designing migrations and large-scale changes intended to solve a particular kind
of defect in the language.
This document describes:
- How features are added to the language.
- How editions are defined and "proclaimed".
- How to build different kinds of large-scale changes.
- Tooling in
protocto support large-scale changes. - An OSS strategy.
Defining Features
There are two kinds of features:
- Global features, which are the fields of
proto.Features. In this document,
we refer to them asfeatures.<name>, e.g.features.enum. - Language-scoped features, which are defined in a typed extension field for
that language. In this document, we refer to them asfeatures.(<lang>).name, e.g.features.(proto.cpp).legacy_string.
Global features require a descriptor.h change, and are relatively heavy
weight, since defining one will also require providing helpers in Descriptor
wrapper classes to avoid the need for users to resolve inheritance. Because they
are not specific to a language, they need to be carefully, visibility
documented.
Language-scoped features require only a change in a backend's feature extension,
which has a smaller blast radius (except in C++ and Java). Often these are
relevant only for codegen and do not require reflective introspection.
Adding a feature is never a breaking change.
Feature Lifetime
In general, features should have an original default and a desired default:
features are intended to gradually flip from one value to another throughout the
ecosystem as migrations progress. This is not always true, but this means most
features will be bools or enums.
Any migration that introduces a feature should plan to eventually deprecate and
remove that feature from both our internal codebase and open source, generally
with a multi-year horizon. Features are transient.
Removing a feature is a breaking change, but it does not need to be tied to an
edition. Feature removal in OSS must thus be batched into a breaking release.
Deletion of a feature should generally be announced to OSS a year in advance.
Do's and Don'ts
Here are some things that we could use features for, very broadly:
- Changing the generated API of any syntax production (name, behavior,
signature, whether it is generated at all). E.g.features.(proto.cpp).legacy_string. - Changing the serialization encoding of a field (so long as it does not break
readers). E.g.,features.packed, eventuallyfeatures.group_encoding. - Changing the deserialization semantics of a field. E.g.,
features.enum,features.utf8.
Although almost any semantic change can be feature-controlled, some things would
be a bit tricky to use a feature for:
- Changing syntax. If we introduce a new syntax production, gating it doesn't
do people much good and is just noise. We should avoid changing how things
are spelled. In Protobuf's history, it has been incredibly rare that we have
needed to do this. - Shape of a descriptor. Features should generally not cause fields, message,
or enum descriptors to appear or disappear. - Names and field numbers. Features should not change the names or field
numbers of syntax entities as seen in a descriptor. This is separate from
using features to change generated API names. - Changing the wire encoding in an incompatible way. Using features to change
the wire format has some long horizons and caveats described below.
Proclaiming an Edition
An edition is a set of default values for all features that protoc's
frontend, and its backends, understand. Edition numbers are announced by
protobuf-team, but not necessarily defined by us. protoc only defines the
edition defaults for global features, and each backend defines the edition
defaults for its features.
Total Ordering of Editions
The FileDescriptorProto.edition field is a string, so that we can avoid nasty
surprises around needing to mint multiple editions per year: even if we mintedition = "2022";, we can mint edition = "2022.1"; in a pinch.
However, protobuf-team does not define editions, it only proclaims them.
Third-party backends are responsible for changing defaults across editions. To
minimize the amount of synchronization, we introduce a total order on
editions.
This means that a backend can pick the default not by looking at the edition,
but by asking "is this proto older than this edition, where I introduced this
default?"
The total order is thus: the edition string is split on '.'. Each component is
then ordered by a.len < b.len && a < b. This ensures that 9 < 10, for
example.
By convention, we will make the edition be either the year, like 2022, or the
year followed by a revision, like 2022.1. Thus, we have the following total
ordering on editions:
2022 < 2022.0 < 2022.1 < ... < 2022.9 < 2022.10 < ... < 2023 < ... < 2024 < ...(Note: The above edition ordering is updated in
Edition Naming.)
Thus, if an imaginary Haskell backend defines a featurefeature.(haskell).more_monads, which becomes true in 2023, the backend can askfile.EditionIsLaterThan("2023"). If it becomes false in 2023.1, a future
version would ask file.EditionIsBetween("2023", "2023.1").
This means that backends only need to change when they make a change to
defaults. However, backends cannot add things to editions willy-nilly. A backend
can only start observing an edition after protobuf-team proclaims the next
edition number, and may not use edition numbers we do not proclaim.
Proclamation
"Proclamation" is done via a two-step process: first, we announce an upcoming
edition some months ahead of time to OSS, and give an approximate date on which
we plan to release a non-breaking version that causes protoc to accept the new
edition. Around the time of that release, backends should make a release adding
support for that edition, if they want to change a default. It is a faux-pas,
but ultimately has no enforcement mechanism, for the meaning of an edition to
change long (> 1 month) after it has been released.
We promise to proclaim an edition once per calendar year, even if first-party
backends will not use it. In the event of an emergency (whatever that means), we
can proclaim a Y.1, Y.2, and so on. Because of the total order, only
backends that desperately need a new edition need to pay attention to the
announcement. As we gain experience, we should define guidelines for third
parties to request an unscheduled edition bump, but for the time being we will
deal with things case-by-case.
We may want to have a canonical way for finding out what the latest edition is.
It should be included in large print on our landing page, and protoc --latest-edition should print the newest edition known to protoc. The intent
is for tooling that wants to generate .proto templates externally can choose
to use the latest edition for new messages.
Large-scale Change Templates
The following are sketches of large-scale change designs for feature changes we
would like to execute, presented as example use-cases.
Large-scale Changes with No Functional Changes: Edition Zero
We need to get the ecosystem into the "editions" syntax. This migration is
probably unique because we are not changing any behavior, just the spelling of a
bunch of things.
We also need to track down and upgrade (by hand) any code that is using the
value of syntax. This will likely be a manual large-scale change performed
either by Busy Beavers or a handful of protobuf-team members furnished with
appropriate stimulants (coffee, diet mountain dew, etc). Once we have migrated
95% of callers of syntax, we will mark all accessors of that field in various
languages as deprecated.
Because the value of syntax becomes unreliable at this point, this will be a
breaking change.
Next, we will introduce the features defined in
Edition Zero Features. We will then implement
tooling that can take a proto2 or proto3 file and add edition = "2023";
and option features.* = ...; as appropriate, so that each file retains its
original behavior.
This second large-scale change can be fully automated, and does not require
breaking changes.
Large-scale Changes with Features Only: Immolation of `required`
We can use features to move fields off of features.field_presence = LEGACY_REQUIRED (the edition’s spelling of required) and ontofeatures.field_presence = EXPLICIT_PRESENCE.
To do this, we introduce a new value for features.field_presence,ALWAYS_SERIALIZE, which behaves like EXPLICIT_PRESENCE, but, if the has-bit
is not set, the default is serialized. (This is sort of like a cross betweenrequired and proto3 no-label.)
It is always safe to turn a proto from LEGACY_REQUIRED to ALWAYS_SERIALIZE,
because required is a constraint on initialization checking, i.e., that the
value was present. This means the only requirement is that old readers not
break, which is accomplished by always providing a value. Because required
fields don't set the value anyways, this is not a behavioral change, but it now
permits writers to veer off of actually setting the value.
After an appropriate build horizon, we can assume that all readers are tolerant
of a potentially missing value (even though no writer would actually be omitting
it). At this point we can migrate from ALWAYS_SERIALIZE toEXPLICIT_PRESENCE. If a reader does not see a record for the field, attempting
to access it will produce the default value; it is not likely that callers are
actually checking for presence of required fields, even though that is
technically a thing you can do.
Once all required fields have gone through both steps, LEGACY_REQUIRED andALWAYS_SERIALIZE can be removed as variants (breaking change).
Large-scale Changes with Editions: absl::string_view Accessors
In C++, a string or bytes typed field has accessors that produce const std::string&s. The missed optimizations of doing this are well-understood, so
we won't rehash that discussion.
We would like to migrate all of them to return absl::string_view, a-lactype = STRING_PIECE.
To do this, we introduce features.(proto.cpp).legacy_string[^1], a boolean
feature by default true. When false on a field of appropriate type, it does the
needful and causes accessors to become representationally opaque.
The feature can be set at file or field scope; tooling (see below) can be used
to minimize the diff impact of these changes. Changing a field may also require
changing code that was previously assuming they could write std::string x = proto.string_field();. This has the usual "unspooling string" migration
caveats.
Once we have applied 95% of internal changes, we will upgrade the C++ backend at
the next edition to default legacy_string to false in the new edition. Tooling
(again, below) can be used to automatically delete explicit settings of the
feature throughout our internal codebase, as a second large-scale change. This
can happen in parallel to closing the loop on the last 5% of required internal
changes.
Once we have eliminated all the legacy accessors, we will remove the feature
(breaking change).
Large-scale Changes with Wire Format Break: Group-Encoded Messages
It turns out that encoding and decoding groups (end-marker-delimited
submessages) is cheaper than handling length-prefixed messages. There are
likely CPU and RAM savings in switching messages to use the group encoding.
Unfortunately, that would be a wire-breaking change, causing old readers to be
unable to parse new messages.
We can do what we did for packed. First, we modify parsers to accept message
fields that are encoded as either groups or messages (i.e., TYPE_MESSAGE andTYPE_GROUP become synonyms in the deserializer). We will let this soak for
three years[^2] and bide our time.
After those three years, we can begin a large-scale change to addfeatures.group_encoded to message fields throughout our internal codebase
(note that groups don't actually exist in editions; they are just messages withfeatures.group_encoded). Because of our long waiting period, it is (hopefully)
unlikely that old readers will be caught by surprise.
Once we are 95% done, we will upgrade protoc to set features.group_encoded to
true by default in new editions. Tooling can be used to clean up features as
before.
We will probably never completely eliminate length-prefixed messages, so this
is a rare case where the feature lives on forever.
Large-scale Change Tooling
We will need a few different tools for minimizing migration toil, all of which
will be released in OSS. These are:
The features GC. Running
protoc --gc-features foo.protoon a file in
editions mode will compute the minimal (or a heuristically minimal, if this
proves expensive) set of features to set on things, given the edition
specified in the file. This will produce a ProtochangifierProtoChangeSpec
that describes how to clean up the file.The editions "adopter". Running
protoc --upgrade-edition -I... file.proto
figure out how to updatefile.protofromproto2orproto3to the
latest edition, adding features as necessary. It will emit this information
as aProtoChangeSpec, implicitly running features GC.The editions "upgrader". Running
protoc --upgrade-editionas above on a
file that is already in editions mode will bump it up to the latest edition
known toprotocand add features as necessary. Again, this emits a
features GC'dProtoChangeSpec.
This is by no means all the tooling we need, but it will simplify the work of
robots and beavers, along with any bespoke, internal-codebase-specific tooling
we build.
The OSS Story
We need to export our large-scale changes into open source to have any hope of
editions not splitting the ecosystem. It is impossible to do this the way we do
large-scale changes in our internal codebase, where we have global approvals and
a finite but nonzero supply of bureaucratic sticks to motivate reluctant users.
In OSS, we have neither of these things. The only stick we have is breaking
changes, and the only carrots we can offer are new features. There is no "global
approval" or "TAP" for OSS.
Our strategy must be a mixture of:
- Convincing users this is a good thing that will help us make Protobuf easier
to use, cheaper to deploy, and faster in production. - Gently steering users to the new edition in new Protobuf definitions,
through protoc diagnostics (when an old edition is going or has gone out of
date) and developer tooling (editor integration, new-file-boilerplate
templates). - Convincing third-party backend vendors (such as Apple, for Swift) that they
can leverage editions to fix mistakes. We should go out of our way to design
attractive migrations for them to execute. - Providing Google-class tooling for migrations. This includes the large-scale
change tooling above, and, where possible, specialized tooling. When it is
not possible to provide tooling, we should provide detailed migration guides
that highlight the benefits. - Being clear that we have a breaking changes policy and that we will
regularly remove old features after a pre-announced horizon, locking new
improvements behind completing migrations. This is a risky proposition,
because users may react by digging in their heels. Comms planning is
critical.
The common theme is comms and making it clear that these are improvements
everyone can benefit from, and that there is no "I" in "ecosystem": using
Protobuf, just like using Abseil, means accepting upgrades as a fact of life,
not something to be avoided.
We should lean in on lessons learned by Go (see: their go fix tool) and Rust
(see: their rustfix tool); Rust in particular has an editions/epoch mechanism
like we do; they also have feature gates, but those are not the same concept as
our features. We should also lean on the Carbon team's public messaging about
upgrading being a fact of life, to provide a unified Google front on the matter
from the view of observers.
Prior Art: Rust Editions
The design of Protobuf Editions is directly
inspired by Rust's own
edition system[^3].
Rust defines and ships a new edition every three years, and focuses on changes
to the surface language that do not inhibit interop: crates of different
editions can always be linked together, and "edition" is a parallel ratchet to
the language/compiler version.
For example, keywords (like async) have been introduced using editions.
Editions have also been used to change the semantics of the borrow checker to
allow new programs, and to change name resolution rules to be more intuitive.
For Rust, an edition may require changes to existing code to be able to compile
again, but only at the point that the crate opts into the new edition, to
obtain some benefit from doing so.
Unlike Protobuf, Rust commits to supporting all past editions in perpetuity:
there is no ratcheting forward of the whole ecosystem. However, Rust does ship
with rustfix (runnable on Cargo projects via cargo fix), a tool that can
upgrade crates to a new edition. Edition changes are required to come with a
migration plan to enable rustfix.
Crates therefore have limited pressure to upgrade to the latest edition. It
provides better features, but because there is no EOL horizon, crates tend to
stay on old editions to support old compilers. For users, this is a great story,
and allows old code to work indefinitely. However, there is a maintenance burden
on the compiler that old editions and new language features (mostly) work
correctly together.
In Rust, macros present a challenge: rich support for interpreted, declarative
macros and compiled, fully procedural macros, mean that macros written for older
editions may not work well in crates written on newer editions, or vice versa.
There are mitigations for this in the compiler, but such fixes cannot be
perfect, so this is a source of difficulties in getting total conversion.
Protobuf does not have macros, but it does have rich descriptors that mirror
input files, and this is a potential source of problems to watch out for.
Overall, Rust's migration story is poor: they have accepted they need to support
old editions indefinitely, but only produce an edition every three years.
Protobuf plans to be much more aggressive, and we should study where Rust's
leniency to old versions is unavoidable and where it is an explicit design
choice.
Notes
[^1]: ctype has baggage and I am going to ignore it for the purposes of
discussion. The feature is spelled legacy_string because adding string
view accessors is not likely the only thing to do, given we probably want
to change the mutators as well.
[^2]: The correct size of the horizon is arbitrary, due to the "budget phones in
India" problem. Realistically we would need to pick one, start the
migration, and halt it if we encounter problems. It is quite difficult to
do better than "hope" as our strategy, but packed is an existence proof
that this is not insurmountable, merely very expensive.
[^3]: Rust also has feature gates, used mostly so that people may start trying
out experimental unstable features. These are largely orthogonal to
editions, and tied to compiler versions. Rust's feature gates generally do
not change the semantics of existing programs, they just cause new
programs to be valid. When a feature is "stabilized", the feature flag is
removed. Feature flags do not participate in Rust's stability promises.
Design Editions Minimum Required Edition (`docs/design/editions/minimum-required-edition.md`)
Minimum Required Edition
Author: @mcy
Approved: 2022-11-15
A versioning mechanism for descriptors that ensures old runtimes do not load
descriptors that are "too new."
Background
Suppose we decide to add a novel definition like
const int32 MY_CONSTANT = 42;to the Protobuf language. This would entail a descriptor change to track the
values of constants, but they would not be loaded properly by older runtimes.
This document describes an addition to descriptor.proto that prevents this
version mismatch issue.
Protobuf Editions intends to add the concept of
an edition to Protobuf, which will be an approximately-annually incrementing
value. Because of their annual nature, and because runtimes need to be updated
to handle new features they implement regardless, we can use them as a poison
pill for old runtimes that try to load descriptors that are "too new."
Overview
We propose adding a new field to FileDescriptorProto:
optional string minimum_required_edition = ...;This field would exist alongside the edition field, and would have the
following semantics:
Every Protobuf runtime implementation must specify the newest edition whose
constructs it can handle (at a particular rev of that implementation). If that
edition is less than minimum_required_edition, loading the descriptor must
fail.
"Less than" is defined per the edition total order given in
Life of an Edition. To restate it, it is the following
algorithm:
def edition_less_than(a, b):
parts_a = a.split(".")
parts_b = b.split(".")
for i in range(0, min(len(parts_a), len(parts_b))):
if int(parts_a[i]) < int(parts_b[i]): return True
return len(a) < len(b)protoc should keep track of which constructions require which minimum edition.
For example, if constants are introduced in edition 2025, but they are not
present in a file, protoc should not require that runtimes understand
constants by picking a lower edition, like 2023 (assuming no other construct
requires a higher edition).
In particular, the following changes should keep the minimum edition constant,
with all other things unchanged:
- An upgrade of the proto compiler.
- Upgrading the specified edition of a file via Prototiller.
Bootstrapping Concerns
"Epochs for descriptor.proto" (not available externally) describes a potential
issue with bootstrapping. It is not the case here: minimum edition is only
incremented once a particular file uses a new feature. Since descriptor.proto
and other schemas used by protoc and the backends would not use new features
immediately, introducing a new feature does not immediately stop the compiler
from being able to compile itself.
Concerns for Schema Producers
Schema producers should consider changes to their schemas that increase the
minimum required edition to be breaking changes, since it will stop compiled
descriptors from being loaded at runtime.
Recommendation
We recommend adding the aforementioned minimum required edition field, along
with the semantics it entails. This logic should be implemented entirely in the
protoc frontend.
Alternatives
Use a Non-Editions Version Number
Rather than using the editions value, use some other version number. This number
would be incremented rarely (3-5 year horizon). This is the approach proposed
in "Epochs for descriptor.proto."
Pros
- Does not re-use the editions value for a semantically-different meaning; the
edition remains being "just" a key into a table of features defaults.
Cons
- Introduces another version number to Protobuf that increments at its own
cadence. - Could potentially be confused with the edition value, even though they serve
distinct purposes.
Minimum Required Edition Should Not Be Minimal
The proto compiler should not guarantee that the minimum required edition is as
small as it could possibly be.
Pros
- Reduces implementation burden.
Cons
- This introduces situations where an upgrade of the proto compiler, or an
innocuous change to a schema, can lead the the minimum required edition
being incremented. This is a problem for schema producers.
Do Nothing
Pros
- Reduces churn in runtimes, since they do not need to implement new handling
for new editions (as contrasted to just features) regularly. - Avoids a situation where old software cannot load new descriptors at
runtime.
Cons
- Commits us to never changing the descriptor wire format in
backwards-incompatible ways, which has far-reaching effects on evolution.
These consequences are discussed in "Epochs fordescriptor.proto."
Design Editions Protobuf Design Options Attributes (`docs/design/editions/protobuf-design-options-attributes.md`)
Protobuf Design: Options Attributes
A proposal to create target and retention attributes to support.
Author: @kfm
Approved: 2022-08-26
Background
The Protobuf Editions project plans to use
custom options to model features and
encourage language bindings to build custom features off options as well.
This design proposed the specific addition of target and retention
attributes for options as well as their suggested meaning.
Both target and retention attributes are no-ops when applied to fields that
are not options (either from descriptor.proto or custom options).
Target Attributes
Historically, options have only applied to specific entities, but features will
be available on most entities. To allow language specific extensions to restrict
the places where options can bind, we will allow features to explicitly specify
the targets they apply to (similar in concept to the "target" attribute on Java
annotations). TARGET_TYPE_UNKNOWN will be treated as absent.
message FieldOptions {
...
optional OptionTargetType target = 17;
enum OptionTargetType {
TARGET_TYPE_UNKNOWN = 0;
TARGET_TYPE_FILE = 1;
TARGET_TYPE_EXTENSION_RANGE = 2;
TARGET_TYPE_MESSAGE = 3;
TARGET_TYPE_FIELD = 4;
TARGET_TYPE_ONEOF = 5;
TARGET_TYPE_ENUM = 6;
TARGET_TYPE_ENUM_VALUE = 7;
TARGET_TYPE_SERVICE = 8;
TARGET_TYPE_METHOD = 9;
};
}If no target is provided, protoc will permit the target to apply to any
entity. Otherwise, protoc will allow an option to be applied at either the
file level or to its target entity (and will produce a compile error for any
other placement). For example
message Features {
...
enum EnumType {
OPEN = 0;
CLOSED = 1;
}
optional EnumType enum = 2 [
target = TARGET_TYPE_ENUM
];
}would allow usage of
// foo.proto
edition = "tbd"
option features.enum = OPEN; // allowed at FILE scope
enum Foo {
option features.enum = CLOSED; // allowed at ENUM scope
A = 2;
B = 4;
}
message Bar {
option features.enum = CLOSED; // disallowed at Message scope
enum Baz {
C = 8;
}
}Retention
To reduce the size of descriptors in protobuf runtimes, features will be
permitted to specify retention rules (again similar in concept to "retention"
attributes on Java annotations).
enum FeatureRetention {
RETENTION_UNKNOWN = 0;
RETENTION_RUNTIME = 1;
RETENTION_SOURCE = 2;
}Options intended to inform code generators or protoc itself can be annotated
with SOURCE retention. The default retention will be RUNTIME as that is the
current behavior for all options. Code generators that emit generated
descriptors will be required to omit/strip options with SOURCE retention from
their generated descriptors. For example:
message Cpp {
enum StringType {
STRING = 1;
STRING_VIEW = 0;
CORD = 2;
}
optional string namespace = 2 [
retention = RETENTION_SOURCE,
target = TARGET_TYPE_FILE
];
}Motivation
While the proximal motivation for these options is for use with "features" in
"editions", I believe they provide sufficient general utility that adding them
directly to FieldDescriptorOptions is warranted. For example, significant
savings in binary sizes could be realized if ExtensionRangeOptions::Metadata
had only SOURCE retention. Previously, we have specifically special-cased this
behavior on a per-field basis, which does work but does not provide good
extensibility.
Discussion
In the initial design target was serving the dual purpose of identifying the
semantic entity, and also the granularity of inheritance for features. After
discussion about concerns around over use of inheritance, we decided for a
slightly refined definition that decouples these concerns. target only
specifies the semantic entity to which an option can apply. Features will be
able to be set on both the FILE level and their semantic entity. Everything in
between will be refused in the initial release. This allows us a clean
forward-compatible way to allow arbitrary feature inheritance, but doesn't
commit us to doing that until we need it.
Similarly, we will start with optional target, because we can safely move torepeated later should the need arise.
The naming for target and retention are directly modeled after Java
annotations. Other names were considered, but no better name was found and the
similarity to an existing thing won the day.
Alternatives
Use a repeated `target` proposed
This is the proposed alternative.
Pros
- Allows fine-grained control of target applicability.
Cons
- Harder to generalize for users (every feature's specification is potentially
unique).
Allow hierarchy based on `target` semantic location.
Rather than having a repeated target that specifies all locations, we allow
only the level at which it semantically applies to be specified. The protoc
compiler will implicitly allow the field to be used on entities that can
lexically group that type of entry. For this target can be either singular or
repeated.
Pros
- Enables tooling that understands when a feature is used for grouping vs when
it has semantic value (helpful for minimizing churn in large-scale changes). - Easier to generalize for users (any
FIELDfeature can apply to a message
as opposed to only theFIELDfeatures that explicitly specified an
additionaltarget).
Cons
- Forces all
targetapplications to be permitted on scoping entities.
Use Custom Options (aka "We Must Go Deeper")
Rather than building retention and target directly as fields ofFieldOptions, we could use custom options to define an equivalent thing. This
option was rejected because it pushes extra syntax onto users for a fundamental
feature.
Pros
- Doesn't require modifying
descriptor.proto.
Cons
- Requires a less-intuitive spelling in user code.
- Requires an additional import for users.
- Language-level features would have to have a magic syntax or a side table
instead of using the same consistent option as user per code gen features.
Hard Code Behaviors in `protoc`
Rather than building a generic mechanism we could simply hard code the behavior
of protoc and document it.
Pros
- Can't be misused.
Cons
- Not extensible for users.
- Requires more special cases users need to learn.
Original Approved Proposal
The proposal as originally approved had some slight differences from what was
ultimately implemented:
- The retention enum did not have an
UNKNOWNtype. - The enums were defined at the top level instead of nested inside
FieldOptions. - The enum values did not have a scoping prefix.
- The target enum had a
STREAMentry, but this turned out to be unnecessary
since the syntax that it applied to was removed.
Do Nothing
We could omit this entirely and get ice cream instead. This was rejected because
the proliferation of features on entities they do not apply to is considered too
high a cost.
Pros
- Ice cream is awesome.
Cons
- Doesn't address any of the problems that caused this to come up.
- Some people are lactose intolerant.
Design Editions Protobuf Editions Design Features (`docs/design/editions/protobuf-editions-design-features.md`)
Protobuf Editions Design: Features
Approved: 2022-10-13
A proposal to use custom options as our way of defining and representing
features.
Background
The Protobuf Editions project uses "editions"
to allow Protobuf to safely evolve over time. An edition is formally a set of
"features" with a default value per feature. The set of features or a default
value for a feature can only change with the introduction of a new edition.
Features define the specific points of change and evolution on a per entity
basis within a .proto file (entities being files, messages, fields, or any other
lexical element in the file). The design in this doc supplants an earlier design
which used strings for feature definition.
Protobuf already supports
custom options
and we will leverage these to provide a rich syntax without introducing new
syntactic forms into Protobuf.
Sample Usage
Here is a small sample usage of features to give a flavor for how it looks
edition = "2023";
package experimental.users.kfm.editions;
import "net/proto2/proto/features_cpp.proto";
option features.repeated_field_encoding = EXPANDED;
option features.enum = OPEN;
option features.(pb.cpp).string_field_type = STRING;
option features.(pb.cpp).namespace = "kfm::proto_experiments";
message Lab {
// `Mouse` is open as it inherits the file's value.
enum Mouse {
UNKNOWN_MOUSE = 0;
PINKY = 1;
THE_BRAIN = 2;
}
repeated Mouse mice = 1 [features.repeated_field_encoding = PACKED];
string name = 2;
string address = 3 [features.(pb.cpp).string_field_type = CORD];
string function = 4 [features.(pb.cpp).string_field_type = STRING_VIEW];
}
enum ColorChannel {
// Turn off the option from the surrounding file
option features.enum = CLOSED;
UNKNOWN_COLOR_CHANNEL = 0;
RED = 1;
BLUE = 2;
GREEN = 3;
ALPHA = 4;
}Language-Specific Features
We will use extensions to manage features specific to individual code
generators.
// In net/proto2/proto/descriptor.proto:
syntax = "proto2";
package proto2;
message Features {
...
extensions 1000; // for features_cpp.proto
extensions 1001; // for features_java.proto
}This will allow third-party code generators to use editions for their own
evolution as long as they reserve a single extension number indescriptor.proto. Using this from a .proto file would look like this:
edition = "2023";
import "third_party/protobuf/compiler/cpp/features_cpp.proto"
message Bar {
optional string str = 1 [features.(pb.cpp).string_field_type = true];
}Inheritance
To support inheritance, we will specify a single Features message that extends
every kind of option:
// In net/proto2/proto/descriptor.proto:
syntax = "proto2";
package proto2;
message Features {
...
}
message FileOptions {
optional Features features = ..;
}
message MessageOptions {
optional Features features = ..;
}
// All the other `*Options` protos.At the implementation level, feature inheritance is exactly the behavior ofMergeFrom
void InheritFrom(const Features& parent, Features* child) {
Features tmp(parent);
tmp.MergeFrom(child);
child->Swap(&tmp);
}which means that custom backends will be able to faithfully implement
inheritance without difficulty.
Target Attributes
While inheritance can be useful for minimizing changes or pushing defaults
broadly, it can be overused in ways that would make simple refactoring of.proto files harder. Additionally, not all features are meaningful on all
entities (for example features.enum = OPEN is meaningless on a field).
To avoid these issues, we will introduce "target" attributes on features
(similar in concept to the "target" attribute on Java annotations).
enum FeatureTargetType {
FILE = 0;
MESSAGE = 1;
ENUM = 2;
FIELD = 3;
...
};These will restrict the set of entities to which a feature may be attached.
message Features {
...
enum EnumType {
OPEN = 0;
CLOSED = 1;
}
optional EnumType enum = 2 [
target = ENUM
];
}Retention
To reduce the size of descriptors in protobuf runtimes, features will be
permitted to specify retention rules (again similar in concept to "retention"
attributes on Java annotations).
enum FeatureRetention {
SOURCE = 0;
RUNTIME = 1;
}Specification of an Edition
An edition is, effectively, an instance of the Feature proto which forms the
base for performing inheritance using MergeFrom. This allows protoc and
specific language generators to leverage existing formats (like text-format) for
specifying the value of features at a given edition.
Although naively we would think that field defaults are the right approach, this
does not quite work, because the default is editions-dependent. Instead, we
propose adding the following to the protoc-provided features.proto:
message Features {
// ...
message EditionDefault {
optional string edition = 1;
optional string default = 2; // Textproto value.
}
extend FieldOptions {
// Ideally this is a map, but map extensions are not permitted...
repeated EditionDefault edition_defaults = 9001;
}
}To build the edition defaults for a particular edition current in the context
of a particular file foo.proto, we execute the following algorithm:
- Construct a new
Features feats;. - For each field in
Features, take the value of theFeatures.edition_defaultsoption (call itdefaults), and sort it by the
value ofedition(per the total order for edition names,
Life of an Edition). - Binsearch for the latest edition in
defaultsthat is earlier or equal tocurrent.- If the field is of singular, scalar type, use that value as the value of
the field infeats. - Otherwise, the value of the field in
featsis given by merging all of
the values less thancurrent, starting from the oldest edition.
- If the field is of singular, scalar type, use that value as the value of
- For the purposes of this algorithm,
Features's fields all behave as if
they wererequired; failure to find a default explicitly via the editions
default search mechanism should result in a compilation error, because it
means the file's edition is too old. - For each extension of
Featuresthat is visible fromfoo.protovia
imports, perform the same algorithm as above to construct the editions
default for that extension message, and add it tofeat.
This algorithm has the following properties:
- Language-scoped features are discovered via imports, which is how they need
to be imported for use in a file in the first place. - Every value is set explicitly, so we correctly reject too-old files.
- Files from "the future" will not be rejected out of hand by the algorithm,
allowing us to provide a flag like--allow-experimental-editionsfor ease
of allowing backends to implement a new edition.
Edition Zero Features
Putting the parts together, we can offer a potential Feature message for
edition zero: Edition Zero Features.
message Features {
enum FieldPresence {
EXPLICIT = 0;
IMPLICIT = 1;
LEGACY_REQUIRED = 2;
}
optional FieldPresence field_presence = 1 [
retention = RUNTIME,
target = FIELD,
(edition_defaults) = {
edition: "2023", default: "EXPLICIT"
}
];
enum EnumType {
OPEN = 0;
CLOSED = 1;
}
optional EnumType enum = 2 [
retention = RUNTIME,
target = ENUM,
(edition_defaults) = {
edition: "2023", default: "OPEN"
}
];
enum RepeatedFieldEncoding {
PACKED = 0;
EXPANDED = 1;
}
optional RepeatedFieldEncoding repeated_field_encoding = 3 [
retention = RUNTIME,
target = FIELD,
(edition_defaults) = {
edition: "2023", default: "PACKED"
}
];
enum StringFieldValidation {
REQUIRED = 0;
HINT = 1;
SKIP = 2;
}
optional StringFieldValidation string_field_validation = 4 [
retention = RUNTIME,
target = FIELD,
(edition_defaults) = {
edition: "2023", default: "REQUIRED"
}
];
enum MessageEncoding {
LENGTH_PREFIXED = 0;
DELIMITED = 1;
}
optional MessageEncoding message_encoding = 5 [
retention = RUNTIME,
target = FIELD,
(edition_defaults) = {
edition: "2023", default: "LENGTH_PREFIXED"
}
];
extensions 1000; // for features_cpp.proto
extensions 1001; // for features_java.proto
}Design Editions Protobuf Editions For Schema Producers (`docs/design/editions/protobuf-editions-for-schema-producers.md`)
Protobuf Editions for Schema Producers
Author: @fowles
Explains the expected use of and interaction with editions for schema providers
and their customers.
Background
The Protobuf Editions project uses "editions"
to allow Protobuf to safely evolve over time. This is primarily accomplished
through "features". The first edition
(colloquially known as "Edition Zero") will use features to unify proto2 and
proto3 (Edition Zero Features). This document will
use definitions from Protobuf Editions: Rollout
but focus primarily on the use case of Schema Producers and Schema
Consumers:
Schema Producers are teams that produce
.protofiles for the consumption
of other teams.
As a reminder, features will generally not change the wire format of messages
and thus changing the edition for a .proto will not change the wire format of
message.
Initial Release
There will be a large period of time during which protoc is able to consumeproto3, proto2, and editions files. Once all of the supported protoc
releases handle editions, schema producers should upgrade their published.proto files to edition zero. The protobuf team will provide a tool that
upgrades proto2 and proto3 files to edition zero in a fully compatible way.
Edition Zero Features
In order to unify proto2 and proto3, "Edition Zero" is taking an opinionated
stance on which choices are good and bad, by choosing "good" defaults and
requiring explicit requests for the "bad" semantics
(Edition Zero Features). Schema producers that are
simply upgrading existing .proto files should publish these files as produced
by the upgrade tool. This will ensure wire compatibility. Newly published.proto files should use the default values from this first edition.
Steady State Flow
For the most part, editions should not disturb the general pattern for schema
producers. Any schema producer should already specify what versions of protobuf
they support and should not support versions of protobuf that are themselves
unsupported. Schema producers should generally publish all of their .proto
files with a consistent edition for the simplicity of their users. When updating
the edition for their .proto files, producers should target an edition
supported by all of the versions of protobuf in their support matrix. A good
rule of thumb is to target the newest edition supported by the oldest release of
protobuf in the support matrix.
Best Practices
Publish `.proto` files
Schema producers should publish .proto files and not generated sources. This
is already the case and editions do not change it. Publishing generated sources
can lead to mismatches between the compiler and runtime environment used.
Protobuf does not support mixed generation/runtime configurations and sometimes
security patches require updating both.
Minimize use of features
Codegenerator specific features (likefeatures.(pb.cpp).string_field_type) should only be applied within the context
of a single code base. Consumers of published schemas may wish to add
generator specific features (either by hand or with an automated .proto
refactoring tool), but producers should not force that onto users.
Client Usage Patterns
Consumers’ usage is heavily constrained by their build system. Language agnostic
build systems, like Bazel, can run protoc as one of the build steps. Language
specific build systems, like Maven or Go, make running protoc more difficult
and so consumers often avoid it. Languages like Python that traditionally lack a
build system are more extreme.
Running `protoc` Directly
Because language-specific features will not change the wire format of messages,
clients will be able to update to newer editions or specify specific features
appropriate to their environment while still connecting to external endpoints.
In particular, protobuf will provide two distinct mechanisms for supporting
these users. First, we will provide tools for automating updates to .proto
files in a safe way. These tools will apply semantic patches to .proto files
that they can then commit into source control. Second, we will provide
primitives in protoc to compile a .proto file and a semantic patch as a set
of inputs so that users never have to materialize the modified .proto file.
Protobuf team will investigate adding support for semantic patches when it
addresses Bazel rules.
In the long term, we want a Bazel rule (and possibly similar for other build
systems) that seamlessly packages changes like:
proto_library(
name = "cloud_spanner_proto",
modifications = ["cloud_spanner.change_spec"],
deps = ["@com_google_cloud//spanner"],
)Publishing Generated Libraries
As a reminder, publishing generated code is not a good idea. It frequently runs
afoul of runtime/generation time mismatches and is an active source of confusion
where users are unable to reason about what version they are on.
Teams determined to do this anyway should adhere to the following best
practices.
- Publish generated libraries using semver.
- Publish both the generated code and the protobuf runtime.
- Pin the protobuf runtime library to the exact version used to generate the
library. - When upgrading the
protocgenerator for a major, minor, or micro release,
increment the corresponding number in the published library’s version. - When updating the edition of the
.protofile, increment the major number
of the published library’s version.
It is worth note that only the last bullet point is new, everything else is a
restatement of current best practice.
Design Editions Stricter Schemas With Editions (`docs/design/editions/stricter-schemas-with-editions.md`)
Stricter Schemas with Editions
Author: @mcy
Approved: 2022-11-28
Overview
The Protobuf language is surprisingly lax in what it allows in some places, even
though these corners of the syntax space are rarely exercised in real use, and
which add complexity to backends and runtimes.
This document describes several such corners in the language, and how we might
use Editions to fix them (spoiler: we'll add a feature for each one and then
ratchet the features).
This is primarily a memo on a use-case for Editions, and not a design doc per
se.
Potential Lints
Entity Names
Protobuf does not enforce any constraints on names other than the "ASCII
identifier" rule: they must match the regex [A-Za-z_][A-Za-z0-9_]*. This
results in problems for backends:
- Backends need to be able to convert between PascalCase, camelCase,
snake_case, and SHOUTY_CASE. Doing so correctly is surprisingly tricky. - Extraneous underscores, such as underscores in names that want to be
PascalCase, trailing underscores, leading underscores, and repeated
underscores create problems for case conversion and can clash with private
names generated by backends. - Protobuf does not support non-ASCII identifiers, mostly out of inertia more
than anything else. Because some languages (Java most prominent among them)
do not support them, we can never support them, but we are not particularly
clear on this point.
The Protobuf language should be as strict as possible in what patterns it
accepts for identifiers, since these need to be transformed to many languages.
Thus, we propose the following regexes for the three casings used in Protobuf:
([A-Z][a-zA-Z0-9]*)+for PascalCase. We require this case for:- Messages.
- Enums.
- Services.
- Methods.
[a-z][a-z0-9]*(_[a-z0-9]+)*for snake_case. We require this case for:- Fields (including extensions).
- Package components.
[A-Z][A-Z0-9]*(_[A-Z0-9]+)*for SHOUTY_CASE. We require this case for:- Enum values.
These patterns are intended to reject extraneous underscores, and to make casing
of ASCII letters consistent. We explicitly only support ASCII for maximal
portability to target languages. Note that option names are not included, since
those are defined as fields in a proto, and would be subject to this rule
automatically.
To migrate, we would introduce a bool feature feature.relax_identifier_rules,
which can be applied to any entity. When set, it would cause the compiler to
reject .proto files which contain identifiers that don't match the above
constraints. It would default to true and would switch to false in a future
edition.
Keywords as Identifiers
Currently, the Protobuf language allows using keywords as identifiers. This
makes the parser somewhat more complicated than it has to be for minimal
benefit, and shadowing behavior is not well-specified. For example, what does
the following compile as?
message Foo {
message int32 {}
optional int32 foo = 1;
}This is particularly fraught in places where either a keyword or a type name can
follow. For example, optional foo = 1; is a proto3 non-optional with typeoptional, but the parser can't tell until it sees the = sign.
To avoid this and eventually stop supporting this in the parser, we make the
following set of keywords true reserved names that cannot be used as
identifiers:
bool bytes double edition enum extend extensions fixed32
fixed64 float group import int32 int64 map max
message oneof option optional package public repeated required
reserved returns rpc service sfixed32 sfixed64 sint32 sint64
stream string syntax to uint32 uint64 weakAdditionally, we introduce the syntax #optional for escaping a keyword as an
identifier. This may only be used on keywords, and not non-keyword
identifiers.
To migrate, we would introduce a bool feature feature.keywords_as_identifiers,
which can be applied to any entity. When set, it would cause the compiler to
reject .proto files which contain identifiers that use the names of keywords.
It would migrate true->false in a future edition. The #optional syntax would
not need to be feature-gated.
From time to time we may introduce new keywords. The best procedure for doing so
is to add a feature.xxx_is_a_keyword feature, start it out as true, and then
switch it to false in an edition, which would cause it to be treated as a
keyword for the purposes of this check. There's nothing stopping us from
starting to use it in the syntax without an edition if it would be relatively
unambiguous (i.e., a "contextual" keyword). Rust provides guidance here: they
really hate contextual keywords since it complicates the parser, so keywords
start out as contextual and become properly reserved in the next Rust edition.
Nonempty Package
Right now, an empty package is technically permitted. We should remove this
functionality from the language completely and require every file to declare a
package.
We would introduce a feature like feature.allow_missing_package, start it out
as true, and switch it to false.
Invalid Names in `reserved`
Currently, reserved "foo-bar"; is accepted. It is not a valid name for a field
and thus should be rejected. Ideally we should remove this syntax altogether and
only permit the use of identifiers in this position, such as reserved foo, bar;.
We would introduce a feature like feature.allow_strings_in_reserved, start it
out as true, and then switch it to false.
Almost All Names are Fully Qualified
Right now, Protobuf defines a complicated name resolution scheme that involves
matching subsets of names inspired by that of C++ (which is even more
complicated than ours!). Instead, we should require that every name be either a
single identifier OR fully-qualified. This is an attempt to move to Go-style
name resolution, which is significantly simpler to implement and explain.
In particular, if a name is a single identifier, then:
- It must be the name of a type defined at the top level of the current file.
- If it is the name of a message or enum for a field's type, it may be the
name of a type defined in the current message. This does not apply to
extension fields.
Because any multi-component path must be fully qualified, we no longer need the.foo.Bar syntax anymore, except to refer to messages defined in files without
a package. We forbid .-prefixed names except in that case.
We would introduce a feature like features.use_cpp_style_name_resolution,
start it out as true, and then switch it to false.
Ideally, if we get strict identifier names, we can tell that Foo.Bar is rooted
at a message, rather than a package. In that case, we could go as far as saying
that "names that start with a lower-case letter are fully-qualified, otherwise
they are relative to the current package, and will only find things defined in
the current file."
Unlike Go, we do not allow finding things in other packages without being
fully-qualified; this mostly comes from doing source-diving in very large
packages, like the Go runtime, where it is very hard to find where something is
defined.
Unique Enum Values
Right now, we allow aliases in enums:
enum Foo {
BAR = 5;
BAZ = 5;
}This results in significant complexity in some parts of the backend, and weird
behavior in textproto and JSON. We should disallow this.
We would introduce a feature like features.allow_enum_aliases, which would
switch from true to false.
Imports are Used
We should adopt the Go rule that all non-public imports are used (i.e, every
import provides at least one type referred to in the file).
We would introduce a feature like features.allow_unused_imports, which would
switch from true to false.
Next Field # is Reserved
There's a few idioms for this checked by linters, such as // Next ID: N. We
should codify this in the language by rewriting that every message begin withreserved N to max;, with the intent that N is the next never-used field
number. Because it is required to be the first production in the message, it can
be
We could, additionally, require that every field number be either used or
reserved, in addition to having a single N to max; reservation. Alternatively,
we could require that every field number up to the largest one used be reserved;
gaps between message numbers are usually a smell.
This applies equally to message fields and enum values.
We would introduce a feature like features.allow_unused_numbers, which we
would switch from true to false.
Disallow Implicit String Concatenation
Protobuf will implicitly concatenate two adjacent strings in any place it allows
quoted strings, e.g. option foo = "bar " "baz;. This has caused interesting
problems around reserved in the past, if a comma is omitted: reserved "foo" "bar"; is reserved "foobar";.
We would introduce a feature like features.concatenate_adjacent_strings, which
would switch from true to false.
Package Is First
The package declaration can appear anywhere in the file after syntax oredition. We should take cues from Go and require it to be the first thing in
the file, after the edition.
We would introduce a feature like features.package_anywhere, which would
switch from true to false.
Strict Boolean Options
Boolean options can use true, false, True, False, T, or F as a value: option my_bool = T;. We should restrict to only true and false.
We would introduce a feature like features.loose_bool_options, which would
switch from true to false.
Decimal Field Numbers
We permit non-decimal integer literals for field numbers, e.g. optional int32 x = 0x01;. Thankfully(?) we do not already permit a leading + or -. We should
require decimal literals, since there is very little reason to allow other
literals and makes the Protobuf language harder to parse.
We would introduce a feature like features.non_decimal_field_numbers, which
would switch from true to false.
Design Editions What Are Protobuf Editions (`docs/design/editions/what-are-protobuf-editions.md`)
What are Protobuf Editions?
Summary
This document is an introduction to the Protobuf Editions project, an ambitious
re-imagining of how we migrate Protobuf users into the future.
Goal
Enable incremental evolution of Protobuf across the entire ecosystem without
introducing permanent forks in the Protobuf language.
TL;DR
- We are replacing
syntax= ...withedition = ....- We plan to produce a new "edition" on a roughly yearly basis.
- We plan to regularly deprecate and remove old editions after a wide
horizon. - This gradual churn is enabled by the
Protobuf Breaking Changes policy.
- "Features" are a special kind of file/message/field/enum/etc option.
- Features control the individual codegen and runtime behavior of fields,
messages, enums, etc. - Features cannot introduce changes that would directly break existing
binaries. - We expect heavy churn of features in
.protofiles, so their design is
optimized to minimize diffs to.protofiles while permitting
fine-grained control. - Features are usually attached to the field/message/enum they apply
to.- Features can be specified at a higher-level entity, such as a file,
to apply to all definitions inside of that entity. This is called
feature inheritance. - Inheritance is intended to allow us to factor frequently-occurring
feature declarations, minimizing clutter during migrations.
- Features can be specified at a higher-level entity, such as a file,
- Features control the individual codegen and runtime behavior of fields,
- Editions change only the defaults of features and do not otherwise introduce
new behavior.- New behavior is fundamentally controlled by features (explicitly set or
implicit from an edition). - Editions allow us to ratchet the ecosystem forward.
- Editions can be incremented on a per
.protofile basis; projects
can upgrade incrementally.
- Editions can be incremented on a per
- New behavior is fundamentally controlled by features (explicitly set or
- Messages with any permutation of features are always interoperable (they can
import each other freely and use messages from each other).- Editions do not split the ecosystem, and migration is largely automated.
- Directly inspired by
Rust editions. - Carbon has a similar philosophy
- The
proto2/proto3distinction is going away.- Editions will support everything from both and allow mixed semantics
even within the same message or field. - Undesirable features will be LSC'd away, using the same template as any
other feature/edition migration.
- Editions will support everything from both and allow mixed semantics
Motivation
Arguably the biggest hard-earned lesson among Software Foundations is that
successful migrations are incremental. Most of our experience with these has
been for internal migrations. Externally, progress has often ossified because of
a lack of established evolution mechanisms. More recently large projects have
started planning incremental evolution into their structure. For example, Carbon
is heavily focused on evolution as a core precept, and Rust has built language
evolution via editions into its core design..
Protobuf is one of Google's oldest and most successful toolchain projects.
However, it was designed before we learned and internalized this lesson, making
modernization difficult and haphazard. We still have required and group,packed is not everywhere, and string accessors in C++ still return const std::string&. The last radical change to Protobuf (syntax = "proto3";) split
the ecosystem.
Editions and features are new language features that will allow us to
incrementally evolve Protobuf into the future. This will be done by introducing
a new syntax, hopefully the last syntax addition we will ever need.
This high-level document is intended as an introduction to Protobuf Editions for
engineers not familiar with the background and the set of tradeoffs that lead us
here. Low-level technical details are skipped in preference to describing the
kernel of our proposed design. This document reflects the approximate consensus
of protobuf-team members who have been developing Protobuf Editions, but please
beware: many open questions remain.
What is a feature?
A feature, in the narrow context of Protobuf Editions, is an option on any
syntax entity of a .proto file that has the following properties:
- It is a field or extension of a top-level option named
features, which is
present on every syntax entity (file, message, enum, field, etc). It can be
of any type, butboolandenumare the most common. - If a syntax entity's lexical parent has a particular value for a feature,
then the child has the same value, unless the feature has a new value
specified on the child, explicitly. This is called feature inheritance,
and applies recursively. Features can specify a new value at any of the
points where a feature can be added. - It explicitly specifies what syntax entities it can be set on, similar to
Java annotations (although this does not preclude inheritance to or through
an entity that it cannot be set on).
Features allow us to control the behavior of protoc, its backends, and the
Protobuf runtimes at arbitrary granularity. This is critical for large-scale
changes: if a message has few usages, features can be changed at a bigger scope,
minimizing diff churn, but if it has heavy usage and the CL to migrate a single
field is large, cleanups can happen at the field level, as necessary.
Features won't change a message’s serialization formats (binary, text, or json)
in incompatible ways except for extreme circumstances that will always be
managed directly by protobuf-team. It is critical for migrations that any
behavioral change coming from a feature is the result of a textual change to a.proto file (either an edition bump or a feature change).
ctype is an existing field option that looks exactly like a feature: it
controls the behavior of the codegen backend, although it does not have the nice
ratcheting properties of editions.
Because features can be extensions, language backends can specify
language-scoped features. For example, [ctype = CORD] could instead be
phrased as [features.(pb.cpp).string_type = CORD]. Codegen backends own the
definitions of their features.
What is an Edition?
An edition is a collection of defaults for features understood by protoc and
its backends. Editions are year-numbered, although we have defined a breakout in
case we need multiple editions in a particular year.
Instead of writing syntax = "...";, a Protobuf Editions-enabled .proto file
begins with edition = "2022"; or similar. edition implies syntax = "editions";, and the syntax keyword itself becomes deprecated. This is to
ensure that old tools not owned by protobuf-team, which only work for old
Protobuf syntaxes, crash or fail quickly and noticeably, instead of wandering
into a descriptor that they cannot understand (we will attempt to migrate what
we can, of course).
protoc specifies which editions it understands, and will reject .proto files
"from the future", since it cannot meaningfully parse them. protoc backends,
which can specify their own set of language-scoped features, must advertise the
defaults for a particular edition that they understand (and reject editions that
they don't). Runtimes must be able to handle descriptors "from the future"; this
only means that upon encountering a descriptor with an edition or feature it
does not understand, there must be a reasonable fallback for the runtime's
behavior.
What is an Edition used for?
Editions provide the fundamental increments for the lifecycle of a feature. At
this point it is important to reiterate that mo