A crate prost e a implementacao mais popular de Protocol Buffers (protobuf) para Rust. Protocol Buffers e um formato de serializacao binario desenvolvido pelo Google, amplamente utilizado em microsservicos, APIs gRPC, armazenamento de dados e comunicacao entre sistemas. O prost gera codigo Rust idiomatico a partir de arquivos .proto, com structs nativas, enums, e implementacoes de Message para serializacao/deserializacao eficiente.
Diferente de alternativas como protobuf-rs (que usa uma API mais proxima do C++), o prost gera codigo Rust limpo e idiomatico: structs com campos publicos, enums Rust reais, e integracao natural com o ecossistema (serde, etc.). E a escolha padrao para projetos que usam gRPC com Tonic.
Instalação
Adicione ao seu Cargo.toml:
[dependencies]
prost = "0.13"
prost-types = "0.13" # Tipos well-known (Timestamp, Duration, etc.)
[build-dependencies]
prost-build = "0.13"
Se voce vai usar gRPC com Tonic:
[dependencies]
prost = "0.13"
tonic = "0.12"
tokio = { version = "1", features = ["full"] }
[build-dependencies]
tonic-build = "0.12"
Uso Básico
Definindo Mensagens Proto
Crie o arquivo proto/mensagens.proto:
syntax = "proto3";
package meuapp;
// Mensagem simples
message Usuario {
uint64 id = 1;
string nome = 2;
string email = 3;
int32 idade = 4;
Endereco endereco = 5;
repeated string telefones = 6;
Status status = 7;
}
message Endereco {
string rua = 1;
string cidade = 2;
string estado = 3;
string cep = 4;
string pais = 5;
}
enum Status {
STATUS_DESCONHECIDO = 0;
STATUS_ATIVO = 1;
STATUS_INATIVO = 2;
STATUS_BLOQUEADO = 3;
}
// Mensagem com oneof
message Notificacao {
uint64 id = 1;
string destinatario = 2;
oneof conteudo {
string texto = 3;
EmailNotificacao email = 4;
PushNotificacao push = 5;
}
}
message EmailNotificacao {
string assunto = 1;
string corpo_html = 2;
repeated string anexos = 3;
}
message PushNotificacao {
string titulo = 1;
string corpo = 2;
string icone = 3;
}
Configurando o build.rs
Crie build.rs na raiz do projeto:
fn main() -> Result<(), Box<dyn std::error::Error>> {
prost_build::compile_protos(
&["proto/mensagens.proto"],
&["proto/"],
)?;
Ok(())
}
Usando as Mensagens Geradas
// O prost gera codigo em OUT_DIR, incluimos com include!
pub mod meuapp {
include!(concat!(env!("OUT_DIR"), "/meuapp.rs"));
}
use meuapp::{Endereco, Notificacao, Status, Usuario};
use prost::Message;
fn main() {
// Criar um usuario
let usuario = Usuario {
id: 1,
nome: "Maria Silva".to_string(),
email: "maria@exemplo.com".to_string(),
idade: 30,
endereco: Some(Endereco {
rua: "Rua das Flores, 123".to_string(),
cidade: "Sao Paulo".to_string(),
estado: "SP".to_string(),
cep: "01310-100".to_string(),
pais: "Brasil".to_string(),
}),
telefones: vec![
"(11) 98765-4321".to_string(),
"(11) 3456-7890".to_string(),
],
status: Status::Ativo as i32,
};
// Serializar para bytes (protobuf binario)
let mut buf = Vec::new();
usuario.encode(&mut buf).unwrap();
println!("Tamanho serializado: {} bytes", buf.len());
// Deserializar de bytes
let usuario_decodificado = Usuario::decode(buf.as_slice()).unwrap();
println!("Nome: {}", usuario_decodificado.nome);
println!("Email: {}", usuario_decodificado.email);
println!("Telefones: {:?}", usuario_decodificado.telefones);
if let Some(endereco) = &usuario_decodificado.endereco {
println!("Cidade: {}", endereco.cidade);
}
// Verificar igualdade
assert_eq!(usuario, usuario_decodificado);
println!("Serializacao/deserializacao OK!");
}
Trabalhando com Enums
use meuapp::Status;
fn descrever_status(status: i32) -> &'static str {
match Status::try_from(status) {
Ok(Status::Desconhecido) => "Desconhecido",
Ok(Status::Ativo) => "Ativo",
Ok(Status::Inativo) => "Inativo",
Ok(Status::Bloqueado) => "Bloqueado",
Err(_) => "Valor invalido",
}
}
fn main() {
let status = Status::Ativo;
println!("Status: {} ({})", descrever_status(status as i32), status as i32);
// Converter de i32
let status_de_int = Status::try_from(2);
println!("Status 2: {:?}", status_de_int); // Ok(Inativo)
// Valor invalido
let invalido = Status::try_from(99);
println!("Status 99: {:?}", invalido); // Err(99)
}
Trabalhando com Oneof
use meuapp::{
notificacao::Conteudo, EmailNotificacao, Notificacao, PushNotificacao,
};
use prost::Message;
fn criar_notificacao_texto() -> Notificacao {
Notificacao {
id: 1,
destinatario: "usuario@exemplo.com".to_string(),
conteudo: Some(Conteudo::Texto(
"Sua encomenda foi enviada!".to_string(),
)),
}
}
fn criar_notificacao_email() -> Notificacao {
Notificacao {
id: 2,
destinatario: "usuario@exemplo.com".to_string(),
conteudo: Some(Conteudo::Email(EmailNotificacao {
assunto: "Confirmacao de Pedido #12345".to_string(),
corpo_html: "<h1>Pedido Confirmado</h1><p>Obrigado!</p>".to_string(),
anexos: vec!["nota_fiscal.pdf".to_string()],
})),
}
}
fn criar_notificacao_push() -> Notificacao {
Notificacao {
id: 3,
destinatario: "device_token_abc123".to_string(),
conteudo: Some(Conteudo::Push(PushNotificacao {
titulo: "Nova mensagem".to_string(),
corpo: "Voce tem uma nova mensagem de Maria".to_string(),
icone: "message_icon.png".to_string(),
})),
}
}
fn processar_notificacao(notif: &Notificacao) {
println!("Notificacao #{} para {}", notif.id, notif.destinatario);
match ¬if.conteudo {
Some(Conteudo::Texto(texto)) => {
println!(" Tipo: Texto");
println!(" Conteudo: {}", texto);
}
Some(Conteudo::Email(email)) => {
println!(" Tipo: Email");
println!(" Assunto: {}", email.assunto);
println!(" Anexos: {:?}", email.anexos);
}
Some(Conteudo::Push(push)) => {
println!(" Tipo: Push");
println!(" Titulo: {}", push.titulo);
println!(" Corpo: {}", push.corpo);
}
None => {
println!(" Sem conteudo!");
}
}
}
fn main() {
let notificacoes = vec![
criar_notificacao_texto(),
criar_notificacao_email(),
criar_notificacao_push(),
];
for notif in ¬ificacoes {
processar_notificacao(notif);
// Serializar e deserializar
let mut buf = Vec::new();
notif.encode(&mut buf).unwrap();
let decodificada = Notificacao::decode(buf.as_slice()).unwrap();
assert_eq!(notif, &decodificada);
println!(" Serializado: {} bytes\n", buf.len());
}
}
Recursos Avançados
Configuração Avançada do build.rs
use std::io::Result;
fn main() -> Result<()> {
let mut config = prost_build::Config::new();
// Adicionar derives customizados a todas as mensagens
config.type_attribute(".", "#[derive(serde::Serialize, serde::Deserialize)]");
// Adicionar atributos a tipos especificos
config.type_attribute(
".meuapp.Usuario",
"#[derive(Hash)]"
);
// Renomear campos
config.field_attribute(
".meuapp.Usuario.email",
"#[serde(rename = \"email_address\")]"
);
// Gerar em diretorio especifico (opcional)
// config.out_dir("src/proto");
// Compilar
config.compile_protos(
&[
"proto/mensagens.proto",
"proto/servicos.proto",
],
&["proto/"],
)?;
Ok(())
}
Tipos Well-Known (prost-types)
use prost_types::{Duration, Timestamp};
fn main() {
// Timestamp
let agora = Timestamp {
seconds: 1709913600, // epoch seconds
nanos: 500_000_000, // 0.5 segundos
};
println!("Timestamp: {}s {}ns", agora.seconds, agora.nanos);
// Duration
let duracao = Duration {
seconds: 3600,
nanos: 0,
};
println!("Duracao: {}s", duracao.seconds);
// Converter de/para std::time
use std::time::{SystemTime, UNIX_EPOCH};
let system_time = SystemTime::now();
let desde_epoch = system_time.duration_since(UNIX_EPOCH).unwrap();
let timestamp = Timestamp {
seconds: desde_epoch.as_secs() as i64,
nanos: desde_epoch.subsec_nanos() as i32,
};
println!("Agora: {}s", timestamp.seconds);
}
Integração com Tonic (gRPC)
Defina servicos no proto (proto/servicos.proto):
syntax = "proto3";
package meuapp;
import "mensagens.proto";
service UsuarioService {
rpc CriarUsuario(CriarUsuarioRequest) returns (CriarUsuarioResponse);
rpc BuscarUsuario(BuscarUsuarioRequest) returns (Usuario);
rpc ListarUsuarios(ListarUsuariosRequest) returns (stream Usuario);
rpc AtualizarUsuarios(stream AtualizarUsuarioRequest) returns (AtualizarUsuariosResponse);
}
message CriarUsuarioRequest {
string nome = 1;
string email = 2;
int32 idade = 3;
}
message CriarUsuarioResponse {
uint64 id = 1;
string mensagem = 2;
}
message BuscarUsuarioRequest {
uint64 id = 1;
}
message ListarUsuariosRequest {
int32 pagina = 1;
int32 por_pagina = 2;
Status filtro_status = 3;
}
message AtualizarUsuarioRequest {
uint64 id = 1;
string nome = 2;
string email = 3;
}
message AtualizarUsuariosResponse {
int32 atualizados = 1;
}
Atualize build.rs para Tonic:
fn main() -> Result<(), Box<dyn std::error::Error>> {
tonic_build::configure()
.build_server(true)
.build_client(true)
.compile_protos(
&["proto/servicos.proto"],
&["proto/"],
)?;
Ok(())
}
Implementacao do servidor:
use tonic::{transport::Server, Request, Response, Status};
pub mod meuapp {
tonic::include_proto!("meuapp");
}
use meuapp::{
usuario_service_server::{UsuarioService, UsuarioServiceServer},
BuscarUsuarioRequest, CriarUsuarioRequest, CriarUsuarioResponse,
ListarUsuariosRequest, Usuario,
};
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
#[derive(Debug)]
struct UsuarioServiceImpl {
usuarios: Arc<Mutex<HashMap<u64, Usuario>>>,
proximo_id: Arc<Mutex<u64>>,
}
impl UsuarioServiceImpl {
fn novo() -> Self {
UsuarioServiceImpl {
usuarios: Arc::new(Mutex::new(HashMap::new())),
proximo_id: Arc::new(Mutex::new(1)),
}
}
}
#[tonic::async_trait]
impl UsuarioService for UsuarioServiceImpl {
async fn criar_usuario(
&self,
request: Request<CriarUsuarioRequest>,
) -> Result<Response<CriarUsuarioResponse>, Status> {
let req = request.into_inner();
let mut id_lock = self.proximo_id.lock().unwrap();
let id = *id_lock;
*id_lock += 1;
let usuario = Usuario {
id,
nome: req.nome.clone(),
email: req.email.clone(),
idade: req.idade,
endereco: None,
telefones: vec![],
status: meuapp::Status::Ativo as i32,
};
self.usuarios.lock().unwrap().insert(id, usuario);
Ok(Response::new(CriarUsuarioResponse {
id,
mensagem: format!("Usuario '{}' criado com sucesso", req.nome),
}))
}
async fn buscar_usuario(
&self,
request: Request<BuscarUsuarioRequest>,
) -> Result<Response<Usuario>, Status> {
let id = request.into_inner().id;
let usuarios = self.usuarios.lock().unwrap();
match usuarios.get(&id) {
Some(usuario) => Ok(Response::new(usuario.clone())),
None => Err(Status::not_found(format!(
"Usuario com id {} nao encontrado",
id
))),
}
}
type ListarUsuariosStream =
tokio_stream::wrappers::ReceiverStream<Result<Usuario, Status>>;
async fn listar_usuarios(
&self,
request: Request<ListarUsuariosRequest>,
) -> Result<Response<Self::ListarUsuariosStream>, Status> {
let req = request.into_inner();
let usuarios = self.usuarios.lock().unwrap().clone();
let (tx, rx) = tokio::sync::mpsc::channel(128);
tokio::spawn(async move {
let pagina = req.pagina.max(1) as usize;
let por_pagina = req.por_pagina.max(10) as usize;
let inicio = (pagina - 1) * por_pagina;
let mut lista: Vec<Usuario> = usuarios.values().cloned().collect();
lista.sort_by_key(|u| u.id);
for usuario in lista.into_iter().skip(inicio).take(por_pagina) {
if tx.send(Ok(usuario)).await.is_err() {
break;
}
}
});
Ok(Response::new(tokio_stream::wrappers::ReceiverStream::new(rx)))
}
async fn atualizar_usuarios(
&self,
_request: Request<tonic::Streaming<meuapp::AtualizarUsuarioRequest>>,
) -> Result<Response<meuapp::AtualizarUsuariosResponse>, Status> {
// Implementacao de streaming bidirecional
Ok(Response::new(meuapp::AtualizarUsuariosResponse {
atualizados: 0,
}))
}
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let addr = "0.0.0.0:50051".parse()?;
let servico = UsuarioServiceImpl::novo();
println!("Servidor gRPC ouvindo em {}", addr);
Server::builder()
.add_service(UsuarioServiceServer::new(servico))
.serve(addr)
.await?;
Ok(())
}
Serialização Eficiente com Bytes
use bytes::{Bytes, BytesMut};
use prost::Message;
pub mod meuapp {
include!(concat!(env!("OUT_DIR"), "/meuapp.rs"));
}
use meuapp::Usuario;
fn serializar_eficiente(usuario: &Usuario) -> Bytes {
let mut buf = BytesMut::with_capacity(usuario.encoded_len());
usuario.encode(&mut buf).unwrap();
buf.freeze()
}
fn deserializar_eficiente(dados: Bytes) -> Result<Usuario, prost::DecodeError> {
Usuario::decode(dados)
}
fn main() {
let usuario = Usuario {
id: 42,
nome: "Maria".to_string(),
email: "maria@exemplo.com".to_string(),
idade: 30,
endereco: None,
telefones: vec![],
status: 1,
};
// Serializar
let bytes = serializar_eficiente(&usuario);
println!("Tamanho: {} bytes", bytes.len());
// Comparar com JSON
let json = serde_json::to_string(&usuario).unwrap_or_default();
println!("Tamanho JSON: {} bytes", json.len());
println!("Reducao: {:.1}%", (1.0 - bytes.len() as f64 / json.len() as f64) * 100.0);
// Deserializar
let decodificado = deserializar_eficiente(bytes).unwrap();
assert_eq!(usuario.nome, decodificado.nome);
}
Boas Práticas
1. Organize seus Arquivos Proto
projeto/
├── proto/
│ ├── meuapp/
│ │ ├── mensagens.proto
│ │ ├── servicos.proto
│ │ └── tipos_comuns.proto
│ └── google/
│ └── protobuf/
│ └── timestamp.proto
├── build.rs
├── Cargo.toml
└── src/
└── main.rs
2. Versione seus Protos
syntax = "proto3";
package meuapp.v1; // Versionamento no package
// Nunca remova ou renumere campos!
// Use reserved para campos removidos
message Usuario {
uint64 id = 1;
string nome = 2;
string email = 3;
reserved 4; // Campo 'idade' removido na v2
reserved "idade";
}
3. Use Optional para Campos Explicitamente Opcionais
syntax = "proto3";
message Perfil {
string nome = 1; // Sempre presente (empty string se nao definido)
optional string bio = 2; // Explicitamente opcional (gera Option<String> em Rust)
optional int32 idade = 3; // Gera Option<i32>
}
4. Prefira Mensagens Wrapper para Tipos Complexos
// Em vez de campos soltos, agrupe em mensagens
// Isso facilita extensao futura e reutilizacao
// BOM: mensagem wrapper
// message Pagina {
// int32 numero = 1;
// int32 tamanho = 2;
// }
// RUIM: campos soltos
// message ListarRequest {
// int32 pagina_numero = 1;
// int32 pagina_tamanho = 2;
// int32 outra_pagina_numero = 3; // Confuso
// }
5. Trate Valores Default do Proto3
use meuapp::Usuario;
fn processar_usuario(usuario: &Usuario) {
// Em proto3, campos nao definidos tem valores default:
// string -> ""
// int -> 0
// bool -> false
// enum -> primeiro valor (0)
// message -> None
// Verifique campos obrigatorios
if usuario.nome.is_empty() {
eprintln!("Aviso: usuario sem nome");
}
// Use match para enums
match meuapp::Status::try_from(usuario.status) {
Ok(status) => println!("Status: {:?}", status),
Err(_) => eprintln!("Status desconhecido: {}", usuario.status),
}
// Mensagens aninhadas sao Option
if let Some(endereco) = &usuario.endereco {
println!("Cidade: {}", endereco.cidade);
}
}
Exemplos Práticos
Exemplo Completo: Sistema de Mensagens com Serialização
use prost::Message;
use std::collections::HashMap;
use std::io::{Read, Write};
use std::time::{SystemTime, UNIX_EPOCH};
pub mod meuapp {
include!(concat!(env!("OUT_DIR"), "/meuapp.rs"));
}
use meuapp::*;
// === Repositorio de Mensagens ===
struct RepositorioMensagens {
arquivo: String,
mensagens: Vec<Notificacao>,
}
impl RepositorioMensagens {
fn novo(arquivo: &str) -> Self {
RepositorioMensagens {
arquivo: arquivo.to_string(),
mensagens: Vec::new(),
}
}
fn adicionar(&mut self, notif: Notificacao) {
self.mensagens.push(notif);
}
fn salvar(&self) -> Result<(), Box<dyn std::error::Error>> {
let mut arquivo = std::fs::File::create(&self.arquivo)?;
for msg in &self.mensagens {
let mut buf = Vec::new();
msg.encode(&mut buf)?;
// Prefixar com tamanho (length-delimited)
let tamanho = buf.len() as u32;
arquivo.write_all(&tamanho.to_le_bytes())?;
arquivo.write_all(&buf)?;
}
println!("Salvas {} mensagens em '{}'", self.mensagens.len(), self.arquivo);
Ok(())
}
fn carregar(&mut self) -> Result<(), Box<dyn std::error::Error>> {
let mut arquivo = std::fs::File::open(&self.arquivo)?;
let mut buf_tamanho = [0u8; 4];
self.mensagens.clear();
loop {
match arquivo.read_exact(&mut buf_tamanho) {
Ok(()) => {
let tamanho = u32::from_le_bytes(buf_tamanho) as usize;
let mut buf = vec![0u8; tamanho];
arquivo.read_exact(&mut buf)?;
let msg = Notificacao::decode(buf.as_slice())?;
self.mensagens.push(msg);
}
Err(ref e) if e.kind() == std::io::ErrorKind::UnexpectedEof => break,
Err(e) => return Err(e.into()),
}
}
println!("Carregadas {} mensagens de '{}'", self.mensagens.len(), self.arquivo);
Ok(())
}
fn listar(&self) {
for msg in &self.mensagens {
println!("ID: {}, Para: {}", msg.id, msg.destinatario);
match &msg.conteudo {
Some(notificacao::Conteudo::Texto(t)) => println!(" Texto: {}", t),
Some(notificacao::Conteudo::Email(e)) => println!(" Email: {}", e.assunto),
Some(notificacao::Conteudo::Push(p)) => println!(" Push: {}", p.titulo),
None => println!(" (sem conteudo)"),
}
}
}
fn estatisticas(&self) -> HashMap<String, usize> {
let mut stats = HashMap::new();
for msg in &self.mensagens {
let tipo = match &msg.conteudo {
Some(notificacao::Conteudo::Texto(_)) => "texto",
Some(notificacao::Conteudo::Email(_)) => "email",
Some(notificacao::Conteudo::Push(_)) => "push",
None => "vazio",
};
*stats.entry(tipo.to_string()).or_insert(0) += 1;
}
stats
}
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut repo = RepositorioMensagens::novo("/tmp/mensagens.pb");
// Criar mensagens
repo.adicionar(Notificacao {
id: 1,
destinatario: "maria@exemplo.com".to_string(),
conteudo: Some(notificacao::Conteudo::Email(EmailNotificacao {
assunto: "Bem-vinda ao sistema!".to_string(),
corpo_html: "<h1>Ola Maria!</h1>".to_string(),
anexos: vec![],
})),
});
repo.adicionar(Notificacao {
id: 2,
destinatario: "joao@exemplo.com".to_string(),
conteudo: Some(notificacao::Conteudo::Texto(
"Seu pedido foi enviado!".to_string(),
)),
});
repo.adicionar(Notificacao {
id: 3,
destinatario: "device_token_xyz".to_string(),
conteudo: Some(notificacao::Conteudo::Push(PushNotificacao {
titulo: "Promocao!".to_string(),
corpo: "50% de desconto hoje!".to_string(),
icone: "sale.png".to_string(),
})),
});
// Salvar e carregar
repo.salvar()?;
let mut repo2 = RepositorioMensagens::novo("/tmp/mensagens.pb");
repo2.carregar()?;
println!("\n=== Mensagens ===");
repo2.listar();
println!("\n=== Estatisticas ===");
for (tipo, contagem) in repo2.estatisticas() {
println!(" {}: {}", tipo, contagem);
}
// Comparar tamanho com JSON
let json_size: usize = repo.mensagens.iter().map(|m| {
let mut buf = Vec::new();
m.encode(&mut buf).unwrap();
buf.len()
}).sum();
println!("\n=== Tamanho Total ===");
println!(" Protobuf: {} bytes", json_size);
// Cleanup
let _ = std::fs::remove_file("/tmp/mensagens.pb");
Ok(())
}
Comparação com Alternativas
| Formato | Tamanho | Velocidade | Schema | Legibilidade |
|---|---|---|---|---|
| Protobuf (prost) | Muito pequeno | Muito rapido | .proto (obrigatorio) | Binario |
| JSON (serde_json) | Grande | Rapido | Opcional | Texto legivel |
| MessagePack (rmp) | Pequeno | Muito rapido | Nao | Binario |
| Cap’n Proto | Muito pequeno | Extremamente rapido | .capnp | Binario |
| FlatBuffers | Muito pequeno | Extremamente rapido | .fbs | Binario |
| Bincode | Pequeno | Muito rapido | Nao | Binario |
| CBOR | Pequeno | Rapido | Nao | Binario |
Prost/Protobuf e ideal quando voce precisa de interoperabilidade entre linguagens, schema evolutivo, e integracao com gRPC. Para comunicacao Rust-Rust pura, bincode pode ser mais simples. Para APIs HTTP, JSON continua sendo o padrao.
Conclusão
O prost torna Protocol Buffers uma escolha natural para projetos Rust que precisam de serializacao binaria eficiente e interoperabilidade entre linguagens. A geracao de codigo idiomatico, combinada com a integracao transparente com Tonic para gRPC, cria um pipeline completo para comunicacao entre microsservicos.
Lembre-se de nunca remover ou renumerar campos em seus protos (use reserved), versionar seus packages, e aproveitar os tipos well-known do prost-types para timestamps e duracoes.
Proximos passos: