Modelo de memória
Em Python e JavaScript, você nunca pensa em quem libera memória. Você aloca algo, e em algum momento depois um coletor de lixo percebe que nada mais referencia aquele valor e o recupera. Funciona, mas tem custos: pausas imprevisíveis, overhead de memória extra, e você genuinamente não sabe quando os destrutores são disparados.
Em C, o extremo oposto: você chama malloc e chama free. Esqueça o free e você vaza. Chame duas vezes e você trava.
Glide encontra um caminho do meio com duas ferramentas: para valores locais ao escopo, o compilador insere o free por você (auto-drop); para valores que precisam sobreviver a um escopo, você segura um ponteiro bruto e o libera sozinho — com o verificador de empréstimos garantindo que emprestar um valor jamais se torne um uso-após-liberação. Sem coletor de lixo. Sem anotações de tempo de vida como o 'a do Rust. Este capítulo explica como ambos funcionam.
Onde cada coisa vive
Dois lugares onde um valor pode residir:
- A pilha: valores escalares (
i32,bool,f64...) e structs por valor (let p: Point = Point { x: 1, y: 2 };). Barato, automático, com escopo restrito à chamada de função. Copiado na atribuição e no retorno. - O heap: qualquer coisa que cresce em tempo de execução —
*Vector<T>,*HashMap<V>, um*MinhaCoisadefinido pelo usuário viaMinhaCoisa::new(). O prefixo*no tipo é o seu sinal de que o valor vive no heap e é acessado por meio de um ponteiro.
let n: i32 = 42; // pilha
let v: *Vector<i32> = Vector::new(); // heap — um ponteiro para ele
Dois tipos de ponteiro para o heap
Esta é a ideia central que faz o modelo de memória do Glide fazer sentido. Um ponteiro para o heap vem em dois sabores, e você escolhe qual deles pela forma como escreve o binding:
let v* = Vector::new(); // ① auto-drop: liberado ao fim do escopo
let p: *Vector<i32> = Vector::new(); // ② bruto: você gerencia
① Auto-drop — o * escrito depois do nome do binding significa "eu possuo isso; libere-o por mim quando este escopo terminar." Você não escreve nenhum free; o compilador o insere na chave de fechamento. O porém: um valor auto-drop não pode sair do seu escopo. Você não pode retorná-lo, não pode atribuí-lo a outro binding, não pode passá-lo a uma função que receba o ponteiro por valor — tudo isso permitiria que ele fosse liberado duas vezes.
② Bruto — um binding *T simples sem o marcador * é apenas um endereço. O compilador não o rastreia e não o libera. Você é o responsável: chame free(p as *void) quando terminar, passe-o a uma arena, ou — em um programa de vida curta — deixe-o viver até o processo encerrar. Construtores como Vector::new() e Tipo::new() devolvem ponteiros brutos.
Auto-drop: limpeza que você não precisa escrever
Use let nome* = ... para um valor no heap que nasce e morre dentro de uma única função:
fn process() -> i32 {
let v* = Vector::new(); // possui aqui
v.push(10);
v.push(20);
return v.len();
} // compilador libera v aqui — você não escreveu nenhum `free`
Tente deixá-lo escapar e o compilador o impede:
fn make_list() -> *Vector<i32> {
let v* = Vector::new();
v.push(1);
return v; // ✗ erro: não é possível retornar valor com posse `v` (auto-drop o liberaria)
}
Esse erro resume toda a história de segurança em uma linha: o compilador vai liberar v ao fim de make_list, então devolvê-lo ao chamador seria um uso-após-liberação. Ele não vai deixar você fazer isso.
Retornando um valor do heap: use um ponteiro bruto
Quando uma função genuinamente precisa construir algo e devolvê-lo, retorne um ponteiro bruto (sem o marcador *). O chamador assume a responsabilidade:
fn make_list() -> *Vector<i32> {
let v: *Vector<i32> = Vector::new(); // bruto — sem auto-drop
v.push(1);
v.push(2);
return v; // tudo bem: nada o libera aqui
}
fn main() -> i32 {
let list: *Vector<i32> = make_list(); // agora seguramos um ponteiro bruto
println!(list.len());
// não é liberado automaticamente; para um programa curto, tudo bem viver até o fim.
// código de longa duração usaria `free(list as *void)` (ou uma arena).
return 0;
}
Note a troca: a versão com auto-drop é mais segura, mas não pode escapar; a versão bruta pode escapar, mas você é responsável pela limpeza. Não existe uma terceira opção que seja as duas coisas — essa honestidade é o ponto.
Empréstimos: emprestar, não dar
Um empréstimo é acesso temporário de leitura ou escrita a um valor que o tomador não possui. Dois sabores:
&T— empréstimo compartilhado. Somente leitura. Vários tomadores podem ter um ao mesmo tempo.&mut T— empréstimo exclusivo. Leitura/escrita. Apenas um por vez, sem outros.
fn print_first(v: &*Vector<i32>) { // recebe um empréstimo compartilhado
println!(v.get(0));
}
fn append_one(v: &mut *Vector<i32>) { // recebe um empréstimo mutável
v.push(99);
}
fn main() -> i32 {
let v* = Vector::new();
v.push(1);
v.push(2);
print_first(&v); // empresta, somente leitura
append_one(&mut v); // empresta, com escrita
println!(v.len()); // ainda somos os donos — os empréstimos terminaram quando as chamadas retornaram
return 0;
}
Leia os padrões:
- O tipo do parâmetro da função diz que sabor de acesso ela precisa.
- O chamador marca o ponto de chamada com
&ou&mutpara combinar. - A posse permanece com o chamador o tempo todo.
Por que Glide não precisa de anotações de tempo de vida
Rust força você a anotar por quanto tempo os empréstimos vivem quando o compilador não consegue descobrir sozinho (fn foo<'a>(...)). A análise de fluxo do Glide é suficientemente forte para que você quase nunca precise disso — o tempo de vida de um empréstimo é inferido pelo contexto de uso.
A troca que Glide faz: nos raros casos em que a análise não consegue determinar, você não pode contornar com uma anotação. Em vez disso, você reestrutura o código (frequentemente clonando, às vezes mudando os padrões de posse). Para 95% dos casos, isso é um grande ganho de ergonomia.
Auto-drop em ação
Veja o compilador decidir, usando a forma auto-drop (com o marcador *):
fn main() -> i32 {
let v* = Vector::new();
v.push(1);
v.push(2);
if some_condition() {
let w* = Vector::new();
w.push(10);
// w é liberado ao fim deste ramo
}
// v é liberado ao fim de main
return 0;
}
O compilador calcula: w só é alcançável dentro do bloco if, então sua liberação vai na } de fechamento do bloco. v é alcançável ao longo de todo main, então sua liberação vai logo antes do return 0;. Você não vê nenhuma chamada a free no seu código; o compilador as emite nos pontos exatamente certos.
Isso funciona corretamente até mesmo com retornos antecipados e propagação de ? — todo caminho de saída libera os valores ativos.
Arenas: alocação em lote para caminhos críticos
Alguns programas alocam centenas de pequenos objetos durante uma única operação (analisar uma requisição, processar um quadro), todos os quais se tornam inúteis ao fim dessa operação. Chamar free em cada um é desperdício; é melhor liberá-los todos de uma vez.
É para isso que serve uma arena: um bloco de memória do qual você aloca de forma barata e libera em uma única operação. Glide vem com uma:
let arena: *void = __glide_palloc_make();
let prev: *void = __glide_palloc_get();
__glide_palloc_set(arena);
defer __glide_palloc_free(arena);
defer __glide_palloc_set(prev);
// todo Vector::new(), .concat(), .substring(), etc. dentro deste escopo
// aloca a partir de `arena` e é liberado de uma vez quando arena é liberada.
Você verá esse padrão no topo de um handler de requisição HTTP — cada alocação por requisição vai para a arena, e tudo é liberado quando a resposta é enviada. Muito mais rápido do que rastrear cada alocação individualmente.
Por que não há coletor de lixo
Três razões pelas quais Glide prescinde do GC:
- Desempenho previsível. Um GC introduz pausas que você não consegue cronometrar. Para um servidor que trata milhares de requisições por segundo, uma pausa de 50ms do GC é um pico de latência de 50ms para qualquer usuário que tenha o azar de cair no meio dela.
- Menor overhead de memória. GCs tipicamente mantêm 1,5–3× o conjunto de trabalho ativo a qualquer momento. A posse em tempo de compilação usa exatamente o que você precisa.
- Honestidade. Um GC esconde a alocação. Com a posse explícita, você consegue ler uma função e saber o que aloca e o que não aloca.
O custo é o que este capítulo acabou de cobrir: você pensa um pouco sobre quem possui o quê. O retorno são programas rápidos, previsíveis e enxutos.
Tabela comparativa rápida
| O que Python faz | O que Glide faz |
|---|---|
| Contagem de referências + GC | Sem GC — auto-drop ou um ponteiro bruto que você gerencia |
| Liberado quando a última referência cai | let v* liberado ao fim do escopo; *T bruto liberado por você |
| Tudo é uma referência | Valores na pilha são copiados; heap acessado via *T; emprestado com & / &mut |
| Sem anotação de memória | Ponteiro *T no heap, let v* auto-drop, empréstimo &T |
| Uso-após-liberação pode travar em tempo de execução | Retornar ou mover um valor auto-drop é um erro de compilação |
Para onde ir agora
Você já consegue ler código Glide e entender quem possui o quê. O próximo capítulo — Structs, enums, traits, generics — é sobre modelar seus próprios dados e comportamentos.