Pertinence du développement système avec Hare
Prérequis
- Langage C
- Syntaxe Hare non nécessaire
Introduction
Si vous avez déjà utilisé un langage système comme le C, le C++ ou le Rust, vous savez qu’un grand pouvoir implique de grandes responsabilités. Les langages systèmes offrent une capacité d’intéraction direct avec le matériel et des gains de performances. Cependant, le développement bas-niveau manque encore de facilité et de sureté. C’est en partant de ces constats que Drew Devault s’est lancé dans le développement du Hare. Comment l’Hare permet d’allier bas-niveau et aisance de développement ? Dans cet article, je détaille uniquement le positionnement du Hare par rapport au C en exposant ses mécanismes de sureté et de facilité d’utilisation.
Fiabilité
Le langage C offre énormément de liberté mais il est facile de faire des erreurs. Hare apporte des garantis sur certains undefined behaviours
observables en C.
L’assignation des variables
Comme en C, Hare ne permet pas d’utiliser une variable tant qu’elle n’est pas assignée. L’Hare oblige l’assignation de l’ensemble des variables déclarées. Cependant, ici, le code échoue à la compilation. En C, il faudra attendre non seulement l’exécution, mais aussi l’utilisation de cette variable pendant l’exécution et la lecture.
En Hare :
export fn main() void = {
let x: int; //expected '='
};
Sortie : unexpected ';' at /home/user/main.ha:2:15, expected '='
Équivalent en C :
#include <stdio.h>
int main()
{
int *x;
*x = *x; //Segfault
}
Néanmoins si vous avez l’habitude d’utiliser des arbres, des listes chaînées … il peut être nécessaire pour d’utiliser les NULL
. C’est pourquoi, pour éviter les segfaults
, les valeurs ne peuvent être NULL
par défaut.
export fn main() void = {
let x = "toto";
let y: *str = &x; //Garantie non-null
let z: nullable *str = y; //Possiblement null
*y;
*z;
match (z) {
case null =>
abort();
case ...
};
};
Le code ci-dessus échoue a la ligne 7
. Il est nécessaire, avant d’utiliser z
, de s’assurer qu’il peut l’être. C’est ce que je fais, ligne 9.
Moins d'undefined behaviour
Au-delà d’obligations, pour éviter les undefined behaviour
, l’Hare défini lui-même certains comportements que le C ne définit pas.
Sous couvert d’optimisation les compilateurs C ont une fâcheuse tendance à faire ce qu’ils veulent. Cela se place en opposition totale avec la philosophie du Hare. Le Hare se veut constant et choisit donc toujours les mêmes instructions assembleur, pour les mêmes comportements.
Un cas d’exemple de ces comportements non prédictibles est celui des octets en C. Effectivement, en C, un octet ne vaut pas toujours 8 bits. Il est nécessaire d’utiliser un uint8_t
pour garantir sa taille. En Hare, un octet vaudra toujours 8 bits.
En C, un shift supérieur à la largeur de votre valeur entraine une perte de bits, en Hare, ce problème n’existe pas.
Également, l’overflow et l’underflow sont définis. Dans le cadre de l’overflow d’un nombre :
- Entier, celui-ci est tronqué vers les bits les moins significatifs (LSB)
- Flottant, celui-ci est tronqué vers zéro.
- Signé, la troncature entraîne le changement du bit de signe.
Aisance de développement
Contrairement au C, l’Hare apporte un grand nombre d’abstractions pour faciliter le développement. Celles-ci permettent majoritairement de travailler des groupes d’éléments afin de repeter l’écriture de boucle.
L’opérateur ...
L’opérateur ...
permet d’initialiser un ensemble de valeurs d’une variable.
Dans le cas ci-dessous, y
est défini à la valeur par défaut d’un int
, soit 0. Nul besoin prendre le temps de définir y
tant que l’on ne s’en sert pas.
use fmt;
type coords = struct { x: int, y: int };
export fn main() void = {
let A = coords { x = 1337, ... };
fmt::printfln("({}, {})", A.x, A.y)!;
};
Sortie : (1337, 0)
Dans ce nouvel exemple, l’opérateur ...
nous permet cette fois-ci d’assigner l’ensemble de cases de notre tableau, à la valeur 3
.
use fmt;
export fn main() void = {
let x: [4096] int = [3...];
fmt::printfln("{}", x[6])!;
};
Sortie : 3
Les slices
Si vous voulez changer une portion d’un tableau en C, vous êtes contraint d’écrire une boucle. À l’instar de Python ou de Golang, Hare propose des slices pour éviter cette écriture longue et fastidieuse.
Ce code modifie le tableau y
initialiser à 0. Entre les indices 6
(compris) et 9
(non compris).
use fmt;
export fn main() void = {
let x: [4096] int = [3...];
x[7] = 9;
let y: [16] int = [0...];
y[6..9] = x[6..9];
fmt::printfln("{}, {}, {}, {}", y[6], y[7], y[8], y[9])!;
};
Sortie : 3, 9, 3, 0
Les tableaux sans limites
Le C n’offre pas de mécanisme simple de tableau sans longueur prédéfini. Vous êtes contraint de créer vos propres vecteurs pour reproduire ce comportement.
Hare offre cette possibilité avec l’opérateur [*]
. Dans l’exemple, en observe que j’accède à l’indice 43
de mon tableau dont je n’ai pas défini la longueur.
use fmt;
export fn main() void = {
let x = [1, 2, 3];
let y = &x: *[*]int;
y[42] = 1337;
y[43];
};
Avertissement : cette pratique n’est pas sûre si l’indice est excessivement grand. Cela peut provoqué un segfault
.
Mais elle reste pratique pour de petits tests non utilisés en production.
Les formats de string
Le formatage de caractère en C est assez rébarbatif. Vous devez à chaque fois définir le format de votre chaine de caractère et surtout vous rappeler des caractères correspondant à la variable que vous voulez afficher (%s
, %d
, %lu
…).
Pour éviter cela, Hare intègre au type passé à la fonction de formatage de str
, la manière de traiter la variable a utilisé.
use fmt;
export fn main() void = {
let x = 12;
let c = 'Z';
let txt = "Hello";
let y = 3.14;
fmt::printfln("{} | {} | {} | {}", x, c, txt, y)!;
};
Typage multiple de fonction
La gestion d’erreur en C est assez complexe et le langage manque de constance. En effet, des fois il faut lire errno
, d’autres fois évaluer le retour de notre fonction.
Pour faciliter la gestion d’erreur Hare permet aux fonctions de retourner plusieurs types. Dans l’exemple, la fonction fun()
peut retourner soit un int
, soit un str
.
use fmt;
fn fun(boo: bool) (int | str) = {
let ret = if (boo) {
yield "toto";
} else {
yield 42;
};
return ret;
};
export fn main() void = {
let r1 = fun(true);
fmt::printfln("{}", r1)!;
let r2 = fun(false);
fmt::printfln("{}", r2)!;
};
Indication : Le mot clé yield
indique le retour du scope
Les apports des string
Hare cherche à optimiser les performances des strings avec 3 points :
- Pour faciliter l’usage du texte sur différentes machines, Hare a choisi pour ces chaines de caractères le format
UTF-8
et pour ces caractères le formatUCS-32
. A l’inverse du C qui utilise l’ASCII. - Dans un souci d’optimisation, chaque
str
stock sa longueur. - Le charactère
null
est autorisé à l’intérieur d’unstr
.
Conclusion
Hare est un langage système cherchant à apporter, vis-à-vis du C, de la fiabilité et une facilité d’écriture tout en gardant la même capacité de contrôle. Hare n’as pas pour but d’apporter de nouveaux paradigmes de programmation mais cherche à concilier bas-niveau, sureté et aisance de développement. On pourrait croire qu’avec ces contraintes, Hare, s’oppose au principe de liberté d’un langage système. Cependant si vous allez plus loin, vous vous apercevrez vite que les sécurités du langage peuvent être outrepassées pour garder cette liberté. Néanmoins, et comme souvent évoqué dans la documentation, attention à ne pas vous tirer une balle dans le pied.