Inline assembly
Quando e por quê
Seção intitulada “Quando e por quê”Inline assembly é a extensão low-level da FFI (seção 17): assim como o @cimport traz C pra dentro, o assembly traz instruções de máquina cruas, pra quem precisa descer ao metal (um syscall, um rdtsc, uma instrução SIMD que o compilador não emite, uma barreira de memória). É a fronteira mais baixa que a linguagem oferece, usada por uma minoria, e como toda fronteira pra fora das garantias, é unsafe: o juízo “este assembly está correto” é inexprimível pro compilador, é o caso-B humano do assume (seção 5).
Há duas formas, que compartilham os mesmos decoradores: a função-assembly (a função inteira é asm, com params e retorno ligados a registradores) e o bloco inline (instruções no meio de código normal). A regra pra escolher é simples. Se você quer um valor de asm, nomeado e reutilizável, use a função; se quer rodar asm no meio de código normal, mexendo em variáveis que já existem, use o bloco.
Função-assembly
Seção intitulada “Função-assembly”O corpo da função é assembly, e os params e o retorno se ligam a registradores via @asm(in, …)/@asm(out, …) colocados no próprio param (o binding fica junto do que ele liga):
@asm(x86, intel)unsafe fn add10(value: u32 @asm(in, rax)) -> u32 @asm(out, rbx) { mov rbx, rax add rbx, 10}
// uso: forma-valor, como qualquer função:result := unsafe add10(x) assume "x cabe em u32 e rbx não é usado pela ABI aqui"@asm(x86, intel) decora a função com arquitetura e dialeto (e opções, abaixo). value: u32 @asm(in, rax) liga o param ao rax; -> u32 @asm(out, rbx) diz que o retorno é o rbx. O mesmo @asm carrega dois papéis sem ambiguidade: na função ele leva (arquitetura, dialeto); num param ou local ele leva (in|out|clobber, registrador), e a posição mais o primeiro argumento distinguem. Os clobbers são implícitos aqui: a função é uma fronteira de chamada, então a ABI já define quais registradores ela suja, e você não lista nada. A função retorna valor (result := add10(x)), é nomeada, reutilizável, testável, e com inlining tem custo zero. É a forma pra “asm que produz um valor”.
Bloco inline
Seção intitulada “Bloco inline”Pra rodar asm no meio de uma função normal, use o bloco. Os decoradores ficam empilhados acima do unsafe {} (decorador sempre em cima do que decora), e o bloco contém só instruções:
var lo: u32var hi: u32@asm(x86, intel)@asm(out, rax) lo@asm(out, rdx) hi@asm(clobber, "memory")unsafe { rdtsc // põe o timestamp em EDX:EAX}// 'lo' e 'hi' têm o valor agoraO cabeçalho liga locals em escopo a registradores (@asm(out, rax) lo), e o unsafe {} (que já é escopo-expressão, seção 5) carrega as instruções. São três diferenças em relação à função, cada uma por um motivo:
- Binding por cabeçalho, não por param: o bloco não tem assinatura onde colocar o binding, então os
@asm(in, …)/@asm(out, …)ficam acima, ligando locals. - Clobbers explícitos (
@asm(clobber, …)), e aqui é correção, não verbosidade: o bloco está no meio de código que usa registradores, então ele tem que declarar o que suja (registradores,"memory"), senão o compilador assume que estão intactos e gera código errado. A função não precisa porque a ABI cobre; o bloco precisa porque protege o código ao redor. - É statement, não expressão: o bloco escreve nos locals de saída (
@asm(out, rax) loescreve emlo), não avalia pra um valor. Quem quer asm como valor usa a função-assembly; o bloco cobre o “asm no meio do código”. (Não há forma-expressão: ela seria uma função-assembly anônima, duplicando a função sem poder reusar nem nomear, e oresult :=no topo, separado dounsafe {}lá embaixo por três decoradores, lê pior.)
Registradores: explícito ou xreg
Seção intitulada “Registradores: explícito ou xreg”Cada binding nomeia um registrador específico (rax) ou a classe xreg (o compilador escolhe um registrador livre, e otimiza):
@asm(x86, intel)unsafe fn double(value: u32 @asm(in, xreg)) -> u32 @asm(out, xreg) { add value, value // refere pelo NOME: você não sabe qual registrador é}A diferença no corpo: com registrador explícito você escreve o registrador cru (rax) ou o nome do operando, e ambos resolvem, porque você sabe qual é. Com xreg você tem que usar o nome simbólico (value), porque o registrador é escolhido pelo compilador e você não tem como nomeá-lo. Explícito é pra quando a instrução exige um registrador específico (muitos syscalls); xreg é pra quando você só precisa de “um registrador” e quer deixar o alocador otimizar.
Dialetos por arquitetura
Seção intitulada “Dialetos por arquitetura”O dialeto vive no segundo argumento do @asm, e é por arquitetura (cada máquina tem a sua sintaxe):
- x86:
intelouatt(a mesma instrução, grafias diferentes, ambas parseadas pelo backend). - ARM e RISC-V: os dialetos nativos de cada uma.
- WASM: as duas sintaxes do WebAssembly text (folded/S-expression e linear/stack), aceitas no mesmo bloco sem toggle, porque a forma folded é só açúcar da linear (o parser de WAT expande uma na outra).
@asm(x86, att) // mesma adição, dialeto AT&Tunsafe fn add10(value: u32 @asm(in, rax)) -> u32 @asm(out, rbx) { movl %eax, %ebx addl $10, %ebx}WASM é assimétrico: sem registradores
Seção intitulada “WASM é assimétrico: sem registradores”WASM é uma máquina de pilha, não tem registradores. Então todo o modelo de binding por registrador (@asm(in, rax)) não se aplica: não há rax. Mas isso é menos trabalho, não mais, porque o WASM já é mais alto-nível, com params e locals tipados nativamente. Uma função-assembly WASM liga pelos próprios params (sem @asm(in/out)); um bloco WASM liga por locals:
@asm(wasm, wasm) // sem @asm(in/out): WASM já tem params/result tipadosunsafe fn add(a: u32, b: u32) -> u32 { local.get a local.get b i32.add}A assimetria é parte do design: binding por registrador no x86, ARM e RISC-V, e binding nativo por param ou local no WASM. O @asm tem semântica de operando dependente da arquitetura: nas máquinas de registrador você liga registradores, na máquina de pilha os params já são os operandos.
Seleção por arquitetura
Seção intitulada “Seleção por arquitetura”Como asm é específico de arquitetura, você escreve uma versão por arquitetura que suporta e o compilador escolhe a do alvo, via o pacote arch da stdlib (constantes comptime: .x86_64, .arm, .riscv, .wasm e afins) com o comptime match/comptime if que já existe:
use arch
fn timestamp() -> u64 { comptime if arch.current == .x86_64 { return unsafe rdtsc_x86() assume "rdtsc disponível neste alvo" } return portable_clock() // outro alvo: caminho normal, sem asm}O prefixo comptime é o que faz a seleção resolver em tempo de compilação: o if inteiro é avaliado pelo compilador, um braço é escolhido e SÓ esse braço é emitido (eliminação do braço morto). É o mesmo prefixo que comptime match e comptime loop usam, e é o que um if de runtime não conseguiria — um if de runtime ainda emitiria os dois braços, então o @asm de outra arquitetura no braço não-tomado chegaria ao codegen. A seleção roda em comptime (o arch.current é constante de compilação), então o binário do alvo só contém o caminho da sua arquitetura. E mismatch de arquitetura é erro de compilação: compilar um bloco @asm(x86, ...) pra um alvo ARM ou WASM não compila. Você precisa fornecer o bloco da arquitetura alvo, sem fallback mágico (é a natureza do asm; um mov rax não existe em ARM). É a mesma rigidez do Rust e do Zig: sem o asm da arch, não compila.
Assembly em arquivos
Seção intitulada “Assembly em arquivos”Além do inline, assembly também pode morar em arquivos próprios (.s / .asm / .extasm), pra trechos grandes (uma rotina cripto inteira, um hot path extenso) que poluiriam o código se inline. Os arquivos seguem os mesmos dialetos por arquitetura, e são linkados pelo build como o .c do FFI.
Backend e o que fica fora
Seção intitulada “Backend e o que fica fora”O assembly é emitido pelo LLVM por enquanto, escolha pragmática que viabiliza os múltiplos alvos (incluindo WASM) e o cross-compile sem reescrever um assembler por arquitetura. Um backend próprio está nos planos (o caminho que o Zig trilhou: LLVM primeiro, backends nativos depois), mas é trabalho futuro.
E o que não entra é uma sintaxe de assembly normalizada própria (o que o Go fez com Plan9, uma única gramática cross-arch). Com suporte aos N dialetos reais (Intel, AT&T, ARM, RISC-V, WASM), inventar mais uma sintaxe unificada é trabalho sem retorno: você escreve no dialeto nativo de cada arquitetura, que é o que as ferramentas e a documentação de cada ISA já usam.