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.
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.

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;