Rust and Protocol Buffers are an excellent combination. Rust's memory safety and zero-cost abstractions, paired with Protobuf's efficiency, create high-performance systems. The Rust ecosystem offers prost, a modern protobuf implementation that generates idiomatic Rust code.
This guide shows you how to use Protocol Buffers in Rust using prost, the most popular choice in the Rust community.
What You'll Need
- •Rust: Version 1.60 or higher (install from rustup.rs)
- •Protocol Buffer Compiler: Required for code generation
- •Basic Rust knowledge: Understanding of Cargo and basic syntax
Step 1: Install Protocol Buffer Compiler
# On macOS brew install protobuf # On Linux sudo apt install protobuf-compiler # On Windows choco install protoc
Verify installation:
protoc --version # Should show: libprotoc 3.x.x or higher
Step 2: Create a Rust Project
cargo new protobuf-rust-example cd protobuf-rust-example mkdir proto
Update Cargo.toml
:
[package] name = "protobuf-rust-example" version = "0.1.0" edition = "2021" [dependencies] prost = "0.12" prost-types = "0.12" [build-dependencies] prost-build = "0.12"
Step 3: Create Your .proto File
Create proto/person.proto
:
syntax = "proto3"; package person; message Person { string name = 1; int32 id = 2; string email = 3; repeated string phone_numbers = 4; enum PhoneType { MOBILE = 0; HOME = 1; WORK = 2; } message PhoneNumber { string number = 1; PhoneType type = 2; } repeated PhoneNumber phones = 5; bool is_active = 6; } message AddressBook { repeated Person people = 1; }
Step 4: Set Up Code Generation
Create build.rs
at the project root:
fn main() { prost_build::compile_protos(&["proto/person.proto"], &["proto/"]) .unwrap(); }
This automatically generates Rust code when you build the project.
Step 5: Use Protocol Buffers in Rust
Update src/main.rs
:
use prost::Message; use std::fs; // Include generated protobuf code pub mod person { include!(concat!(env!("OUT_DIR"), "/person.rs")); } use person::{Person, AddressBook, person::PhoneType, person::PhoneNumber}; fn main() { // Create a Person let person = Person { name: "John Doe".to_string(), id: 1234, email: "john.doe@example.com".to_string(), phone_numbers: vec![ "555-1234".to_string(), "555-5678".to_string(), ], phones: vec![ PhoneNumber { number: "555-9999".to_string(), r#type: PhoneType::Mobile as i32, }, ], is_active: true, }; println!("Created Person:"); println!("Name: {}", person.name); println!("ID: {}", person.id); println!("Email: {}", person.email); // Serialize to bytes let mut buf = Vec::new(); person.encode(&mut buf).unwrap(); println!("\nSerialized to {} bytes", buf.len()); // Deserialize from bytes let decoded = Person::decode(&buf[..]).unwrap(); println!("\nDeserialized Person:"); println!("Name: {}", decoded.name); println!("Active: {}", decoded.is_active); // Save to file fs::write("person.bin", &buf).unwrap(); println!("\nSaved to person.bin"); // Read from file let file_data = fs::read("person.bin").unwrap(); let from_file = Person::decode(&file_data[..]).unwrap(); println!("Read from file: {}", from_file.name); // Create AddressBook let address_book = AddressBook { people: vec![ person, Person { name: "Jane Smith".to_string(), id: 5678, email: "jane@example.com".to_string(), phone_numbers: vec![], phones: vec![], is_active: true, }, ], }; println!("\nAddress book has {} people", address_book.people.len()); }
Step 6: Build and Run
cargo build cargo run
Expected output:
Created Person: Name: John Doe ID: 1234 Email: john.doe@example.com Serialized to 78 bytes Deserialized Person: Name: John Doe Active: true Saved to person.bin Read from file: John Doe Address book has 2 people
Common Operations in Rust
Encode to Vec
let mut buf = Vec::new(); person.encode(&mut buf)?;
Encode to Fixed Buffer
let encoded_len = person.encoded_len(); let mut buf = vec![0u8; encoded_len]; person.encode(&mut buf.as_mut_slice())?;
Clone a Message
let person2 = person.clone();
Merge Messages
let mut person1 = Person::default(); person1.merge(&person2)?;
JSON Conversion (with serde)
// Add to Cargo.toml: // prost = { version = "0.12", features = ["serde"] } // serde_json = "1.0" let json = serde_json::to_string(&person)?; println!("{}", json);
Best Practices for Rust
Use Result Types
Always handle errors with Result
. Use ?
operator for clean error propagation.
Leverage Rust's Type System
Generated Rust code uses proper types. Enums become Rust enums, and required fields are enforced.
Don't Edit Generated Code
Generated files are in target/
directory. Never modify them directly.
Use Derive Macros
Enable serde
feature for JSON serialization and other useful derives.
Advanced: Custom Type Attributes
You can customize generated code in build.rs
:
fn main() { let mut config = prost_build::Config::new(); // Add derive macros config.type_attribute(".", "#[derive(serde::Serialize, serde::Deserialize)]"); // Compile config.compile_protos(&["proto/person.proto"], &["proto/"]) .unwrap(); }
Common Issues
Issue: protoc not found during build
Solution: Install protoc and ensure it's in your PATH. Run which protoc
to verify.
Issue: Generated file not found
Solution: Run cargo clean
thencargo build
to regenerate files.
Issue: Type mismatch errors
Solution: Ensure all prost dependencies use the same version in Cargo.toml.
Related Tools
Conclusion
Protocol Buffers fit beautifully into Rust's ecosystem. The prost library generates clean, type-safe Rust code that leverages the language's strengths. Combined with Rust's performance and safety guarantees, you get a powerful stack for building high-performance systems.
Start with these examples and explore more advanced features like custom derives, tonic for gRPC, and streaming as your needs grow.