Sistema de Macros do Rust
O sistema de macros do Rust permite que você escreva código que escreve código, expandindo em tempo de compilação para Rust comum antes que o compilador verifique os tipos.
Esta página é o mapa conceitual para toda a seção macros. Ela explica o que é fundamentalmente uma macro, como as duas famílias de macros diferem e por que a higiene é a propriedade que torna as macros seguras para compartilhar entre crates em vez de uma fonte de bugs misteriosos. O restante das páginas desta seção, desde a mecânica de macro_rules! até a escrita de crates de macros procedurais completas com syn e quote, tudo se baseia no modelo descrito aqui.
Resumo
- Uma macro Rust é um programa em tempo de compilação que consome tokens de origem e emite novos tokens de origem antes que o pipeline normal do compilador seja executado.
- Por que Importa: Macros permitem eliminar boilerplate repetitivo e construir uma pequena sintaxe específica de domínio que uma função simples, que opera apenas em valores, não consegue expressar.
- Conceitos Chave: stream de tokens, expansão de macro, macro declarativa, macro procedural, higiene, site de chamada versus site de definição.
- Quando Usar: Implementações de traits repetitivas, incorporação de uma pequena DSL, geração de código a partir da forma de uma struct (como
derive), ou encapsulamento de um padrão sintático repetido que funções não podem abstrair. - Limitações / Compromissos: Macros trocam legibilidade e suporte de ferramentas (depuração, clareza de erros, completude de IDE) por poder expressivo, e devem ser um fallback após funções e genéricos serem descartados.
- Tópicos Relacionados: árvores de sintaxe abstrata, passes do compilador, derivação de traits, linguagens específicas de domínio.
Fundamentos
Uma macro em Rust é fundamentalmente diferente de uma função.
Uma função recebe valores e retorna um valor em tempo de execução.
Uma macro recebe código-fonte, representado como um stream de tokens, e produz mais código-fonte, e faz isso inteiramente durante a compilação.
Quando seu programa realmente roda, toda chamada de macro já foi substituída pelo código Rust em que ela se expandiu.
É por isso que macros podem fazer coisas que funções não podem, como aceitar um número variável de argumentos de diferentes tipos, gerar novos nomes de função ou struct, ou implementar um trait para uma struct apenas inspecionando sua lista de campos.
Rust tem duas famílias de macros, e entender qual delas você está olhando é a primeira habilidade que esta seção constrói.
Macros declarativas, escritas com macro_rules!, funcionam fazendo pattern-matching nos tokens passados para a macro contra um ou mais "braços", semelhante a uma expressão match, mas operando em sintaxe em vez de valores.
Cada braço tem um padrão e um template, e o primeiro padrão que corresponde determina qual código é substituído.
Macros procedurais são diferentes em tipo, não apenas em grau: elas são funções Rust comuns, compiladas em seu próprio crate especial, que recebem um TokenStream como entrada e retornam um TokenStream como saída.
Como uma macro procedural é apenas código Rust com um TokenStream de entrada e um TokenStream de saída, ela pode fazer qualquer coisa que um programa Rust possa fazer, incluindo analisar a entrada em uma árvore de sintaxe estruturada e raciocinar sobre ela antes de gerar a saída.
Uma analogia útil é a diferença entre um template preencher-lacunas e um plugin de compilador.
macro_rules! é o template: rápido de escrever, limitado a combinar formas que foi instruído a esperar, e incapaz de analisar verdadeiramente o que recebe.
Uma macro procedural é o plugin de compilador: muito mais trabalho para escrever, mas capaz de análise e geração de código arbitrárias porque tem todo o poder de um programa Rust disponível para inspecionar sua entrada.
// Uma macro declarativa mínima: combina uma forma, expande para uma expressão.
macro_rules! square {
($x:expr) => {
$x * $x // substituído literalmente no site de chamada
};
}Esse trecho ilustra a mecânica central: $x:expr captura qualquer expressão que o chamador passe, e o template à direita é o que o compilador vê após a expansão, nada mais exótico do que isso.
Mecânicas e Interações
A expansão de macro é uma fase distinta no pipeline do compilador, e saber onde ela se situa explica muito sobre como as macros se comportam.
O front-end do Rust primeiro tokeniza seu arquivo de origem, então a expansão de macro é executada, reescrevendo invocações de macro em seus streams de tokens expandidos, e somente após a expansão estar completa o compilador constrói a árvore de sintaxe abstrata que ele irá verificar tipos, verificar empréstimos e, eventualmente, reduzir para código de máquina.
Essa ordenação tem uma consequência direta: uma macro não pode ver tipos, não pode ver se uma variável é mutável, e não pode ver nada sobre a semântica do programa, porque nenhuma dessas informações existe ainda no momento da expansão.
Uma macro opera puramente na forma da sintaxe.
É também por isso que as mensagens de erro de macro podem parecer indiretas: o compilador relata erros contra o código expandido, então uma incompatibilidade de tipo dentro da saída de uma macro muitas vezes aponta para a definição da macro ou para o site de chamada de uma maneira que leva um momento para mapear de volta ao que você realmente escreveu.
A própria expansão prossegue de fora para dentro e pode recursar: uma invocação de macro pode expandir para código que contém mais invocações de macro, e o compilador continua re-expandindo até que nenhuma chamada de macro permaneça, limitada por um limite de recursão configurável para capturar expansão infinita.
Higiene é o mecanismo que torna essa substituição recursiva, que parece textual, realmente segura.
Sem higiene, uma macro que declara internamente uma variável chamada x poderia silenciosamente sombrear uma variável também chamada x no código do chamador, porque a substituição de texto ingênua não tem conceito de escopo.
As macros macro_rules! do Rust são higiênicas por padrão: identificadores introduzidos dentro do corpo da macro vivem em um "contexto de sintaxe" separado dos identificadores que o chamador passa, então um x interno à macro e um x do chamador nunca colidem, mesmo que compartilhem a mesma grafia.
Isso é o que torna seguro chamar uma macro sem ler sua implementação primeiro, a mesma confiança que uma chamada de função lhe dá.
Macros procedurais não obtêm essa garantia gratuitamente.
Como uma macro proc constrói sua saída token por token usando bibliotecas como quote, a higiene depende de como os spans são atribuídos a identificadores gerados, e um autor descuidado de macro proc pode gerar identificadores que vazam para o escopo do chamador.
Esta é uma das arestas mais afiadas no sistema de macros: macros declarativas são higiênicas por construção, macros procedurais são higiênicas apenas se seu autor for cuidadoso com isso.
Um segundo modelo mental que vale a pena manter é site de chamada versus site de definição.
O site de definição de uma macro é onde seu padrão e template são escritos, enquanto o site de chamada é onde ela é invocada, e as regras de higiene geralmente anexam os próprios nomes gerados de uma macro ao seu site de definição, enquanto permitem que os nomes que o chamador passou explicitamente sejam resolvidos no site de chamada, que é exatamente a divisão que impede a captura acidental em qualquer direção.
Considerações Avançadas e Aplicações
Em escala, a escolha entre macros declarativas e procedurais torna-se uma decisão de arquitetura, não apenas uma preferência de sintaxe.
Macros declarativas vivem inline no crate que as utiliza e compilam rapidamente, o que as torna o padrão certo para padrões repetitivos, mas simples: implementar o mesmo trait para uma lista de tipos, construir uma pequena DSL estilo builder, ou encapsular um padrão de asserção repetido em testes.
Macros procedurais exigem um crate separado proc-macro = true, trazem uma cadeia de dependências mais pesada (syn, quote, proc-macro2), e rodam como plugins de compilador que executam na máquina host durante a compilação de qualquer crate que as utilize, o que adiciona custo real de compilação em um workspace.
Esse custo compra capacidade real: uma macro procedural pode analisar uma definição de struct inteira, inspecionar nomes de campo, tipos e atributos, e gerar uma implementação de trait completa. É exatamente assim que funcionam #[derive(Serialize)] e derives semelhantes padrão do ecossistema, e nenhuma quantidade de inteligência macro_rules! pode replicar esse nível de análise estrutural, porque macros declarativas nunca constroem uma árvore de sintaxe real.
Ambos os tipos de macro interagem com o toolchain mais amplo de maneiras que valem a pena conhecer.
cargo expand mostra o código totalmente expandido para qualquer um dos tipos, que é a ferramenta de depuração mais útil em todo este sistema porque transforma uma pergunta abstrata "o que minha macro produziu" em Rust concreto que você pode ler.
O tempo de compilação é um custo real e mensurável que cresce com quantos crates em um grafo de dependência usam macros procedurais pesadas, já que cada uma adiciona sua própria unidade de compilação e passe de análise a cada build downstream.
O raciocínio de segurança também difere entre os dois: uma macro procedural é código Rust compilado arbitrário que roda durante sua compilação, então adicionar uma como dependência significa confiar que esse código será executado em sua máquina em tempo de compilação, uma fronteira que uma macro declarativa, que apenas reescreve tokens contra um matcher fixo, não cruza.
| Abordagem | Força | Fraqueza | Melhor Ajuste |
|---|---|---|---|
macro_rules! | Rápido de escrever, higiênico por padrão, sem crate extra | Não pode inspecionar tipos ou construir um AST real, pattern matching limitado | Templates de sintaxe repetitivos, pequenas DSLs, ajudantes de teste |
| Macro procedural | Programa Rust completo sobre a entrada, pode analisar e inspecionar estrutura | Crate separado, builds mais lentos, higiene é responsabilidade do autor | Implementações #[derive], codegen baseado em atributos, boilerplate de framework |
| Função ou genérico simples | Melhores mensagens de erro, suporte completo de IDE, sem etapa de expansão | Não pode gerar nova sintaxe ou variar pela forma literal de tokens do chamador | Qualquer coisa expressável sem tocar na própria sintaxe |
O sistema de macros também evoluiu junto com o resto da linguagem: crates de macros procedurais modernos dependem da análise tipada de syn e do "quoting" de tokens de quote para evitar manipulação manual de tokens, o que tornou as macros procedurais dramaticamente mais acessíveis do que eram na era anterior de plugins de compilador do Rust.
Concepções Erradas Comuns
- "Macros rodam em tempo de execução como uma chamada de função" - elas não rodam; cada macro é totalmente expandida durante a compilação, e o binário compilado contém apenas o código expandido, sem nenhum vestígio da invocação da macro em si.
- "Macros declarativas são apenas macros procedurais mais simples" - elas são um mecanismo inteiramente diferente, fazendo pattern-matching de tokens contra formas fixas em vez de executar um programa arbitrário sobre um stream de entrada.
- "Todas as macros são higiênicas" - macros
macro_rules!são higiênicas por padrão, mas macros procedurais só obtêm essa propriedade se seu autor atribuir spans cuidadosamente ao gerar identificadores. - "Macros são sempre a escolha poderosa, então prefira-as" - macros sacrificam clareza de erro, suporte de IDE e depurabilidade, e Rust idiomático busca funções e genéricos primeiro, macros apenas quando estes genuinamente não conseguem expressar o padrão.
- "Uma macro pode ver os tipos do que está recebendo" - a expansão de macro ocorre antes da verificação de tipos, então uma macro só pode raciocinar sobre a forma do token, nunca sobre o tipo real de um valor.
FAQs
O que exatamente é um "stream de tokens"?
Um stream de tokens é a sequência de tokens lexicais (identificadores, pontuação, literais, grupos delimitados) que compõem um pedaço de código-fonte, antes que ele seja analisado em uma AST.
- Macros declarativas combinam streams de tokens com padrões declarados.
- Macros procedurais recebem um stream de tokens como sua entrada de função literal e retornam um como saída.
Por que as mensagens de erro de macro às vezes apontam para um local confuso?
Porque o compilador verifica tipos e empréstimos no código expandido, não na invocação da macro que você escreveu, então um erro pode aparecer em uma linha dentro do template da macro que você nunca digitou diretamente. cargo expand é a maneira padrão de ver exatamente qual código produziu o erro.
É `macro_rules!` mais antigo ou mais simples que macros procedurais?
É mais simples em mecanismo, não necessariamente mais antigo em termos de capacidade, e ambos permanecem partes totalmente suportadas e idiomáticas da linguagem. Cada um visa um problema diferente: macro_rules! para templates sintáticos, macros procedurais para análise e geração de código estrutural.
Como a higiene realmente impede a captura de variáveis?
Cada identificador que uma macro introduz carrega um contexto de sintaxe ligado a onde foi definido, então um identificador chamado x gerado dentro do corpo da macro é tratado como distinto de um identificador chamado x fornecido pelo chamador, mesmo que ambos sejam escritos da mesma forma.
Uma macro pode chamar outra macro?
Sim, a expansão de macro é recursiva, e o compilador continua re-expandindo até que nenhuma invocação de macro permaneça, sujeito a um limite de recursão configurável que protege contra expansão descontrolada.
Macros procedurais rodam quando compilo meu código, ou quando meu programa roda?
Elas rodam na máquina host durante a compilação de qualquer crate que as invoque, como parte do próprio build, nunca em tempo de execução no binário compilado.
Por que macros procedurais precisam de seu próprio crate?
O compilador precisa compilar e executar o código da macro antes de poder compilar o crate que a chama, então as definições de macro proc devem viver em um crate marcado como proc-macro = true que é compilado e executado como uma etapa separada e anterior.
Para que servem `syn` e `quote`, conceitualmente?
syn analisa um stream de tokens em uma árvore de sintaxe Rust tipada para que uma macro procedural possa raciocinar sobre a estrutura em vez de tokens brutos, e quote faz o oposto, transformando sintaxe semelhante a Rust de volta em um stream de tokens para retornar como saída.
É verdade que macros sempre tornam o código mais difícil de ler?
Não inerentemente, mas uma macro mal escopada pode ocultar o fluxo de controle ou gerar código surpreendente, então o compromisso honesto é poder expressivo contra transparência, que é por que a orientação de melhores práticas da seção favorece funções e genéricos primeiro.
Macros afetam o desempenho em tempo de execução?
Não diretamente, já que a saída de uma macro é código Rust comum que é compilado e otimizado como qualquer outra coisa, então quaisquer características de desempenho vêm inteiramente do que o código expandido faz, não do fato de que uma macro o produziu.
Quando devo escolher uma macro em vez de uma função genérica?
Alcance uma macro apenas quando o padrão varia no nível da sintaxe de uma maneira que um genérico não consegue expressar, como um número variável de argumentos de tipos diferentes, geração de novos identificadores, ou inspeção da forma de uma struct para implementar um trait automaticamente.
Qual é a diferença entre "site de definição" e "site de chamada"?
O site de definição é onde o padrão e o template da macro são escritos, e o site de chamada é onde a macro é realmente invocada. As regras de higiene geralmente resolvem nomes internos da macro no site de definição, enquanto permitem que tokens fornecidos pelo chamador sejam resolvidos onde o chamador os escreveu.
Uma macro pode falhar na compilação de uma maneira que uma função não pode?
Sim, uma macro pode produzir uma falha de pattern-match se nenhum braço corresponder aos tokens de entrada, um erro distinto de qualquer erro de tipo ou de empréstimo, e essa falha ocorre durante a expansão, antes que o compilador tenha sequer tentado verificar os tipos do código gerado.
Relacionados
- Noções Básicas de Macros - um tutorial prático de padrões comuns de macros que se baseia no modelo mental aqui.
- macro_rules! - mergulho profundo na sintaxe de macros declarativas, matchers e repetição.
- Visão Geral de Macros Procedurais - os três tipos de macros procedurais e como elas compilam e rodam.
- syn & quote - as bibliotecas de análise e geração de tokens nas quais as macros procedurais são construídas.
- Melhores Práticas de Macros - quando escolher uma macro em vez de uma função ou genérico.
Versões da Pilha: Esta página foi escrita para Rust 1.97.0 (edição 2024), Tokio 1.x, Axum 0.8, serde 1.0, sqlx 0.8, clap 4, e Polars 0.46+.