Cos'è Zig e perché vuole sostituire il C, non Rust
Cos'è Zig: niente flusso di controllo nascosto, allocatori espliciti, comptime al posto delle macro e un compilatore C incluso. Con i limiti di oggi.
Senti parlare di Zig quasi sempre di riflesso: "Bun è scritto in Zig", "questo terminale è scritto in Zig". Poi provi a capire cosa sia e trovi due risposte opposte, chi lo presenta come l'alternativa a Rust e chi come un C rifatto. In questo articolo trovi cosa è davvero, quale problema risolve, e perché il confronto giusto non è con Rust ma con il C.
Cos'è Zig
Zig è un linguaggio compilato di basso livello, senza garbage collector, progettato per sostituire il C: stesso controllo diretto sulla memoria e sull'hardware, ma senza le trappole storiche del C e con la regola che nessuna operazione costosa o nascosta avvenga senza che tu la veda scritta nel codice.
È stato creato da Andrew Kelley ed è sviluppato da una fondazione senza scopo di lucro. Il dettaglio più importante da sapere subito: non ha ancora raggiunto la versione 1.0 al momento in cui scrivo. Il linguaggio cambia tra una versione e l'altra, e codice scritto un anno fa può non compilare più. Ci torno più avanti, perché è il suo limite principale.
Il principio che spiega tutto: niente di nascosto
Il C++ e molti altri linguaggi fanno cose alle tue spalle. Una somma può chiamare una funzione, se qualcuno ha ridefinito l'operatore. Un'assegnazione può copiare un oggetto enorme. Una riga qualsiasi può lanciare un'eccezione che salta tre livelli di chiamate. Una funzione di libreria può allocare memoria senza dirtelo.
Zig elimina tutto questo per scelta:
- Niente overload degli operatori. Se leggi
a + b, è una somma e basta. - Niente eccezioni. Gli errori sono valori, e ogni punto in cui un errore può risalire è marcato nel codice.
- Niente allocazioni nascoste. Se una funzione ha bisogno di memoria dinamica, deve ricevere un allocatore come parametro.
- Niente preprocessore e niente macro. Al loro posto c'è
comptime, di cui parlo sotto.
L'effetto pratico è che leggendo una funzione sai cosa fa, senza dover inseguire definizioni sparse in altri file. È la stessa filosofia di Go applicata a un livello molto più basso: meno funzionalità, più prevedibilità.
Gli allocatori espliciti
È la caratteristica che distingue Zig da quasi tutto il resto, ed è anche la più strana per chi arriva da altri linguaggi.
In C chiami malloc e la memoria arriva da un allocatore globale unico. In Zig non esiste un allocatore globale implicito: la funzione che deve allocare lo riceve come argomento.
const std = @import("std");
pub fn main() !void {
const allocator = std.heap.page_allocator;
const numeri = try allocator.alloc(u32, 100);
defer allocator.free(numeri);
numeri[0] = 42;
}
Sembra solo più scomodo, ma ha tre conseguenze concrete:
- Vedi dalla firma di una funzione se alloca memoria. Se non riceve un allocatore, non alloca. È un'informazione che in C devi ricavare leggendo tutto il codice.
- Puoi cambiare strategia di allocazione senza toccare la funzione. Le passi un allocatore a blocco unico per una richiesta web, uno che libera tutto insieme alla fine, uno che tiene traccia delle perdite nei test.
- I test trovano le perdite di memoria da soli. L'allocatore usato nei test segnala la memoria non liberata alla fine del test.
Nota anche defer: la liberazione è scritta subito accanto all'allocazione, e viene eseguita all'uscita dal blocco. È la risposta di Zig al problema classico del C, dove malloc e free stanno a cinquanta righe di distanza e prima o poi uno dei due si perde.
Gli errori come valori, ma senza verbosità
Una funzione che può fallire lo dichiara nel tipo di ritorno con un !:
fn dividi(a: i32, b: i32) !i32 {
if (b == 0) return error.DivisionePerZero;
return @divTrunc(a, b);
}
pub fn main() !void {
const risultato = try dividi(10, 2);
_ = risultato;
}
try significa "se c'è un errore, restituiscilo al chiamante". Il compilatore non ti permette di ignorare un errore in silenzio: o lo gestisci, o lo propaghi esplicitamente. È lo stesso principio del Result di Rust, con una sintassi più leggera, ed evita il difetto più noto di Go, dove un errore non controllato passa inosservato.
C'è anche errdefer, che esegue il codice di pulizia solo se la funzione esce con un errore. Nella gestione delle risorse è una comodità che si apprezza la prima volta che devi annullare a metà una sequenza di operazioni.
comptime: codice che gira durante la compilazione
Invece di macro e template, Zig ti permette di eseguire codice Zig normale durante la compilazione.
fn massimo(comptime T: type, a: T, b: T) T {
return if (a > b) a else b;
}
const x = massimo(i32, 3, 7);
const y = massimo(f64, 2.5, 1.5);
Qui il tipo è un parametro come un altro, noto a compilazione. Con lo stesso meccanismo ottieni i generici, le tabelle precalcolate, la validazione di configurazioni prima che il programma parta.
La differenza rispetto alle macro del C è sostanziale: una macro manipola testo senza capire il linguaggio, e i suoi errori sono tra i più incomprensibili che esistano. comptime è codice Zig vero, con i tipi controllati e messaggi d'errore normali. Rispetto ai template del C++ ottieni buona parte della potenza con un linguaggio solo invece di due.
Il compilatore C incluso: il motivo per cui lo usano anche senza scriverci
Questa è la funzione che molti scoprono per caso. Zig include un compilatore C e C++ completo, e lo rende particolarmente comodo per la compilazione incrociata.
zig cc -o programma main.c -target aarch64-linux-gnu
Con un solo comando compili un programma C per un'altra architettura e un altro sistema operativo, senza installare toolchain separate. Chi ha configurato a mano una cross-compilazione in C sa quanto possa essere doloroso.
Per questo alcuni progetti usano Zig solo come sistema di build per codice C esistente, senza scrivere una riga nel linguaggio. E l'interoperabilità va nell'altra direzione: da Zig puoi importare direttamente un header C e chiamarne le funzioni, senza scrivere collegamenti a mano. È il motivo per cui la migrazione da C può essere graduale, file per file.
Zig e Rust: obiettivi diversi
Il confronto viene fatto di continuo, ma parte da un equivoco.
| Zig | Rust | |
|---|---|---|
| Obiettivo | Un C migliore | Sicurezza della memoria garantita |
| Sicurezza della memoria | Controlli a runtime nelle build di debug | Verificata dal compilatore |
| Complessità del linguaggio | Piccolo, si legge in pochi giorni | Ampio, mesi per padroneggiarlo |
| Interoperabilità col C | Diretta, nativa | Buona, tramite strumenti dedicati |
| Maturità | Prima della 1.0, cambia spesso | Stabile, retrocompatibile |
| Ecosistema | Piccolo | Ampio e maturo |
Il punto che conta: Rust ti garantisce a compilazione che certi errori di memoria non possano esistere. Zig non lo fa. Ti aiuta con controlli attivi durante lo sviluppo — accessi fuori dai limiti, overflow degli interi, uso di valori nulli — che nelle build di produzione ottimizzate puoi scegliere di disattivare per guadagnare velocità. Un use-after-free resta possibile.
È una scelta di progetto, non una svista: Zig sceglie semplicità e controllo totale, Rust sceglie garanzie. Se ti serve la certezza che un'intera classe di vulnerabilità non esista nel tuo codice, Zig non è la risposta. Se vuoi scrivere codice di sistema leggibile, con meno trappole del C e senza la curva di apprendimento di Rust, lo è.
Dove si usa oggi
Nonostante non sia ancora alla 1.0, ci sono progetti noti in produzione:
- Bun, il runtime JavaScript alternativo a Node.js, è scritto in buona parte in Zig — il confronto tra i runtime è in Node, Deno o Bun.
- TigerBeetle, un database specializzato per transazioni finanziarie.
- Ghostty, un emulatore di terminale.
Il filo comune è chiaro: software dove le prestazioni contano, scritto da team piccoli e molto tecnici disposti ad accettare un linguaggio che cambia ancora.
I limiti, detti onestamente
Il linguaggio non è stabile. È il limite principale e non va minimizzato. A ogni versione possono cambiare sintassi e libreria standard, e aggiornare il compilatore significa spesso correggere codice. Per un progetto aziendale che deve durare anni senza manutenzione continua, oggi è un rischio concreto.
La documentazione è incompleta. La libreria standard è documentata in modo parziale, e spesso il modo più rapido di capire una funzione è leggerne il sorgente.
L'ecosistema è piccolo. Pochi pacchetti rispetto a C, Rust o Go. Il vantaggio è che puoi usare direttamente le librerie C, e questo compensa in parte.
Il mercato del lavoro è quasi inesistente, soprattutto in Italia. Le posizioni che richiedono Zig sono rarissime; lo imparerai per interesse o per un progetto specifico, non per trovare lavoro.
Errori comuni di chi inizia
Aspettarsi la sicurezza di Rust. Zig ti avvisa di molti errori mentre sviluppi, ma non ti impedisce di liberare memoria due volte o di usarla dopo averla liberata. Testa in modalità di debug e non disattivare i controlli finché non sai perché lo fai.
Seguire tutorial vecchi. Visto che il linguaggio cambia, un esempio scritto per una versione precedente può non compilare. Controlla sempre per quale versione è stato scritto e allinealo a quella che hai installato.
Dimenticare defer subito dopo l'allocazione. L'abitudine da prendere è scrivere la liberazione nella riga successiva all'allocazione, sempre, prima di scrivere il resto.
Combattere gli allocatori espliciti. La tentazione è creare un allocatore globale e usarlo ovunque, come in C. Funziona, ma butti via il vantaggio principale del linguaggio. Passa l'allocatore come parametro, anche quando sembra ridondante.
Ignorare i valori di ritorno. Zig non ti lascia scartare un valore in silenzio: devi assegnarlo esplicitamente a _. Non è pedanteria, è il modo in cui il linguaggio ti impedisce di perdere un errore per distrazione.
In sintesi
Zig è un tentativo serio di sostituire il C, non di competere con Rust: tiene il controllo diretto sulla memoria e toglie le trappole peggiori, dalle macro al flusso di controllo nascosto.
Le sue idee distintive sono tre: gli allocatori passati esplicitamente, che rendono visibile dove e come si usa la memoria; gli errori come valori che non si possono ignorare; e comptime, che sostituisce macro e template con normale codice Zig eseguito durante la compilazione.
Anche senza scriverci, può esserti utile: il compilatore C incluso rende la compilazione incrociata molto più semplice di qualsiasi toolchain tradizionale.
Il limite che conta è la stabilità. Finché non arriva alla 1.0 è un linguaggio da esplorare, da usare su progetti personali o in team disposti a seguirne l'evoluzione, non la scelta prudente per un prodotto aziendale di lunga durata.
Se ti interessa il livello più basso della programmazione, il punto di partenza resta il C — il perché è in perché imparare il C — e un passo ancora più giù c'è l'Assembly. La mappa di tutti gli altri linguaggi è in tutti i linguaggi di programmazione.
Contenuto redatto con l'assistenza di strumenti di intelligenza artificiale e rivisto dalla redazione di Codegrind, che ne è responsabile. Hai trovato un errore? Scrivici.