Contexte et objectifs

Le comparatif se base sur du code compilé sur macOS avec Zig 0.17 et clang. L’auteur souligne que C reste le langage de base des noyaux Unix, des bibliothèques système et de la plupart des API C. Zig ne cherche pas à remplacer ces API ; il les invoque via l’ABI C. Le point de départ est la volonté de réduire le temps passé à écrire du code C tout en conservant les mêmes contraintes d’absence de ramasse‑miettes.

Gestion des ressources et errdefer

En C, la libération de ressources après une allocation partielle repose souvent sur des goto imbriqués. L’exemple montre quatre points de sortie :

struct user *user_new(const char *name, size_t n_groups) {
    struct user *u = malloc(sizeof *u);
    if (u == NULL) goto fail;
    u->name = strdup(name);
    if (u->name == NULL) goto fail_user;
    u->groups = calloc(n_groups, sizeof *u->groups);
    if (u->groups == NULL) goto fail_name;
    return u;
fail_name:
    free(u->name);
fail_user:
    free(u);
fail:
    return NULL;
}
Le même flux en Zig utilise errdefer immédiatement après chaque allocation :
fn newUser(allocator: std.mem.Allocator, name: []const u8, n_groups: usize) !*User {
    const u = try allocator.create(User);
    errdefer allocator.destroy(u);
    u.name = try allocator.dupe(u8, name);
    errdefer allocator.free(u.name);
    u.groups = try allocator.alloc(u32, n_groups);
    return u;
}
Chaque errdefer s’exécute uniquement en cas d’erreur et dans l’ordre inverse, éliminant le besoin de labels. Le test automatisé avec std.testing.FailingAllocator montre que les trois chemins d’échec sont vérifiables sans outil externe.

Slices et vérifications de limites

En C, un tableau transmis à une fonction devient un pointeur ; la taille doit être passée séparément. L’erreur d’itération i <= n conduit à un dépassement silencieux :

int sum(const int *a, size_t n) {
    int total = 0;
    for (size_t i = 0; i <= n; i++)
        total += a[i];
    return total;
}
Le programme affiche 7 pour {1,2,3} sans alerte. En Zig, un slice porte sa longueur ; la même boucle déclenche un panic en mode debug :
fn sum(a: []const i32) i32 {
    var total: i32 = 0;
    for (0..a.len + 1) |i| total += a[i];
    return total;
}
Le message « index out of bounds » indique précisément le défaut, et l’usage habituel for (a) |x| empêche l’erreur.

Compilation, comptime et comportement d’exécution

Le préprocesseur C travaille sur du texte et introduit des macros à double évaluation ainsi que de nombreux #ifdef. Zig remplace ces constructions par du code réel exécuté à la compilation : if (builtin.os.tag == .linux) élimine le branchement non applicable. Les structures génériques sont des fonctions qui retournent des types, assurant une vérification complète. Concernant le débordement d’entier, le programme C suivant produit un résultat indéfini :

int add(int a, int b) { return a + b; }
int main(void) { printf("%d\n", add(INT_MAX, 1)); }
Clang peut rendre -2147483648. En Zig, le même opération déclenche un panic « integer overflow » en mode sûr, tandis que les opérateurs +% (wrapping) et +| (saturation) offrent des comportements explicites. Enfin, zig cc agit comme un compilateur C autonome, capable de cross‑compiler sans dépendances externes et d’activer automatiquement l’UB‑sanitizer en mode debug.