Um formato binário para dados que precisam durar.
Graven é uma especificação aberta para um formato de serialização zero-copy e sua linguagem de schema. As mensagens são lidas no lugar, direto do buffer. O formato está congelado e totalmente especificado, e seus vetores de conformidade permitem que uma implementação em qualquer linguagem prove que lê cada mensagem corretamente.
Inspirado no FlatBuffers e no Cap'n Proto. Partimos das ideias deles, medimos seus trade-offs em benchmarks e seguimos explorando até chegar a este formato. Como chegamos aqui
Formato v1 congelado. Implementação de referência em Rust. Licença MIT.
A especificação e a documentação técnica estão em inglês.
namespace bank; table Entry { account @0: u64; amount @1: i64; memo @2: string?; }
Feito para dados que duram mais que o código que os escreveu
Dados de negócio são escritos uma vez e lidos por anos: pela próxima versão, por um serviço que só atualiza depois, por uma auditoria muito tempo após a equipe original ter seguido em frente. O Graven foi pensado para essa realidade.
- Leitura no lugar
- Os campos são lidos direto dos bytes. Strings e fatias de bytes são emprestadas do buffer, então ler um registro grande não custa nada até você tocar num campo.
- Evoluir sem adivinhar
- Um lockfile registra cada layout. O compilador recusa mudanças que fariam código antigo e novo discordarem, e pede confirmação explícita antes de qualquer coisa quebrar um leitor antigo.
- Bytes não confiáveis podem ser abertos com segurança
- Uma única passada de
verifypercorre a mensagem inteira com limites fixos de profundidade e de trabalho. Depois disso, a view verificada lê sem checar de novo, e entrada malformada só consegue produzir um erro. - Saber o que aconteceu com um arquivo
- O envelope opcional carrega um schema id e um CRC32C. Uma escrita interrompida por uma queda é reportada como incompleta; um bit trocado é reportado como corrupção. Os dois nunca se confundem.
Como o formato se mantém confiável
Cada afirmação da especificação é garantida por algo que roda. Isto é o que é verificado, e como.
| Garantia | Como é verificada |
|---|---|
| O formato não muda por acidente | Uma especificação normativa (linguagem RFC 2119) e 42 vetores de conformidade congelados, cada um um .bin
com seu resultado esperado. O writer de referência precisa reproduzir os vetores canônicos byte a byte. |
| Duas implementações concordam | Dentro da implementação de referência, um interpretador independente guiado pelo schema como dado e o código gerado leem cada vetor e cada mutação de um byte dele. Os dois precisam aceitar, recusar e decodificar da mesma forma. Toda nova implementação é cobrada pelos mesmos vetores. |
| Entrada hostil fica limitada | verify limita a profundidade a 64 e o trabalho total a max(len × 8, 1 MiB), então ciclos e
amplificação por dados compartilhados são recusados. Cinco alvos de fuzzing rodaram mais de 80 milhões de entradas
com checagem de overflow ligada, sem nenhum achado. |
| O caminho rápido com unsafe é correto | Na implementação em Rust, o único código unsafe pula a revalidação de UTF-8 depois do verify. Suas invariantes
estão documentadas, são cobradas por um teste de mutação, e o runtime e o código gerado rodam sob o Miri sem
nenhum comportamento indefinido reportado. |
| Mudanças de schema não corrompem em silêncio | O compilador compara cada mudança com o lockfile: 15 regras recusam mudanças que leriam dados antigos errado, e 2 avisos exigem confirmação no schema. Um corpus de cadeias de versões testa cada regra. |
| Queda e corrupção são diferenciadas | A ordem de checagem do envelope é normativa, então um final truncado e um registro danificado produzem erros diferentes e previsíveis em todas as implementações. |
Mudanças de schema são revisadas pelo compilador
Mude um tipo ou acrescente um campo obrigatório, e o gravenc update recusa, citando a regra e a linha.
Acrescente o campo com um default, e o novo lock é escrito. Esta é a saída real.
Recusado
table Entry {
account @0: u64;
amount @1: i32;
memo @2: string?;
currency @3: u16;
}
$ gravenc update ledger.graven
ledger.graven:5:3: field `Entry.amount` (@1): type changed
from `i64` to `i32` (type-changed)
ledger.graven:7:3: new field `Entry.currency` (@3) is
required: every new reader would fail on old messages;
use `u16?` or a default (new-required)
Aceito
table Entry {
account @0: u64;
amount @1: i64;
memo @2: string?;
currency @3: u16 = 986;
}
$ gravenc update ledger.graven
ledger.graven.lock: written
Para que serve
O Graven se encaixa onde registros são escritos uma vez, guardados por muito tempo e lidos por software que muda.
- Livros-razão e logs de eventos
- Acrescente envelopes a um arquivo e nunca os reescreva. Cada registro carrega seu schema id e checksum, uma queda deixa um final reconhecidamente incompleto, e relatórios podem varrer o arquivo sem desserializá-lo.
- Protocolos de serviços e SDKs
- Roteie requisições pelo schema id, verifique uma vez na entrada e repasse a view verificada sem copiar. As regras de compatibilidade garantem que um cliente antigo continue funcionando quando o servidor avança.
- Arquivos e trilhas de auditoria
- Um formato congelado com vetores de conformidade públicos é algo que você ainda consegue decodificar daqui a dez anos, com outra implementação se for preciso.
- Dispositivos e sincronização offline
- O formato não precisa de alocador nem de alinhamento, e o runtime em Rust é
no_stdcom duas dependências, então os mesmos registros podem vir de terminais de ponto de venda e dispositivos embarcados que os acumulam enquanto estão offline. - Caches e valores armazenados
- Guarde registros num banco chave-valor e leia só o campo de que precisa, direto dos bytes armazenados.
- Fronteiras com plugins
- Troque buffers com extensões não confiáveis, como módulos WebAssembly, e deixe o
verifyproteger o host de qualquer coisa que elas devolvam.
Implementações
A especificação é o produto; as implementações são medidas por ela. Uma implementação conforme lê os 42 vetores com exatamente o resultado esperado (spec §10).
| Linguagem | Status | O que inclui |
|---|---|---|
| Rust | Implementação de referência. Passa em todos os vetores v1. | Runtime (no_std), o compilador gravenc com lockfile e checagem de
compatibilidade, geração de código Rust e o interpretador de conformidade.
Referência da API |
Escrevendo uma implementação
Comece pelo algoritmo de layout (spec §3.3), pelas regras de leitura (§4–§6) e pelo verify (§7), e então rode
os vetores de conformidade. Cada vetor é um .bin com um
.json que traz o valor decodificado ou o erro exato, e cada vetor inválido tem um único defeito,
então uma falha aponta para uma única regra.
Gerando código para outra linguagem
A linguagem de schema e suas regras de compatibilidade não dependem de linguagem. O lockfile escrito pelo
gravenc já registra o layout, o schema id e a presença dos campos de cada tabela, que é o que um
gerador de código para outra linguagem precisa para emitir leitores e writers.
Primeiros passos com Rust
Usar a implementação de referência exige só o toolchain do Rust.
-
Instale o compilador
cargo install --path crates/graven-compiler -
Escreva um schema e crie seu lockfile
gravenc update schemas/bank.graven --init -
Gere o código no
build.rsgraven_compiler::build::Build::new() .schema("schemas/bank.graven") .compile() .unwrap_or_else(|e| panic!("{e}")); -
Escreva e leia
let bytes = TransactionW::to_bytes(&mut b, |t| { t.id(42).currency(Currency::USD); })?; let tx = Transaction::from_bytes(&bytes)?; println!("{}", tx.id());
O guia de primeiros passos e a referência da linguagem de schema (em inglês) cobrem o fluxo completo.