L'attrait du langage Ada ── Exprimer la conception par les types, faire fonctionner des logiciels pendant des décennies

· Mis à jour le: · · Ada, Langage de Programmation, Typage Fort, SPARK, GNAT, Alire, Haute Intégrité, Embarqué, Haute Fiabilité

1. Ce qu’il faut comprendre en premier

Avez-vous déjà entendu parler d’un langage appelé Ada ?

Beaucoup de gens en ont l’image d’un « vieux langage », d’un « langage militaire », ou d’un nom simplement entendu en cours.

Pourtant, Ada est encore aujourd’hui un langage bien vivant.

Dans le monde des logiciels dont l’arrêt met des vies humaines en jeu — contrôle de vol des avions, systèmes de signalisation ferroviaire, fusées, contrôle du trafic aérien, satellites, dispositifs médicaux — Ada est utilisé sans interruption depuis des décennies.

Pour comprendre Ada, il est important de garder à l’esprit les points de vue suivants.

Ada est un langage qui consacre toute son énergie à « éliminer les bogues avant l'exécution »
Les types ne sont pas des conteneurs de données, mais des outils pour exprimer l'intention de conception
La séparation de la spécification et de l'implémentation, les contrats et le traitement concurrent sont intégrés au langage
Ada n'est jamais devenu à la mode, mais sa philosophie de conception a été héritée par les langages modernes

Cet article passe en revue l’attrait d’Ada : son histoire, sa syntaxe, son typage fort, ses contraintes de plage, ses packages, sa conception par contrat, ses tâches, SPARK, son environnement de développement, et jusqu’à ses faiblesses.

L’objectif est que les personnes qui écrivent habituellement du C#, du C++ ou du Java repartent avec le sentiment de ce que signifie « exprimer la conception par les types ».

Les extraits de code présentés dans cet article sont publiés sur GitHub, organisés par chapitre dans un dépôt de référence.

ada-language-appeal - komurasoft-blog-samples (GitHub)

2. Qu’est-ce qu’Ada ── L’origine du nom et son histoire

Ada est un langage de programmation généraliste né à la fin des années 1970, sous l’impulsion du département de la Défense des États-Unis (DoD).

À l’époque, le DoD était confronté à un problème : chaque projet utilisait un langage différent, et les coûts de maintenance des logiciels explosaient.

Le DoD a donc organisé un concours de conception international pour sélectionner un langage standard, également utilisable pour les systèmes embarqués et temps réel.

C’est le projet conçu par l’équipe dirigée par Jean Ichbiah qui a été retenu.

Le nom du langage, Ada, vient d’Ada Lovelace (Augusta Ada King, comtesse de Lovelace), souvent considérée comme la première programmeuse au monde.

Voici, en résumé, l’histoire d’Ada.

1980 Première spécification établie sous le nom MIL-STD-1815
1983 Ada 83 (standard ANSI)
1987 Devient un standard ISO
1995 Ada 95 (introduction de l'orientation objet et des objets protégés)
2005 Ada 2005 (interfaces, bibliothèque de conteneurs étendue)
2012 Ada 2012 (la conception par contrat introduite comme fonctionnalité du langage)
2022 Ada 2022 (le standard le plus récent)

Ada 95 fait partie des tout premiers langages orientés objet à avoir été normalisés par l’ISO.

Et avec Ada 2012, la conception par contrat (Design by Contract) — préconditions, postconditions et invariants de type — a été intégrée à la spécification même du langage.

Ada n’est pas un « vieux langage » : c’est un langage révisé en continu depuis plus de 40 ans.

3. Où Ada est-il utilisé ?

Le domaine emblématique où Ada continue d’être utilisé est celui des systèmes exigeant une haute intégrité (High Integrity).

Contrôle de vol et avionique des avions civils
Systèmes de contrôle du trafic aérien
Systèmes de signalisation et de sécurité ferroviaires
Fusées et satellites
Systèmes de défense
Dispositifs médicaux
Certains systèmes centraux de la finance et de l'industrie

Ces domaines partagent des caractéristiques communes.

Un bogue se traduit directement par une perte de vies humaines ou des pertes financières considérables
La certification et les audits exigent des preuves de correction
Une fois déployé, le système fonctionne pendant des décennies
Le coût d'une correction ultérieure est extrêmement élevé

C’est un monde où l’idée de « livrer maintenant et corriger plus tard » n’a tout simplement pas cours.

La conception du langage Ada existe précisément pour répondre à ces exigences.

Les bogues détectables à la compilation sont détectés à la compilation ; ceux qui ne peuvent être détectés qu’à l’exécution le sont par des vérifications à l’exécution ; et, allant plus loin, ce qui peut être prouvé mathématiquement est éliminé par la preuve — cette philosophie traverse l’ensemble du langage.

Cette philosophie mérite d’être apprise, même par les développeurs qui écrivent des applications métier web ou de bureau.

4. Pour commencer, Hello, World

Regardons du code Ada.

with Ada.Text_IO;

procedure Hello is
begin
   Ada.Text_IO.Put_Line ("Hello, Ada!");
end Hello;

Voici probablement les premières choses que vous remarquerez.

with importe une unité de bibliothèque
Le corps du programme est une procédure (procedure)
begin / end délimitent un bloc
Le nom est répété après end
Les instructions se terminent par un point-virgule

Réécrire le nom à la fin, comme dans end Hello;, est une caractéristique typiquement Ada.

Même lorsque les blocs sont profondément imbriqués, on voit d’un coup d’œil « de quel bloc est-ce le end ».

Le compilateur vérifie également la correspondance des noms, si bien qu’une mauvaise fermeture de bloc devient une erreur de compilation.

Cela paraît un détail, mais cela illustre bien la philosophie d’Ada : le langage soutient l’humain précisément là où les erreurs de lecture sont les plus probables.

5. Une syntaxe pensée pour la lisibilité

La syntaxe d’Ada a été conçue en donnant la priorité à la lisibilité plutôt qu’à la facilité d’écriture.

Elle repose sur le principe qu’un logiciel est lu bien plus souvent qu’il n’est écrit.

Par exemple, les boucles et les branchements conditionnels s’écrivent ainsi.

for I in 1 .. 5 loop
   Ada.Text_IO.Put_Line (Integer'Image (I * I));
end loop;

if Temperature > 80.0 then
   Start_Cooling;
elsif Temperature < 20.0 then
   Start_Heating;
else
   Keep_Current_State;
end if;

L’instruction case possède une caractéristique bien Ada.

case Today is
   when Mon .. Fri =>
      Put_Line ("Weekday");
   when Sat | Sun =>
      Put_Line ("Weekend");
end case;

Voici les points clés.

Un case doit couvrir toutes les valeurs, sinon c'est une erreur de compilation
Il n'existe pas de fallthrough implicite comme dans les langages de la famille C
Les conditions peuvent être regroupées par plage (Mon .. Fri) ou par liste de choix (Sat | Sun)

Ajoutez une valeur à un type énuméré, et tout case non exhaustif devient une erreur de compilation.

L’expérience où « le compilateur énumère lui-même tous les endroits affectés par un changement de spécification » est de celles qu’on ne peut plus abandonner une fois qu’on l’a goûtée.

Par ailleurs, les arguments peuvent utiliser une association nommée.

Draw_Rectangle (Left => 10, Top => 20, Width => 100, Height => 50);

Cela évite les confusions d’arguments, et le code appelant devient lui-même sa propre documentation.

L’affectation s’écrit := et la comparaison =, si bien que la confusion classique des langages de la famille C avec if (a = b) ne peut tout simplement pas se produire au niveau syntaxique.

6. Typage fort ── Transformer les confusions d’unités en erreurs de compilation

Le plus grand attrait d’Ada est son typage fort.

De nombreux langages emploient l’expression « typage fort », mais celui d’Ada va un cran plus loin.

En Ada, des types déclarés sous des noms différents sont des types distincts, même si leur structure est exactement identique.

type Meters  is new Float;
type Seconds is new Float;

Distance : Meters  := 100.0;
Time     : Seconds := 9.58;

Ces deux types reposent tous deux sur un nombre à virgule flottante, mais ils ne peuvent pas être mélangés.

Distance := Time;            -- erreur de compilation
Distance := Distance + Time; -- erreur de compilation

Seule une conversion délibérée est écrite explicitement.

Speed : constant Float := Float (Distance) / Float (Time);

Pourquoi une telle rigueur ?

Un nombre non négligeable d’accidents logiciels réels ont pour origine une « confusion d’unités ».

Exemple célèbre : la sonde martienne Mars Climate Orbiter a été perdue en 1999 à cause d’un mélange entre le système impérial et le système métrique.

La réponse d’Ada est simple.

Faire des mètres et des pieds des types différents
Faire du mélange des deux une erreur de compilation
Forcer l'écriture explicite des conversions

Non pas « faire attention », « le repérer en relecture » ou « le capturer par des tests », mais empêcher purement et simplement la compilation de réussir.

C’est la posture fondamentale d’Ada.

7. Contraintes de plage ── Empêcher les valeurs invalides au niveau du type de données

En Ada, un type peut porter une plage de valeurs.

subtype Percentage is Integer range 0 .. 100;

Progress : Percentage := 50;

Tenter d’affecter une valeur hors plage à une variable de type Percentage déclenche à l’exécution une exception Constraint_Error.

Progress := 120;  -- Constraint_Error à l'exécution

Les violations détectables à la compilation le sont dès la compilation.

Les hypothèses implicites — « cette valeur devrait être comprise entre 0 et 100 », « cette valeur devrait être au moins 1 » — peuvent être exprimées dans le type, et non dans un commentaire.

D’ailleurs, la bibliothèque standard d’Ada prédéfinit déjà des types contraints d’usage courant.

Natural  = Integer range 0 .. Integer'Last
Positive = Integer range 1 .. Integer'Last

De plus, depuis Ada 2012, on peut attacher des conditions arbitraires sous forme de prédicats.

subtype Even is Integer
  with Dynamic_Predicate => Even mod 2 = 0;

Des types orientés vers le contrôle matériel, comme les types à virgule fixe, sont eux aussi intégrés au langage.

type Temperature is delta 0.1 range -50.0 .. 150.0;

Dans la plupart des langages, la vérification des valeurs invalides prend souvent la forme suivante.

Validation par instruction if en début de fonction
Les oublis de vérification reposent sur la relecture de code
On ne sait pas clairement quelles fonctions reçoivent des valeurs déjà validées

En Ada, on peut affirmer : « dès l’instant où une valeur est de ce type, la plage est déjà garantie ».

En transférant la responsabilité de la validation au type de données, la logique des fonctions peut se concentrer sur son véritable rôle.

8. Tableaux et indices ── Vérification des bornes et indices de type énuméré

Les tableaux Ada permettent de choisir librement le type de l’indice.

type Day is (Mon, Tue, Wed, Thu, Fri, Sat, Sun);

type Hours_Array is array (Day) of Natural;

Work_Hours : Hours_Array := (Mon .. Fri => 8, others => 0);

Voici un tableau indexé par le type énuméré Day.

On y accède par Work_Hours (Wed), sans avoir besoin de se souvenir de « ce que signifie cet indice numérique ».

Les boucles peuvent également suivre le type de l’indice.

for D in Work_Hours'Range loop
   Put_Line (Day'Image (D) & ":" & Natural'Image (Work_Hours (D)));
end loop;

Des attributs comme 'Range, 'First, 'Last et 'Length donnent à tout moment les informations sur les bornes du tableau.

Comme les bornes ne sont jamais codées en dur, redimensionner un tableau ne se répercute pas sur les boucles.

Et surtout, l’accès aux tableaux fait toujours l’objet d’une vérification des bornes.

Buffer : String (1 .. 10);
Index  : Integer := 11;

Buffer (Index) := 'x';  -- Constraint_Error à l'exécution

Les débordements de tampon en C/C++ restent depuis des décennies l’une des principales causes de failles de sécurité.

En Ada, un accès hors plage n’est pas un comportement indéfini, mais une exception définie.

Plutôt que de corrompre silencieusement la mémoire et de provoquer un plantage mystérieux ailleurs, le programme s’arrête bruyamment et immédiatement, à l’endroit précis où le problème s’est produit.

Compte tenu du coût d’investigation dans les systèmes exploités sur le long terme, cette différence est considérable.

9. Packages ── Séparer la spécification de l’implémentation

Le mécanisme de modularité d’Ada est le package.

Un package se divise en deux fichiers : la spécification (spec) et le corps (body).

counters.ads  spec : l'interface exposée vers l'extérieur
counters.adb  body : les détails de l'implémentation

La spécification s’écrit ainsi.

package Counters is

   type Counter is private;

   procedure Increment (C : in out Counter);
   function  Value     (C : Counter) return Natural;

private
   type Counter is record
      Count : Natural := 0;
   end record;

end Counters;

Le corps s’écrit ainsi.

package body Counters is

   procedure Increment (C : in out Counter) is
   begin
      C.Count := C.Count + 1;
   end Increment;

   function Value (C : Counter) return Natural is
   begin
      return C.Count;
   end Value;

end Counters;

Voici les points à noter.

Déclarer un type private empêche le code appelant de toucher à sa structure interne
Lire uniquement la spec (.ads) suffit à tout comprendre de son utilisation
Modifier le body (.adb) n'implique qu'une recompilation minimale du code appelant, tant que la spec reste identique

Cela ressemble aux fichiers d’en-tête C/C++, mais au lieu d’une expansion textuelle comme #include, la cohérence est vérifiée par la spécification même du langage.

Un décalage entre la spec et le body est une erreur de compilation.

De plus, chaque paramètre doit obligatoirement déclarer un mode : in, out ou in out.

procedure Increment (C : in out Counter);

On voit dès la signature si un paramètre est en lecture seule, en écriture seule, ou en lecture-écriture.

Même sans aucune connaissance des pointeurs ou des références, on peut lire directement le sens du flux de données.

10. Enregistrements et discriminants

L’équivalent Ada d’une structure (struct) est l’enregistrement (record).

type Point is record
   X : Float := 0.0;
   Y : Float := 0.0;
end record;

P : Point := (X => 1.0, Y => 2.0);

Les champs peuvent porter des valeurs par défaut, et les agrégats permettent une initialisation nommée.

Une fonctionnalité typiquement Ada est le discriminant.

type Buffer (Size : Positive) is record
   Data   : String (1 .. Size);
   Length : Natural := 0;
end record;

Small : Buffer (Size => 16);
Large : Buffer (Size => 4096);

Un discriminant est un paramètre qui détermine la « forme » d’un enregistrement.

Buffer (16) et Buffer (4096) sont du même type, mais la taille du tableau interne est fixée à la déclaration et ne change plus ensuite.

C’est comme si le langage gérait, de manière sûre, sous forme de type, ce qui en C serait « une structure à membre de longueur variable, plus un champ de taille ».

L’incohérence entre la taille et les données réelles — une porte d’entrée classique aux bogues en C — n’existe tout simplement pas dès le départ.

11. Génériques

Ada possède des génériques (generic) depuis son tout premier standard, en 1983.

C’est bien plus tôt que les templates C++ (années 1990) ou les génériques Java (2004).

generic
   type Element is private;
procedure Swap (Left, Right : in out Element);

procedure Swap (Left, Right : in out Element) is
   Temp : constant Element := Left;
begin
   Left  := Right;
   Right := Temp;
end Swap;

Le code appelant l’instancie avec des types concrets.

procedure Swap_Integers is new Swap (Element => Integer);
procedure Swap_Floats   is new Swap (Element => Float);

La caractéristique déterminante des génériques Ada est que les opérations requises sont explicitement énoncées.

generic
   type Element is private;
   with function "<" (Left, Right : Element) return Boolean is <>;
function Max (Left, Right : Element) return Element;

La spécification indique : « cette fonction générique exige un opérateur de comparaison sur le type Element ».

Le problème qui a longtemps affligé les templates C++ — les erreurs qui n’apparaissent qu’à l’instanciation — ne se produit jamais avec Ada.

Le problème que les concepts de C++20 et les bornes de traits de Rust ont cherché à résoudre, Ada y avait déjà répondu 40 ans plus tôt.

12. Gestion des exceptions

Ada dispose d’une gestion des exceptions.

with Ada.Text_IO;
with Ada.Exceptions;

procedure Read_Config is
begin
   Load_File ("config.txt");
exception
   when Ada.Text_IO.Name_Error =>
      Ada.Text_IO.Put_Line ("Fichier de configuration introuvable");
   when E : others =>
      Ada.Text_IO.Put_Line (Ada.Exceptions.Exception_Information (E));
      raise;
end Read_Config;

On écrit une partie exception à la fin du bloc, et on y aligne les gestionnaires pour chaque type d’exception.

Voici les exceptions définies par le langage les plus représentatives.

Constraint_Error  violation de contrainte de plage, violation des bornes d'un tableau, division par zéro, etc.
Program_Error     violation d'une règle du langage (par exemple atteindre un point qui ne devrait jamais l'être)
Storage_Error     mémoire insuffisante
Tasking_Error     échec de communication entre tâches

Ce qui mérite l’attention, c’est que les violations de contraintes de plage et de vérification des bornes sont toutes intégrées à ce même mécanisme d’exceptions.

Quand une « contrainte écrite dans le type » est violée, on obtient un Constraint_Error.

Autrement dit, les contraintes de plage vues au chapitre 7 fonctionnent comme des assertions d’exécution générées automatiquement.

Il n’est pas nécessaire d’écrire soi-même du code de vérification par instructions if un peu partout.

13. Conception par contrat ── Écrire les conditions Pre/Post comme fonctionnalité du langage

La fonctionnalité phare d’Ada 2012 est le support langage de la conception par contrat (Design by Contract).

On peut écrire directement une précondition (Pre) et une postcondition (Post) sur un sous-programme.

package Stacks is

   type Stack is private;

   function Is_Full  (S : Stack) return Boolean;
   function Is_Empty (S : Stack) return Boolean;
   function Count    (S : Stack) return Natural;

   procedure Push (S : in out Stack; Item : Integer)
     with Pre  => not Is_Full (S),
          Post => Count (S) = Count (S)'Old + 1;

   procedure Pop (S : in out Stack; Item : out Integer)
     with Pre  => not Is_Empty (S),
          Post => Count (S) = Count (S)'Old - 1;

private
   -- Détails de l'implémentation
end Stacks;

Pre est « la promesse que l’appelant doit respecter », Post est « la promesse que l’implémentation garantit ».

L’attribut 'Old permet de référencer la valeur d’avant l’appel.

Ces contrats peuvent être activés comme vérifications à l’exécution via une option de compilation (-gnata sous GNAT).

En cas de violation d’un contrat, une exception Assertion_Error est levée, et l’on sait clairement quel côté a rompu la promesse.

Violation de Pre  -> bogue du côté de l'appelant
Violation de Post -> bogue du côté de l'implémentation

Quelle est la différence avec le fait d’écrire, dans un commentaire de documentation, « cette fonction ne doit pas être appelée sur une pile vide » ?

Un commentaire peut diverger de l'implémentation sans que personne ne s'en aperçoive
Un contrat voit sa syntaxe et ses types vérifiés par le compilateur
Un contrat peut être vérifié automatiquement à l'exécution
Un contrat devient l'entrée d'une preuve statique via SPARK (chapitre 16)

La spécification existe dans le code, sous une forme vérifiable.

C’est le monde d’Ada depuis 2012.

Avec les invariants de type (Type_Invariant), on peut également écrire des contraintes du type « les valeurs de ce type satisfont toujours cette propriété ».

14. Tâches ── La concurrence intégrée au langage

L’autre grand attrait d’Ada est que la concurrence fait partie de la spécification du langage.

Là où C/C++ s’appuient sur des API du système d’exploitation ou des bibliothèques (pthread, std::thread) pour les threads, Ada avait déjà intégré les tâches au langage dès 1983.

with Ada.Text_IO;

procedure Task_Demo is

   task Worker;

   task body Worker is
   begin
      for I in 1 .. 3 loop
         Ada.Text_IO.Put_Line ("worker:" & Integer'Image (I));
         delay 0.5;
      end loop;
   end Worker;

begin
   for I in 1 .. 3 loop
      Ada.Text_IO.Put_Line ("main  :" & Integer'Image (I));
      delay 0.5;
   end loop;
end Task_Demo;

Déclarez une task, et l’exécution concurrente démarre dès que le bloc englobant commence.

Et surtout, le bloc ne se termine pas tant que toutes les tâches qu’il contient ne sont pas terminées.

Toute une catégorie de bogues du type « on a oublié le join d’un thread, et des choses étranges se produisent à la fin du processus » devient structurellement impossible.

Pour la synchronisation entre tâches, le langage fournit le rendez-vous (rendezvous).

task Logger is
   entry Write (Message : String);
end Logger;

task body Logger is
begin
   loop
      select
         accept Write (Message : String) do
            Ada.Text_IO.Put_Line (Message);
         end Write;
      or
         terminate;
      end select;
   end loop;
end Logger;

Le code appelant écrit Logger.Write ("hello");, exactement sous la forme d’un appel de procédure.

La communication entre tâches par passage de messages peut ainsi s’écrire sans aucune connaissance des verrous.

Pour les systèmes temps réel, la politique d’ordonnancement, le contrôle des priorités, et même — pour faciliter la vérification — le profil Ravenscar, qui restreint les fonctionnalités de tâches, sont tous normalisés.

15. Objets protégés ── Écrire l’exclusion mutuelle comme un type

Pour l’exclusion mutuelle sur des données partagées, on utilise les objets protégés (protected object), introduits avec Ada 95.

protected Shared_Counter is
   procedure Increment;
   function  Value return Natural;
private
   Count : Natural := 0;
end Shared_Counter;

protected body Shared_Counter is

   procedure Increment is
   begin
      Count := Count + 1;
   end Increment;

   function Value return Natural is
   begin
      return Count;
   end Value;

end Shared_Counter;

Les données d’un objet protégé ne sont accessibles que par les opérations que l’on a définies.

Et l’exclusion mutuelle est garantie par le langage lui-même.

procedure  lecture-écriture autorisée, exécution exclusive
function   lecture seule, autorise l'exécution simultanée de plusieurs tâches
entry      peut faire attendre l'appelant jusqu'à ce qu'une condition (barrière) soit satisfaite

Dans la plupart des langages, l’exclusion mutuelle repose souvent sur la discipline du développeur.

Prendre tel mutex avant de toucher à ces données
Ne pas oublier de relâcher le verrou
Respecter l'ordre des verrous

Avec les objets protégés d’Ada, il est tout simplement impossible d’écrire du « code ayant oublié de prendre le verrou ».

Les données et l’exclusion mutuelle qui les protège sont déclarées ensemble, comme un seul et même type.

Grâce aux conditions de barrière d’entry, une synchronisation conditionnelle comme « attendre que des données arrivent dans la file » peut aussi s’écrire sans gérer manuellement de drapeaux ou de variables de condition.

16. SPARK ── La voie vers la vérification formelle

Le monde d’Ada compte un allié puissant : SPARK.

SPARK est un sous-ensemble d’Ada, conçu pour que les propriétés d’un programme puissent être prouvées mathématiquement.

procedure Increment (X : in out Integer)
  with SPARK_Mode,
       Pre  => X < Integer'Last,
       Post => X = X'Old + 1;

L’outillage SPARK (GNATprove) prouve, pour ce code, des propriétés comme les suivantes — sans jamais l’exécuter.

Aucun dépassement (overflow) ne se produit
Aucune violation de contrainte de plage ne se produit
Aucune division par zéro ne se produit
Aucune lecture de variable non initialisée
Cohérence entre Pre et Post

La différence avec les tests est décisive.

Test    vérifie que le programme se comporte correctement pour les entrées choisies
Preuve  démontre qu'une propriété est vraie pour toutes les entrées possibles

Les contrats (Pre/Post) vus au chapitre 13 deviennent, avec SPARK, directement l’objet de la preuve.

Un contrat écrit comme vérification à l’exécution peut ensuite être promu au statut de « prouvé ».

SPARK a fait ses preuves dans le monde de l’aérospatiale et de la défense, mais ces dernières années son usage industriel s’étend — NVIDIA, par exemple, l’a adopté pour la sécurité de ses firmwares.

L’idée reçue selon laquelle « les méthodes formelles sont trop académiques pour un usage professionnel » continue d’être discrètement, mais constamment, démentie par l’écosystème Ada/SPARK.

17. Interopérabilité avec C et C++

Ada n’est pas un langage isolé.

L’interopérabilité avec C est normalisée dans la spécification du langage (Annexe B).

Par exemple, pour appeler la fonction Sleep de l’API Windows depuis Ada, on écrit ceci.

with Interfaces.C;

procedure Sleep_Demo is

   procedure Sleep (Milliseconds : Interfaces.C.unsigned)
     with Import,
          Convention    => Stdcall,
          External_Name => "Sleep";

begin
   Sleep (1000);
end Sleep_Demo;

Voici les points clés.

Import           importe une implémentation externe
Convention       spécifie la convention d'appel (C, Stdcall, etc.)
External_Name    spécifie le nom du symbole au moment de l'édition de liens
Interfaces.C     fournit des types correspondant aux types C (int, unsigned, char*, etc.)

Le sens inverse est également possible.

Avec Export, une procédure écrite en Ada peut être exposée comme une fonction appelable depuis C.

Cela permet une utilisation progressive, par étapes, comme celle-ci.

Utiliser une bibliothèque C existante depuis Ada
N'écrire que le cœur du système en Ada/SPARK, en laissant la périphérie en C/C++
Compiler le code Ada en DLL pour l'appeler depuis d'autres langages

Ada n’est pas un langage dont la seule voie serait « tout réécrire » : il peut coexister avec les actifs existants tout en renforçant la fiabilité en commençant par les parties les plus critiques.

18. Environnement de développement ── GNAT et Alire (fonctionne aussi sous Windows)

On pourrait penser qu’« essayer Ada nécessite des outils coûteux ».

Aujourd’hui, un environnement de développement complet et gratuit est disponible.

GNAT        le compilateur Ada inclus dans GCC (gratuit)
Alire       le gestionnaire de packages et outil de build d'Ada
GNAT Studio l'IDE d'AdaCore
VS Code     complétion et navigation vers les définitions via l'extension Ada Language Server

L’arrivée d’Alire en particulier (dont la commande est alr) a rendu les débuts avec Ada radicalement plus simples.

L’expérience est proche de celle de cargo pour Rust.

alr init --bin hello_ada
cd hello_ada
alr build
alr run

alr init crée le projet, alr build le compile, alr run l’exécute.

Alire récupère lui-même la chaîne d’outils (GNAT), si bien qu’il n’est même pas nécessaire d’installer le compilateur manuellement.

Cela fonctionne aussi bien sous Windows, Linux que macOS.

Pour développer sous Windows, voici le chemin le plus court.

1. Récupérer l'installateur Windows depuis le site officiel d'Alire
2. Créer un squelette avec alr init --bin
3. Installer l'extension Ada (par AdaCore) dans VS Code
4. Compiler et exécuter avec alr build

Les bibliothèques peuvent aussi être ajoutées avec alr with <nom_de_la_bibliothèque>.

L’époque où l’on renonçait dès la mise en place de l’environnement est révolue.

19. Faiblesses et points de vigilance d’Ada

Jusqu’ici, nous avons présenté les atouts d’Ada, mais il a aussi des faiblesses.

Faisons-en un état des lieux équilibré.

Un écosystème restreint
  Peu de choix pour les frameworks web, les interfaces graphiques (GUI), les SDK cloud, etc.
  Le nombre de packages d'Alire n'est pas du même ordre de grandeur que les langages dominants

Peu de talents et d'informations
  L'information en japonais est particulièrement rare
  Adopter Ada en équipe implique de prévoir un coût de formation

Une syntaxe qui peut sembler verbeuse
  Les déclarations de types et la séparation spec/body sont lourdes pour de petits scripts
  Peu adapté à un usage du type « faire tourner quelque chose rapidement »

Un marché de l'emploi limité
  Concentré sur des secteurs comme l'aérospatiale, la défense, le ferroviaire

L’histoire nous enseigne aussi que l’idée « utiliser Ada, c’est être en sécurité » n’est pas si simple.

L’explosion du premier vol de la fusée Ariane 5, en 1996, avait pour cause, entre autres, un logiciel écrit en Ada.

Un code écrit pour Ariane 4 avait été réutilisé sur Ariane 5, dont les caractéristiques de vol diffèrent ; une valeur inattendue, trop grande, a déclenché un Constraint_Error lors d’une conversion, qui n’a pas été traité correctement, provoquant l’arrêt du système.

Voici ce que cet accident nous montre.

La vérification à l'exécution du langage a bien détecté le problème (il n'y a pas eu de rupture silencieuse)
Mais les hypothèses d'exploitation avaient changé, et cela n'avait pas été revérifié
La conception après le déclenchement de l'exception (le mode de repli sûr) était insuffisante

Ni le système de types ni les contrats ne peuvent remplacer le processus de remise en question des hypothèses.

Le langage fait partie de l’ingénierie de la sécurité, il n’en constitue pas la totalité.

C’est, je crois, l’avertissement le plus honnête à garder à l’esprit en apprenant Ada.

20. Logiciels à longue durée de vie et Ada ── Du point de vue de la maintenance

Sur ce site, nous traitons souvent de la maintenance et du prolongement de la durée de vie des actifs Windows existants.

Sous cet angle, Ada présente un autre type d’attrait.

Il n’est pas rare que des systèmes écrits en Ada continuent de fonctionner à l’échelle de plusieurs décennies.

Et la conception même du langage Ada part du principe d’une maintenance sur le long terme.

Séparation de la spec (.ads) et de l'implémentation (.adb)
  -> Un mainteneur, 20 ans plus tard, peut comprendre l'interface en ne lisant que la spec

Types forts et contraintes de plage
  -> Les hypothèses implicites restent dans le code, non dans la tradition orale ou les commentaires

Contrats (Pre/Post)
  -> « Les promesses de cette fonction » restent sous une forme vérifiable

Vérification d'exhaustivité de case
  -> Le compilateur énumère lui-même les endroits affectés par un changement de spécification

Rétrocompatibilité privilégiée même à travers les révisions du standard
  -> Une grande partie du code Ada 83 compile encore avec les compilateurs modernes

Tout cela peut être importé tel quel comme principes de conception, y compris pour la maintenance à long terme en C# ou en C++.

Définir des types porteurs de sens (types représentant un identifiant, types porteurs d'une unité) plutôt que d'utiliser int
Concevoir des types dont les valeurs invalides ne peuvent pas être construites (validation dans le constructeur)
Séparer consciemment l'interface publique de l'implémentation
Exprimer les préconditions et postconditions par des assertions et des tests
Écrire les switch sur des types énumérés de façon exhaustive, et traiter les avertissements comme des erreurs

Même sans jamais utiliser Ada professionnellement, apprendre sa philosophie de conception a une valeur certaine.

En tant que support pédagogique pour ressentir ce que signifie « exprimer la conception par les types », Ada reste aujourd’hui de tout premier ordre.

21. Conclusion

Nous avons passé en revue l’attrait d’Ada.

Récapitulons les points essentiels.

Ada est un langage bien vivant, utilisé sans interruption depuis plus de 40 ans dans les systèmes à haute fiabilité
Son nom vient d'Ada Lovelace, et le dernier standard en date est Ada 2022
Des types de même structure mais de noms différents sont des types différents : confondre des unités devient une erreur de compilation
Les contraintes de plage empêchent les valeurs invalides au niveau même du type
Les tableaux sont vérifiés sur leurs bornes, et les débordements de tampon ne deviennent pas un comportement indéfini
Les packages séparent la spec de l'implémentation, et les modes de paramètres rendent le flux de données explicite
Les génériques énoncent dans la spécification les opérations requises, ce qui rend les erreurs d'utilisation limpides
Les contrats d'Ada 2012 (Pre/Post) laissent la spécification dans le code, sous une forme vérifiable
Les tâches et les objets protégés permettent d'écrire la concurrence de façon sûre, comme une fonctionnalité du langage
Avec SPARK, un contrat peut être promu d'une vérification à l'exécution à une preuve mathématique
Avec GNAT et Alire, on peut l'essayer immédiatement et gratuitement, même sous Windows
Ses faiblesses sont la petite taille de l'écosystème et la rareté des talents
Les mécanismes de sécurité du langage ne remplacent pas le processus de remise en question des hypothèses

En termes de popularité, Ada est un langage qui n’est jamais devenu dominant.

Pourtant, la sécurité des types nuls, la vérification d’exhaustivité, les contrats, une rigueur proche de la notion de propriété (ownership) — autant de choses que les langages modernes introduisent comme des « nouveautés » —, Ada les possédait déjà depuis des décennies.

L’essence d’Ada peut se résumer en une phrase.

Un bogue n’est pas quelque chose qu’on trouve, mais quelque chose que les types et les contrats rendent impossible à écrire.

Un week-end, créez un projet avec Alire, et écrivez un petit programme en vous laissant gronder par le compilateur.

Le jour où vous réaliserez que chacune de ces erreurs de compilation est « un bogue attrapé avant de devenir un incident en production », l’attrait d’Ada vous paraîtra évident.

Références

Articles récents partageant les mêmes étiquettes, pour approfondir des sujets proches.

Ces pages replacent le sujet dans un contexte plus large de services et de décisions.

Questions fréquentes

Questions souvent posées lors d’une consultation sur le sujet de cet article.

Le langage Ada est-il encore utilisé aujourd'hui ?
Oui, il l'est. Ada continue d'être utilisé depuis des décennies dans des systèmes à haute fiabilité où un bogue se traduit directement par une perte de vies humaines ou des pertes financières considérables : contrôle de vol des avions civils, contrôle du trafic aérien, systèmes de signalisation et de sécurité ferroviaires, fusées et satellites, systèmes de défense, dispositifs médicaux, et une partie des systèmes centraux de la finance et de l'industrie. Ada est un langage révisé en continu depuis plus de 40 ans : après Ada 83 en 1983, il est passé par Ada 95, Ada 2005 et Ada 2012, et le dernier standard en date est Ada 2022.
En quoi le typage fort d'Ada diffère-t-il des autres langages ?
En Ada, deux types déclarés sous des noms différents sont traités comme des types distincts, même si leur structure est strictement identique. Par exemple, si l'on crée un type Meters et un type Seconds à partir de Float, les mélanger provoque une erreur de compilation, et toute conversion doit être écrite explicitement. De plus, un subtype peut faire porter au type une contrainte de plage de valeurs (par exemple de 0 à 100), et toute violation déclenche à l'exécution une exception Constraint_Error. C'est une conception qui ne repose pas sur la « vigilance », la « relecture » ou les « tests » pour repérer des bogues comme la confusion d'unités, mais qui empêche purement et simplement la compilation de réussir.
Comment essayer gratuitement le langage Ada ?
Avec GNAT (le compilateur Ada gratuit inclus dans GCC) et Alire (le gestionnaire de packages et outil de build d'Ada, dont la commande est alr), un environnement de développement complet et gratuit est disponible sous Windows, Linux et macOS. Récupérez l'installateur depuis le site officiel d'Alire, créez un squelette de projet avec alr init --bin, compilez avec alr build et exécutez avec alr run — une expérience proche de cargo pour Rust. Alire récupère lui-même la chaîne d'outils (le compilateur), donc aucune installation manuelle du compilateur n'est nécessaire. VS Code dispose d'une extension Ada développée par AdaCore.
Quelles sont les faiblesses du langage Ada ?
On peut citer un écosystème restreint, avec moins de choix que les langages dominants pour les frameworks web, les interfaces graphiques (GUI) ou les SDK cloud ; une pénurie de talents et d'information (particulièrement en japonais), ce qui impose de prévoir un coût de formation ; des déclarations de types et une séparation spécification/corps qui peuvent sembler lourdes pour de petits scripts ; et un marché de l'emploi concentré sur l'aérospatiale, la défense et le ferroviaire. Il faut aussi garder à l'esprit, comme le montre l'accident d'Ariane 5 en 1996, que les mécanismes de sécurité du langage ne remplacent pas le processus de remise en question des hypothèses de départ.

Profil de l’auteur

Page de présentation de l’auteur de l’article.

Go Komura

Représentant de KomuraSoft LLC

Spécialisé dans le développement de logiciels Windows, le conseil technique et l’analyse de pannes, notamment pour les systèmes existants et les incidents difficiles à reproduire.

Retour au blog