How to Use Protocol Buffers in Rust

A practical guide to working with Protocol Buffers in Rust applications

Published: January 2025 • 9 min read

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.