Pular para o conteúdo

Racional · ensaio 10

FFI

rationale.md · 77 linhas · 3 min de leitura

A redefinição: @cimport lê o header do C direto, e todo o estrangeiro vive sob c.. O ponto grita “isto é C” em cada contato.

Falar com C é obrigatório para uma linguagem de sistemas: o ecossistema de meio século está em C. A forma tradicional é escrever bindings à mão (redeclarar cada protótipo, cada struct, manter isso em sincronia) ou gerar bindings com uma ferramenta externa. De um jeito ou de outro, você reconstrói o que o C já expõe.

@cimport("header.h") roda em comptime, invoca um compilador C embutido, traduz o header inteiro e devolve um módulo sob o namespace-raiz c. Você não redeclara nada: @cimport("stdio.h") te dá c.stdio.printf direto. Os tipos fundamentais (dependentes de compilador/plataforma) vêm de um runtime importado: c.gcc.size_t é o size_t como o GCC o define.

Tudo do C fica sob c., e isso é proposital: o ponto grita “aqui há contrato C, não Makoto” em todo ponto de contato, e o seu namespace fica limpo (quem não faz FFI nunca vê nada de C). Chamar C é unsafe (cada chamada é uma caixa-preta que pode violar qualquer invariante), abatido com assert/assume ou agrupado num bloco; e o retorno valida na borda (ponteiro que pode ser NULL vira Optional, bytes que viram string passam por Result).

Na fronteira, a memória continua colorless: @mm(c) (o gerenciador delega para o free do C), @promote(gc) (repatria para o GC), @pin/@unpin (fixa um objeto GC enquanto o C o segura). E o irmão mais baixo: inline assembly via @asm.

A aposta é “o ecossistema C de graça, se você aceitar a marca de segurança”. Se o C é inerentemente unsafe, reescrever bindings à mão não compra segurança nenhuma; só adiciona burocracia e uma cópia que desincroniza. Melhor ler o header direto (a fonte da verdade já existe) e gastar o esforço onde ele importa: na fronteira, marcando o perigo (unsafe, validação na borda) e o crossing de memória (@mm(c)/@promote/@pin).

O namespace c. é o não-envenenamento aplicado à FFI (cap. 01): o estrangeiro não polui o global, e a marca é o próprio caminho do nome. É o mesmo motivo de unsafe ser uma marca e não um modo silencioso: perigo deve ser visível, e c.malloc no meio do seu código é visível.

  • Bindings escritos à mão. Reconstroem o que o C já expõe, desincronizam quando o header muda, e não compram segurança (C continua unsafe). @cimport lê a fonte da verdade.
  • FFI sem namespace dedicado (extern "C" no escopo, à la Zig @import("c") misturado). Nomes colidem (open é seu ou do C?) e a fronteira fica invisível. O c. desambigua e marca.
  • Conversão automática int C i32 Makoto. Esconderia o custo: em alguns alvos int é 32 bits, em outros 64, e o bug fica invisível. Os tipos do C vêm explícitos (c.gcc.size_t); o crossing é escrito.
  • Coloring de async/memória atravessando a fronteira automaticamente. A memória do C chega @mm(c) e você decide o que fazer (@promote, @pin), explícito, porque cruzar mundos é uma decisão, não uma coerção.

O projeto que mantém um arquivo de bindings de 2000 linhas em sincronia manual com um header que muda a cada release. O Zig em que você passa um i32 para um parâmetro int do C e funciona em x86 mas quebra em outro alvo, sem nenhuma pista no código. O NULL que volta de uma função C e vira segfault três chamadas adiante porque ninguém o tratou. Cada uma é a fronteira C mal-marcada cobrando o preço.

Aponta para o .h, e o C funciona. Todo o estrangeiro vive sob c. (que grita “isto é C”); o perigo é unsafe e a memória cruza os mundos explicitamente.

@cimport exige um compilador C embutido na toolchain (peso real, mas paga-se uma vez). Cada chamada a C carrega a obrigação de unsafe + validação na borda, mais cerimônia que um simples call, de propósito, porque o perigo é real. E o c. em cada nome é verboso quando você usa C intensamente, mas é a marca que mantém a fronteira honesta.

Próximo: 11 · Ecossistema e tiers