Ben Brosgol en Pat Rogers zijn senior leden van Adacore’s technische staf.

2 May 2014

Pieter Edelman

Ada staat vooral te boek als traditionele programmeertaal voor missiekritieke systemen. De programmeertaal is echter geschikt als generieke taal die continu mee-evolueert met de ontwikkelingen binnen de software-engineering en heeft enkele relevante features voor embedded toepassingen. Ben Brosgol en Pat Rogers van Adacore introduceren de taal en leggen het fundamentele verschil met C-gebaseerde talen uit.

De jaren zeventig brachten een reeks belangrijke mijlpalen voort in de softwareontwikkeling: gestructureerd programmeren, abstracte datatypes, excepties, generieke templates en bouwblokken voor parallel programmeren. Begin jaren tachtig ontstond er een initiatief om al deze verspreide concepten samen te brengen in een enkele, generieke taal die geschikt is voor grootschalige realtime embedded systemen: Ada.

Vandaag de dag is Ada nog steeds uitstekend geschikt voor dit doel. In de loop der jaren heeft de taal een forse evolutie ondergaan. Met een grootschalige herziening halverwege de jaren negentig is bijvoorbeeld ondersteuning toegevoegd voor objectgeoriënteerd programmeren. De laatste versie stamt uit 2012 en introduceerde het concept van contractgebaseerd programmeren: een kijk op software die uitgaat van suppliers en clients die met elkaar samenwerken via strikte contracten, verklaringen over programma-eigenschappen die runtime of zelfs statisch te checken zijn. Ze worden veel gebruikt om pre- en postcondities te stellen van een subroutine (‘subprogram’ in de Ada-terminologie). Hebben we bijvoorbeeld een subprogramma om een element toe te voegen aan een datastructuur met een vast aantal elementen, dan kunnen we de preconditie stellen dat deze nog niet vol mag zijn en de postconditie dat deze niet leeg is. Dit soort contracten biedt direct vanuit de broncode betekenisvolle en automatisch te controleren semantiek voor het subprogramma.

Ada wordt dan ook volop gebruikt in domeinen waar betrouwbaarheid van embedded systemen een vereiste is: de luchtvaart, het spoor, de ruimtevaart (waaronder satelliet- en raketbesturing), de scheepvaart, communicatie, procescontrole en de energiesector. De taal heeft zich ook bewezen in de academische wereld voor onderzoek naar embedded systemen, met geslaagde projecten uiteenlopend van robotica tot satellietsystemen.

 advertorial 

Free webinar ‘Modernizing your code base with C++20’

As many production tool chains now adopt C++20 features, the potential this brings is unlocked. What advantages can recent versions offer to your code base? In this webinar we’ll look at the great improvements C++ has gone through and how features like concepts and ranges can transform your code. Register for video access.

Uitlaatkleppen

Ada heeft een heel andere ontwerpfilosofie dan C of C++. C gaat in beginsel uit van laagniveaucontrole met een nauwe relatie naar de onderliggende computerarchitectuur – denk aan hoe met arrays wordt gewerkt via een pointer naar het eerste element. Dit is gelijk ook een mooie illustratie van C’s gevoeligheid voor programmeerfouten: crashes en beveiligingsproblemen zijn welbekend wanneer de pointer voorbij de toegewezen ruimte wijst. C is ook slecht schaalbaar doordat er maar beperkte features zijn voor encapsulatie of het opdelen van de programmatuur in modules.

C++ is ontstaan om deze problemen aan te pakken. Op het C-fundament worden features gestapeld als OO, namespaces en excepties. Daarmee behoudt de taal grotendeels de efficiency van C, maar ook veel van de kwetsbaarheden.

Ada is ontworpen met deze kwetsbaarheden in het achterhoofd. Op hoog niveau bieden C++ en Ada vergelijkbare functionaliteit, maar de manier waarop beide die bereiken, is totaal verschillend. Ada is gebouwd op een fundament dat verificatie (tijdens compilatie dan wel bij uitvoering) intrinsiek opneemt in de semantiek.

Dat conflicteert soms met de eisen voor embedded systemen. In C/C++ worden datatypes bijvoorbeeld regelmatig impliciet geïnterpreteerd als een ander datatype (zeg een 32 bit unsigned integer die als pointer naar een datastructuur wordt gebruikt). Dit staat haaks op de type-checks die hoogniveautalen eisen. Zaken als laagniveaucontrole, interruptafhandeling en parallellisme zijn regelmatig nodig in embedded systemen. Andere vereisten betreffen eigenschappen van het doelplatform zoals geheugenlimieten of realtimeaspecten.

Ada pakt deze problemen op verschillende fronten aan (zie de voorbeeldcode onderaan dit artikel). Laagniveaufunctionaliteit, ten eerste, wordt toegevoegd via specifieke taalfeatures en bibliotheken. Om objecten bijvoorbeeld naar een specifieke geheugenlocatie te schrijven, zijn adresclausules beschikbaar, en met representatieclausules zijn de bitpositie en omvang van datastructuren precies te regelen. Ten tweede kunnen de teugels rond types wat worden gevierd via ‘uitlaatkleppen’ als unchecked conversion. Maar deze moeten wel expliciet worden gebruikt, zodat duidelijk is wanneer dat gebeurt. Ten derde kan Ada standaard interfacen met code in andere talen (vooral C). C-bibliotheken zijn zonder probleem te gebruiken in Ada, en andersom kan C-code Ada-software aanroepen.

Om systemen te laten passen in een beperkte embedded omgeving of om te voldoen aan certificatiestandaarden gebeurt het regelmatig dat taalfeatures worden ingeperkt, met name wanneer het gaat om complexe ondersteunende bibliotheken. Ada is daarom, ten vierde, standaard voorzien van een mechanisme om restricties aan te brengen op het gebruik van features.

In veiligheidskritieke domeinen moeten ontwikkelaars laten zien dat het systeem voldoet aan certificatiestandaarden zoals DO-178B (lucht- en ruimtevaart) of EN 50128 (spoorwegen). Certificering vereist vaak analyseerbare code en een zekere eenvoud van bibliotheken. Er is een deterministische subset van de concurrency-faciliteiten, het Ravenscar-profiel, dat geschikt is voor certificering en tegemoetkomt aan de eisen rond efficiëntie en codeafmetingen.

Cubesat
Afgelopen november werd een door Nasa gesponsorde Cubesat van het Vermont Technical College gelanceerd. Vanwege de betrouwbaarheid gebruikten de studenten de Spark-subset van Ada om de software hiervoor te ontwikkelen. De Cubesat zal drie jaar in de ruimte blijven en systemen testen die uiteindelijk voor een maanmissie worden gebruikt.

Kant-en-klare downloads

De markten waarin Ada wordt gebruikt, hebben één ding gemeen: het moet in één keer goed zijn. Problemen of correcties in het veld zijn praktisch onmogelijk of simpelweg te duur. Ada’s compile-time controles voorkomen allerhande soorten problemen en maken het makkelijker voor statische analysetools om kwetsbaarheden al tijdens de ontwikkeling op te sporen.

Maar geen enkele programmeertaal is perfect. Er moeten altijd afwegingen worden gemaakt waarbij sommige doelstellingen het onderspit moeten delven om andere te halen. Bij Ada was de belangrijkste technische vraag begin jaren tachtig of een taal van dergelijke omvang, schijnbare complexiteit en rijke semantiek betrouwbaar en efficiënt geïmplementeerd kon worden, met name voor embedded toepassingen. Mede dankzij de feature voor het inperken van de taal werd dat bereikt: er is geen runtime prijs voor onnodige complexiteit.

Andere belangrijke aspecten bij de keuze van een programmeertaal zijn de beschikbaarheid van documentatie, compilers en gereedschappen, de evolutie van de taal en het aantal actieve gebruikers. De markt voor Ada is weliswaar niet zo groot als die voor C of C++, maar de bestaande implementaties zijn van hoge kwaliteit en de taal wordt streng beheerd onder het wakend oog van standaardisatieorgaan Iso. De Ada-specificatie is gratis beschikbaar online, net als allerhande lesmateriaal en artikelen. Bij Adacore ontwikkelden we een compiler, runtime bibliotheken en allerhande tools, die we als opensource software onder de GPL-licentie beschikbaar stellen aan de Free Software Foundation. Via kant-en-klare downloads voor de meeste platforms proberen we Ada verder onder de aandacht te brengen. Vooral in de academische gemeenschap is de taal waardevol gebleken voor onderzoek naar realtime systemen.

Ada vormt ook de basis voor Spark, een subset verrijkt met annotaties waarmee programma-eigenschappen zoals de afwezigheid van runtime fouten formeel kunnen worden aangetoond. Spark wordt gebruikt in embedded systemen met de hoogste veiligheids- en beveiligingsniveaus.

Een programmeertaal kan er natuurlijk niet voor zorgen dat code correct, onderhoudbaar of platformonafhankelijk zal zijn, maar de features van een taal kunnen wel het beste in de ontwikkelaar naar boven halen. Ada is een programmeertaal die vanaf het allereerste begin is opgebouwd vanuit drie overheersende principes: betrouwbaarheid en onderhoudbaarheid van de software, programmeren als menselijke activiteit en efficiëntie. Al deze drie zijn belangrijk voor embedded systemen, maar het valt op dat de eerste twee ook betrekking hebben op het economische plaatje van hedendaagse software: we zijn steeds afhankelijker van software en arbeidskosten spelen een dominante rol. Het is logisch om een taal te gebruiken die tegemoetkomt aan deze eigenschappen. Nu, maar ook in de toekomst.

Ada-voorbeeld

Ada is een hoogniveautaal; laagniveauprogrammeren wordt via specifieke taalfeatures toegankelijk gemaakt. De onderstaande code toont aan hoe dit typisch in zijn werk gaat. De toepassing – endianness wordt voor het gemak even genegeerd – is een buffer van een enkel element die asynchroon wordt beschreven en uitgelezen door parallelle lezer- en schrijver-threads (‘taken’ in Ada). De waarde wordt opgeslagen als een unsigned 32 bit integer die bijvoorbeeld van een externe bron afkomt, en wordt gelezen als struct (‘record’ in Ada) waarin de velden een specifieke bitpositie hebben. Een van de velden is een enum waarvan de elementen een specifieke integerwaarde hebben.

De Ada-oplossing laat verschillende features zien:

– representatie-clausules worden ingezet om de waarden van de enum-elementen te bepalen, de velden in het record te positioneren en om het aantal bits voor een type te definiëren;

– een protected object zorgt voor encapsulatie van de toegang tot een datastructuur die gedeeld wordt door verschillende taken, waardoor er steeds maar een tegelijk toegang krijgt;

– met unchecked conversion kan een waarde van het ene type als een ander type worden geïnterpreteerd;

– taken herbergen parallelle activiteiten die via een gedeeld object communiceren;

– via het ‘Valid-attribuut wordt gegarandeerd dat een object van een specifiek type aan een specifiek bitpatroon voldoet.

with Interfaces; use Interfaces;
package Communication_Pkg is
 
  type Urgency_Type is (Low, Medium, High); -- enumeration type
  for Urgency_Type use (Low => 2, Medium => 5, High => 10);
      -- Assigns specific values to enumeration elements
  for Urgency_Type'Size use 4;
      -- Values in records take 4 bits 

  type Packet is
     record
         Num     : Unsigned_16; -- from Interfaces package
         Urgency : Urgency_Type;
         F       : Boolean;
      end record;

   for Packet use -- assumes little endian
      record
         Num     at 0 range 0..15; -- Bytes 0 and 1
         Urgency at 2 range 0..3;  -- Byte 2, rightmost 4 bits
         F       at 3 range 2..2;  -- In byte 3
      end record;
   for Packet'Size use 32;

   protected Obj is
     procedure Write( New_Value : Unsigned_32 );
     procedure Read (Is_Valid  : out Boolean;
                     New_Value : out Packet);
   private
     Value : Unsigned_32;
     -- encapsulated, accessible only through Write and Read
   end Obj;
   -- Invocations of "protected operations" are executed with
   -- mutual exclusion
end Communication_Pkg;

with Unchecked_Conversion;
package body Communication_Pkg is

  function U32_To_Packet is
    new Unchecked_Conversion(Unsigned_32, Packet);
 
   protected body Obj is
     procedure Write( New_Value : Unsigned_32 ) is
     begin
        Value := New_Value;
     end Write;

     procedure Read (Is_Valid  : out Boolean;
                     New_Value : out Packet) is
       P : constant Packet := U32_To_Packet( Value );
     begin
        Is_Valid := P.Urgency'Valid;
        if Is_Valid then
          New_Value := P;
        else
          New_Value := U32_To_Packet( 0 );
        end if;
     end Read;
   end Obj;
end Communication_Pkg;

with Interfaces; use Interfaces;
with Communication_Pkg; use Communication_Pkg;
procedure Driver is
  task Reader;
  task Writer;

  task body Reader is
    P        : Packet;
    Is_Valid : Boolean;
  begin
     loop
        ...
        Obj.Read( Is_Valid, P );
        if Is_Valid then
           ... -- Process P
        else
           ... -- Report error
        end if;
     end loop;
  end Reader;
 
  task body Writer is
    N : Unsigned_32;
  begin
    loop
      ...
      N := ...; -- Acquire value to be written
      Obj.Write(N);
    end loop;
  end Writer;
begin
   null;
end Driver;

Edited by Pieter Edelman