Perchè Zig quando ci sono già C++, D e Rust?
Nessun intervento nascosto sul flusso di esecuzione
Se vedi del codice Zig e non sembra che stia chiamando una funzione, allora non lo sta facendo. Questo significa che il seguente codice chiamerà sicuramente solo foo()
e poi bar()
, e questo è garantito anche senza conoscere i tipi di dato utilizzati:
var a = b + c.d;
foo();
bar();
Esempi di controllo nascosto del flusso di esecuzione:
- D ha le funzioni
@property
, ovvero metodi che possono essere chiamati in un modo che sembra l'accesso a un attributo. Quindi, nell'esempio sopra,c.d
potrebbe chiamare una funzione. - C++, D, e Rust supportano l'overload degli operatori, quindi l'operatore
+
potrebbe chiamare una funzione. - C++, D, e Go hanno eccezioni throw/catch, quindi
foo()
potrebbe lanciare un'eccezione, e impedire l'esecuzione dibar()
. (Ovviamente, persino in Zigfoo()
potrebbe andare in stallo e impedire l'esecuzione dibar()
, ma questo può avvenire in qualunque linguaggio Turing-completo.)
Lo scopo di questa scelta progettuale è migliorare la leggibilità.
Nessuna allocazione dinamica nascosta
Zig ha un approccio schivo riguardo le allocazioni dinamiche. Non c'è alcuna parola chiave new
o parte del linguaggio che fa uso di allocazioni dinamiche (per es. l'operatore di concatenazione di stringhe[1]). L'intero concetto di heap è gestito dal codice di librerie e applicazioni, non dal linguaggio.
Esempi di allocazioni dinamiche nascoste:
- Il
defer
in Go alloca in uno stack locale alla funzione. Oltre che essere una gestione poco intuitivo di questo flusso di esecuzione, può anche causare errori di riempimento della memoria se usidefer
dentro un ciclo. - Le coroutine in C++ allocano memoria nello heap per chiamare una coroutine.
- In Go, una chiamata a funzione può causare un'allocazione nello heap perchè le goroutine allocano piccoli stack che vengono ridimensionati quando lo stack di chiamate supera una certa soglia
- Le API della principale libreria standard di Rust risultano in un kernel panic quando la memoria è piena, e le API alternative che accettano come parametro un allocatore sono state una correzione fatta a posteriori (vedi rust-lang/rust#29802).
Quasi tutti i linguaggi garbage-collected hanno allocazioni dinamiche nascoste qua e là, dato che il garbage collector nasconde le prove con la fase di liberazione della memoria.
Il problema principale delle allocazioni dinamiche nascoste è che impediscono la riusabilità di un insieme di codice, limitando senza alcuna ragione il numero di ambienti appropriati che possono fare uso di quel codice. In sostanza, ci sono casi d'uso in cui è indispensabile poter essere sicuri che il flusso di esecuzione e le chiamate di funzioni non abbiano gli effetti collaterali delle allocazioni dinamiche, quindi un linguaggio di programmazione può essere adatto a questi casi d'uso solo se può dare questa garanzia.
In Zig, alcune parti della libreria standard forniscono e interagiscono con gli allocatori dinamici, ma sono Caratteristiche opzionali della libreria standard, non sono parte del linguaggio. Se non inizializzi alcun allocatore dinamico, hai la certezza che il tuo programma non allocherà nello heap.
Ogni funzione della libreria standard che richiede allocazioni dinamiche accetta un parametro di tipo Allocator
per poterlo fare. Questo significa che Zig supporta le piattaforme puramente hardware. Per esempio puoi usare std.ArrayList
e std.AutoHashMap
in sistemi embedded!
Gli allocatori personalizzati rendono la gestione della memoria una passeggiata. Zig ha un allocatore di debug che gestisce la sicurezza della memoria in caso di use-after-free e double-free. Automaticamente, rileva i memory leak e ne fornisce lo stack trace. C'è un allocatore ad area che permette di combinare un qualunque numero di allocazioni in una sola, per poi liberare l'area di memoria tutta insieme, invece di gestire singolarmente ogni allocazione. Allocatori a uso specifico possono essere utilizzati per migliorare le prestazioni o l'utilizzo di memoria, per soddisfare i requisiti di qualunque applicazione.
[1]: In effetti c'è un operatore di concatenazione di stringhe (o meglio, concatenazione di array), ma funziona solo in fase di compilazione, quindi in ogni caso non esegue alcuna allocazione dinamica a runtime.
Nessun trattamento speciale per le librerie standard
Come suggerito nei paragrafi precedenti, Zig ha una libreria standard completamente opzionale. Ogni API della libreria standard viene compilata nel tuo programma solo se la usi. Che si decida o meno di linkare a libc
, Zig offre lo stesso tipo di supporto. Zig aiuta lo sviluppo per sistemi hardware/embedded e applicazioni ad alte prestazioni.
Così si prendono due piccioni con una fava; per esempio in Zig, i programmi WebAssembly possono usare le funzioni comuni della libreria standard, e comunque risultare in binari minuscoli in confronto ad altri linguaggi che supportano la compilazione per WebAssembly.
Un linguaggio portabile per le librerie
Una dei pilastri della programmazione è il riuso del codice. Purtroppo, nella pratica, si finisce col dover reinventare la ruota più e più volte. Spesso questo sforzo è giustificato.
- Se un'applicazione ha requisiti real-time, allora ogni libreria che include garbage-collection o qualunque altro comportamento non deterministico non è accettabile come dipendenza.
- Se un linguaggio rende troppo semplice ignorare gli errori, e dunque rende difficile verificare che una libreria gestisca e comunichi correttamente gli errori, è naturale farsi tentare dall'idea di ignorare la libreria e re-implementarla, per avere la certezza di aver gestito correttamente tutti gli errori rilevanti. Zig è progettato in modo che la cose più pigra che un programmatore possa fare sia gestire gli errori correttamente, in modo che gli utenti della libreria possano essere ragionevolmente sicuri che gli errori siano comunicati (bubbling) correttamente.
- Al giorno d'oggi è pragmaticamente corretto affermare che C è il linguaggio più versatile e portabile. Ogni linguaggio che non permette di interfacciarsi a C rischia il decadimento. Zig sta tentando di diventare il nuovo linguaggio portabile per le librerie, rendendo semplice conformarsi all'ABI di C per le funzioni esterne, e allo stesso tempo introducendo scelte di progettazione e misure di sicurezza che prevengano comuni bug nella stessa implementazione.
Un gestore di pacchetti e sistema di build per progetti esistenti
Zig è una toolchain, un insieme di strumenti, oltre a essere un linguaggio di programmazione. Include un sistema di build e gestione di pacchetti che sono utili persino nel contesto di un progetto C/C++ tradizionale.
Non solo puoi scrivere codice Zig invece di C o C++, ma puoi usare Zig come sostituto di autotools, cmake, make, scons, ninja, eccetera. In più, hai a disposizione un gestore di pacchetti per le dipendenze native. Questo build system è adatto persino a progetti scritti interamente in C or C++. Per esempio, sostituendo il build system di ffmpeg con quello di Zig, diventa possibile compilare ffmpeg su ogni piattaforma supportata per ogni piattaforma supportata, usando solo un download di Zig da 50 Mib. Per i progetti open source, questa semplificazione della compilazione da codice sorgente - e cross-compilazione - può fare la differenza tra l'attirare o il perdere preziosi contributori.
I gestori di pacchetti di sistema come apt-get, pacman, homebrew e altri sono fondamentali per l'user-experience degli utenti finali, ma possono essere insufficienti per le necessità degli sviluppatori. Un gestore di pacchetti specifico per un linguaggio può fare la differenza tra il non avere contributori e averne molti. Per i progetti open source, la difficoltà di riuscire anche solo a compilare il progetto è un grande ostacolo per i potenziali contributori. Per i progetti C/C++, avere dipendenze può essere fatale, specialmente su Windows, che non ha un suo gestore di pacchetti. Persino con la compilazione dello stesso Zig da sorgente, la maggior parte dei nostri potenziali contributori hanno difficoltà con la dipendenza da LLVM. Zig offre ai progetti un modo per dipendere direttamente dalle librerie native, ma senza che gli utenti debbano per forza avere la versione corretta disponibile nel loro gestore di pacchetti, e in un modo che compila praticamente sempre al primo tentativo a prescindere dalla piattaforma su cui si sta compilando, o per cui si sta compilando.
Altri linguaggi includono un gestore di pacchetti, ma non eliminano la necessità delle dipendenze di sistema come fa Zig.
Zig può sostituire il build system di un progetto con un linguaggio ragionevole che fornisce un'API dichiarativa per compilare i progetti, e che si occupa anche della gestione dei pacchetti, rendendo quindi possibile dipendere davvero da altre librerie C La possibilità di avere dipendenze apre la strada a livelli di astrazione più alti, e quindi alla proliferazione di codice ad alto livello riutilizzabile.
Semplicità
C++, Rust e D hanno così tante funzioni disponibili che possono finire col distrarre dall'effettivo significato del codice che si sta scrivendo. Così ci si ritrova a debuggare la propria conoscenza del linguaggio di programmazione, invece di debuggare il programma.
Zig non ha macro, eppure è abbastanza potente da esprimere programmi complessi in modo chiaro e senza ripetizioni. Persino Rust ha delle macro "speciali" come format!
, che è implementata direttamente nel compilatore. Invece in Zig la funzione equivalente è implementata nella libreria standard, senza il bisogno di aggiungere controlli specifici nel compilatore.
Strumenti
Zig può essere scaricato dalla sezione download. Sono disponibili archivi compressi per Linux, Windows e macOS. La seguente lista indica cosa ottieni con uno di questi archivi:
- installazione scaricando ed estraendo un archivio, nessuna configurazione di sistema richiesta
- compilato staticamente quindi non c'è nessuna dipendenza a runtime
- supporto a LLVM per build ottimizzate, e backend integrati in Zig per ridurre i tempi di compilazione
- un backend per produrre codice C invece di un file binario
- cross-compilazione subito disponibile per la maggior parte delle piattaforme più comuni
- codice sorgente di
libc
che verrà compilato dinamicamente quando necessario per qualunque piattaforma - un build system con processi concorrenti e caching
- compilazione di codice C e C++ con supporto a
libc
- compatibilità con le opzioni da riga di comando di GCC/Clang usando
zig cc
- compilatore di risorse Windows