Async traits: trabalhando com limites atuais
1. O problema fundamental: traits síncronas não comportam métodos assíncronos
1.1. Tentativa ingênua e o erro do compilador
Ao tentar definir um trait com um método assíncrono, o compilador Rust rejeita a sintaxe direta:
trait AsyncProcessor {
async fn process(&self, data: &[u8]) -> Vec<u8>;
}
O erro gerado é claro: error[E0706]: async fn in trait is not yet stable. Isso ocorre porque async fn desugara para um tipo anônimo que implementa Future, e traits não podem conter tipos anônimos como retorno. A sintaxe async fn em traits ainda é uma feature experimental no nightly.
1.2. Por que async não é uma assinatura de função como fn?
async fn não é apenas açúcar sintático para fn() -> impl Future<Output = T>. Em uma trait, o tipo de retorno precisa ser conhecido ou associado ao tipo concreto que implementa o trait. Como impl Trait em posição de retorno não é permitido em traits (RPITIT era instável até recentemente), a abordagem direta falha.
1.3. Consequências para design de APIs e bibliotecas
Essa limitação força designers de APIs a escolher entre diferentes abordagens, cada uma com seus trade-offs. Bibliotecas como tokio, tower e actix precisam lidar com essa restrição, impactando a ergonomia de uso e a composição de componentes assíncronos.
2. Solução clássica: retornar Pin<Box<dyn Future>>
2.1. Assinatura explícita
A abordagem mais direta é retornar um trait object heap-alocado:
use std::future::Future;
use std::pin::Pin;
trait AsyncProcessor {
fn process(&self, data: &[u8]) -> Pin<Box<dyn Future<Output = Vec<u8>>>>;
}
2.2. Implementação concreta
struct MyProcessor;
impl AsyncProcessor for MyProcessor {
fn process(&self, data: &[u8]) -> Pin<Box<dyn Future<Output = Vec<u8>>>> {
Box::pin(async move {
// Simula processamento assíncrono
data.to_vec()
})
}
}
2.3. Limitações
Esta abordagem tem custos significativos:
- Alocação no heap a cada chamada do método
- Dynamic dispatch via vtable, impedindo otimizações do compilador
- Problemas com lifetimes: capturar referências exige anotações complexas
- Send bounds: por padrão, dyn Future não é Send, exigindo Pin<Box<dyn Future + Send>>
3. Trabalhando com async-trait (crate)
3.1. Funcionamento interno
A crate async-trait fornece uma macro que desugara o código para a abordagem com Pin<Box<dyn Future + Send>>:
use async_trait::async_trait;
#[async_trait]
trait AsyncProcessor {
async fn process(&self, data: &[u8]) -> Vec<u8>;
}
struct MyProcessor;
#[async_trait]
impl AsyncProcessor for MyProcessor {
async fn process(&self, data: &[u8]) -> Vec<u8> {
data.to_vec()
}
}
3.2. Ganho de ergonomia vs. custo oculto
A macro esconde a complexidade, mas o custo de alocação e Send implícito permanece. Cada chamada a process() aloca um Box no heap, o que pode ser problemático em loops ou sistemas com restrições de latência.
3.3. Customizando Send
Para ambientes single-threaded, é possível desabilitar Send:
#[async_trait(?Send)]
trait LocalProcessor {
async fn process(&self, data: &[u8]) -> Vec<u8>;
}
4. Abordagem com trait objects manuais e impl Future em associated types
4.1. Definindo um associated type
use std::future::Future;
trait AsyncProcessor {
type Fut: Future<Output = Vec<u8>>;
fn process(&self, data: &[u8]) -> Self::Fut;
}
4.2. Implementação concreta
struct MyProcessor;
impl AsyncProcessor for MyProcessor {
type Fut = impl Future<Output = Vec<u8>>;
fn process(&self, data: &[u8]) -> Self::Fut {
async move { data.to_vec() }
}
}
4.3. Vantagens e desvantagens
Vantagens: sem alocação extra, dispatch estático (monomorfização), melhor performance.
Desvantagens: verbosidade, impossibilidade de usar trait objects com este trait (o tipo associado não é object-safe), e complexidade ao lidar com lifetimes nos parâmetros.
5. GATs (Generic Associated Types) como alternativa elegante
5.1. Usando GATs para capturar lifetimes
Generic Associated Types (estabilizados no Rust 1.65) permitem associar tipos genéricos a traits:
trait AsyncProcessor {
type Fut<'a>: Future<Output = Vec<u8>> where Self: 'a;
fn process<'a>(&'a self, data: &'a [u8]) -> Self::Fut<'a>;
}
5.2. Implementação com GATs
struct MyProcessor;
impl AsyncProcessor for MyProcessor {
type Fut<'a> = impl Future<Output = Vec<u8>> + 'a;
fn process<'a>(&'a self, data: &'a [u8]) -> Self::Fut<'a> {
async move { data.to_vec() }
}
}
5.3. Comparação com outras abordagens
GATs oferecem flexibilidade superior para capturar lifetimes dos parâmetros, mas introduzem complexidade de tipos que pode assustar desenvolvedores iniciantes. A sintaxe impl Trait em posição de tipo associado (TAIT) ainda é instável em muitos casos.
6. Patterns avançados: async fn em traits (RFC 3668 / nightly)
6.1. Estado atual no Rust nightly
A feature async_fn_in_trait permite escrever async fn diretamente em traits no nightly:
#![feature(async_fn_in_trait)]
trait AsyncProcessor {
async fn process(&self, data: &[u8]) -> Vec<u8>;
}
6.2. Como usar
#![feature(async_fn_in_trait)]
#![feature(return_position_impl_trait_in_trait)]
trait AsyncProcessor {
async fn process(&self, data: &[u8]) -> Vec<u8>;
}
struct MyProcessor;
impl AsyncProcessor for MyProcessor {
async fn process(&self, data: &[u8]) -> Vec<u8> {
data.to_vec()
}
}
6.3. Limitações conhecidas
A implementação atual tem limitações em lifetime elision e bounds de Send. O RFC 3668 ainda está em desenvolvimento, e o suporte completo depende da estabilização de RPITIT (Return Position Impl Trait In Traits).
7. Boas práticas e trade-offs na escolha da abordagem
7.1. Quando usar cada abordagem
async-trait: ideal para prototipagem rápida e APIs públicas onde a ergonomia é prioridade- GATs +
impl Future: melhor performance, recomendado para bibliotecas de alto desempenho Pin<Box<dyn Future>>manual: quando você precisa de controle fino sobreSend/Syncboundsasync fnnightly: para experimentar o futuro do Rust assíncrono
7.2. Lidando com Send e Sync
// Garantindo Send em runtime multi-threaded
#[async_trait]
trait SafeProcessor: Send {
async fn process(&self, data: Vec<u8>) -> Vec<u8>;
}
7.3. Testabilidade
Composição de traits assíncronos com mock objects:
#[cfg(test)]
mod tests {
use super::*;
struct MockProcessor;
#[async_trait]
impl AsyncProcessor for MockProcessor {
async fn process(&self, data: &[u8]) -> Vec<u8> {
vec![1, 2, 3] // Resposta mockada
}
}
}
8. Olhando para o futuro: async fn in traits estável e implicações
8.1. O que muda com a estabilização
A estabilização de async fn em traits (prevista para Rust 2024) eliminará a necessidade de crates como async-trait para a maioria dos casos. O compilador gerará código otimizado sem alocações desnecessárias.
8.2. Migração de código existente
// Antes (async-trait)
#[async_trait]
trait OldTrait {
async fn method(&self);
}
// Depois (nativo)
trait NewTrait {
async fn method(&self);
}
8.3. Impacto no ecossistema
Bibliotecas como tower, actix-web e hyper poderão oferecer APIs mais limpas e performáticas. O pattern de Service trait do tower, por exemplo, poderá usar async fn diretamente, simplificando implementações complexas.
Referências
- The Rust Async Book: Async/Await — Guia oficial sobre programação assíncrona em Rust, incluindo fundamentos de traits assíncronos
- RFC 3668: Async Fn in Traits — Proposta oficial para estabilização de async fn em traits
- async-trait crate documentation — Documentação oficial da crate async-trait, com exemplos e configurações avançadas
- Rust Reference: Traits — Documentação oficial sobre traits em Rust, incluindo regras para object safety
- Tokio Tutorial: Async in Traits — Tutorial prático do ecossistema Tokio sobre implementação de traits assíncronos
- Rust Blog: Generic Associated Types — Anúncio de estabilização dos GATs, essencial para patterns avançados de async traits
- Without Boats: Async Traits — Artigo técnico aprofundado sobre os desafios de design de traits assíncronos em Rust