C++/Rust Protobuf interop

WARNING: This page documents functionality that is currently internal to the Google monorepo.

Protocol buffers are Google's language-neutral, platform-neutral, extensible mechanism for serializing structured data. Once you define how you want your data to be structured once, you can generate source code in a variety of languages to manipulate and serialize/deserialize your structured data. Protobuf messages are among the most common types at Google, appearing in vast majority of APIs.

The usual way to passing data from one language to another using Protobufs is to serialize a message in one language, and deserialize it in another. This serialization/deserialization has costs which makes this approach unsuitable for hot code paths.

To avoid those costs, we've intentionally designed C++ and Rust Protobuf message types to have identical layouts. We avoid the need for serialization/deserialization and instead we directly use the same message object from both languages. Crubit automatically generates the zero-cost glue code for us. For example, take this piece of a C++ header:

MyProto Foo(); 

This becomes available to Rust as:

pub fn Foo() -> MyProto {...}

(Specifically, Crubit will detect that this is a Protobuf message, and it will convert from the C++ message type to the Rust message type.)

Calling Rust APIs using Protobuf message types

RustC++
MessageMessage
MessageViewconst Message*
MessageMutMessage*

Protocol buffers are supported by value, and using the View and Mut view types, where they are mapped to C++ pointers.

See cc_bindings_from_rs/test/bridging/protobuf/rust_lib.rs for an example definition, and cc_bindings_from_rs/test/bridging/protobuf/user_of_rust_lib.cc for how to call it from Rust.

Calling C++ APIs using Protobuf message types

Calling C++ APIs which use protobuf is slightly more difficult.

First of all, add your proto_library target to the [allowlist](http://). See b/414381884 for more context & information on when this allowlist will be removed.

Passing by value

C++Rust
MessageMessage

When a C++ proto message is passed or returned by value, it is mapped directly to the Rust message type, as you would expect.

C++:

cs/file:google_internal/protobuf/by_value.h content:foo::Message

Rust:

cs/file:google_internal/protobuf/by_value_test.rs content:by_value\:\:|\bmsg\b

Passing by reference

C++Rust
const Message*, const Message&*const Incomplete<symbol!("Message"), ...>
Message*, Message&*mut Incomplete<symbol!("Message"), ...>

When a C++ proto is passed by pointer or by reference, the Rust type is a pointer to a forward declaration of the C++ protocol buffer type.

In particular, C++ APIs are not exposed using the View or Mut types.

These are pointers because C++ APIs do not annotate ownership, lifetime, or aliasing properties, and so these cannot be mapped to the distinct owned, View, or Mut types of the Rust protobuf API. And these are forward-declared because the C++ types do not have direct Rust bindings: the generated .proto.h file does not get piped through Crubit.

  • To convert a Rust Proto to a C++ const Proto*: use my_proto.as_view().cpp_cast()

  • To convert a Rust Proto to a C++ Proto*: use my_proto.as_mut().cpp_cast()

  • To convert a C++ (const) Proto* to a Rust View/Mut: use unsafe {my_ptr.unsafe_cpp_cast()}.

See support/forward_declare.rs for the definition of Incomplete, CppCast, and UnsafeCppCast.

For copy-pastable example code, see the examples in google_internal/protobuf/