Ada to najbardziej zalecany język do systemów safety-critical. Zainteresowałem się nim bardziej już jakiś czas temu, ale do tej pory nie było okazji się w niego bardziej zagłębić. Sytuacja zmieniła się podczas Embedded World, kiedy to otrzymałem dwie książki o Adzie. Po ich lekturze nabrałem przekonania, że więcej systemów (nie tylko safety-critical) powinno powstawać w tym języku. A przede wszystkim więcej ludzi powinno go znać. Ten artykuł jest wprowadzeniem do Ady z opisem elementów wyróżniających ją na tle innych języków.
Artykuł powstał w ramach „Tygodnia z Adą” podczas którego od poniedziałku do piątku będą ukazywać się materiały na temat języka Ada. Będzie między innymi dlaczego Ada tak dobrze nadaje się do safety-critical, pokażę swoje pierwsze próby z pisaniem programów w Adzie, a także postaram się uruchomić Adę na STM32.
Informacje ogólne
Ada to język stworzony na potrzeby departamentu obrony USA. Prace trwały od połowy lad 70-tych. Pierwsza ustandaryzowana wersja języka to Ada83. Aktualnie obowiązująca wersja to Ada2012. W międzyczasie były jeszcze Ada95 i Ada2005. Więcej o różnicach między poszczególnymi wersjami języka można znaleźć tutaj.
Składnia Ady jest podobna do Pascala. Mamy bloki begin-end zamiast nawiasów klamrowych, czy przypisanie za pomocą znaków :=. Jednak Ada jest od Pascala dużo bardziej rozbudowana i posiada wszystko, co powinien zawierać nowoczesny język programowania. Jest więc wsparcie dla programowania obiektowego – klasy, interfejsy, dziedziczenie. Są wskaźniki na funkcje, przeciążanie funkcji i operatorów, typy generyczne, wsparcie współbieżności. Biblioteka standardowa Ady zawiera implementacje podstawowych kontenerów i algorytmów.
Ada jest językiem kompilowanym do kodu maszynowego. Możemy za jej pomocą pisać aplikacje na Windowsa, Linuxa i na bare-metal. Są wersje kompilatorów dla x86, ARM, AVR a nawet Lego Mindstorms. Co ciekawe, ze strony Adacore można ściągnąć również kompilatory Ady na JVMa i .NET.
Filozofia Ady jest taka, żeby kod jasno wyrażał intencję programisty i trudno było popełnić przypadkowy błąd. Jest to zupełnie inne podejście niż w C, gdzie uważa się, że programista wie co robi i nie powinniśmy go ograniczać.
Rozbudowany system typów
Można deklarować własne typy na podstawie typów bazowych. Ada sprawdza te typy i nie daje ich mieszać podczas operacji arytmetycznych i logicznych nawet, jeżeli typ bazowy jest taki sam. Co to oznacza? W C mamy niby typedefy do definiowania własnych typów. Jednak są to tylko aliasy i kompilator nadal rozumie je jako typy bazowe. Dlatego bez problemu możemy zrobić coś takiego:
typedef uint32_t meter_t; typedef uint32_t second_t; meter_t distance = 5; second_t duration = 10; distance += duration;
Z resztą nawet, gdyby te typy były inne np. uint32_t i float, C zrobi dla nas niejawną konwersję i ewentualnie w niektórych przypadkach rzuci warningiem. Zobaczmy teraz jak by to wyglądało w Adzie:
type meter_t is new Integer; type second_t is new Integer; distance: meter_t := 5; duration: second_t := 10; distance := distance + duration; -- COMPILATION ERROR!
Bezpieczne tablice
Jednną z najczęstszych luk bezpieczeństwa jest wyjście poza rozmiar tablicy. W językach takich jak C jesteśmy w stanie w ten sposób nadpisać dowolny adres. Ada nam na to nie pozwoli:
tab : array (0 .. 10) of Integer; tab(10) := 5; tab(11) := 10; -- warning: value not in range of subtype of "Standard.Integer" -- warning: "Constraint_Error" will be raised at run time -- raised CONSTRAINT_ERROR : main.adb:14 range check failed
Podczas kompilacji dostaliśmy warning o przepełnieniu tablicy, a w run-time program nie pozwolił na wykonanie tej operacji.
Skoro jesteśmy przy tablicach, to w Adzie nie musimy toczyć świętej wojny pod tytułem „Indeksy od 0 vs od 1”. Jak widać w powyższym kodzie, sami deklarujemy sobie zakres indeksów i nic nie stoi na przeszkodzie, żeby zaczynać również od 5, czy jakiejkolwiek innej liczby. Dzięki temu nie musimy w wielu miejscach wykonywać arytmetyki na offsetach.
Funkcje
Częstym problemem podczas wywoływania funkcji jest możliwość pomylenia kolejności argumentów. W Adzie możliwym rozwiązaniem tego problemu jest jawne przypisywanie wartości parametrom wejściowym:
type meter_t is new Integer; type second_t is new Integer; type velocity_t is new Integer; function calc_velocity(distance: meter_t; duration: second_t) return velocity_t is begin return velocity_t(distance)/velocity_t(duration); end calc_velocity; dummy : velocity_t; dummy := calc_velocity(duration => 10, distance => 5);
Jak widać kolejność parametrów nie ma znaczenia jeśli podamy ich nazwy. Możemy również podawać parametry w klasyczny sposób bez nazw i wtedy kolejność jest ważna.
Przy okazji widzimy tutaj, że zwracana przez funkcję wartość musi być koniecznie zapisana. Jeżeli nie mamy zamiaru jej wykorzystywać, powinniśmy zapisać ją do zmiennej o nazwie dummy, unused itp. (jest kilka dozwolonych nazw na takie zmienne określonych przez standard). Inne nazwy zmiennych zmuszają nas do późniejszego wykorzystania wyniku do dalszego przetwarzania. W ten sposób język wymusza na nas pamiętanie np. o obsłudze zwracanych kodów błędów.
Inne cechy zwiększające bezpieczeństwo
W Adzie zastosowano również wiele innych rozwiązań zwiększających bezpieczeństwo. Nie będę tutaj każdego tak szczegółowo opisywał. Zainteresowanych zachęcam do samodzielnego zgłębienia tych tematów. Do tych rozwiązań należą między innymi:
- Enumy, których nie są traktowane jako inty – czyli podobnie jak enum class w C++. Z tą różnicą, że mogą być indeksami tablicy.
- Wsparcie dla Design by Contract – możliwość zdefiniowania warunków wejściowych (preconditions), wyjściowych (postconditions) i niezmienników (invariants) dla procedur.
- Deterministyczne sprzątanie obiektów podobne do RAII w C++.
- Bezpieczne stringi przechowujące rozmiar, zamiast „null terminated strings”.
- Brak preprocesora.
- Podział kodu na moduły za pomocą package. Package składa się z dwóch plików – ads zawierający specyfikację modułu (odpowiednik headera) i pliki adb zawierający body, czyli implementację modułu. Nie ma dzięki temu problemów znanych z C i C++ z wielokrotnym includowaniem tego samego pliku.
- Obsługa współbieżności za pomocą zmiennych protected (implementujących mutexy pod spodem) i mechanizmu Rendezvous do komunikacji między taskami. Dzięki temu programista nie musi zarządzać niskopoziomowymi metodami synchronizacji tasków.
Tytuł
Jeżeli ktoś pisał projekt w C/C++ i trzymał się podwyższonych standardów takich jak MISRA C, na pewno nie raz przeklinał narzędzia do analizy statycznej zwracające miejsca odstęp od standardu. Pisząc w C/C++ praktycznie nie da się samodzielnie wyłapać wszystkich takich miejsc. Nie myślimy o wszystkich niejawnych rzutowaniach, możliwych overflowach itp. Po statycznej analizie musimy więc zmieniać nasz kod, co nieraz psuje czytelność. Ada większość tych sprawdzeń wykonuje podczas kompilacji. Więc już sam fakt, że nasz kod kompiluje się bez błędów stawia program napisany w Adzie na równi z kodem C/C++ trzymającym się dodatkowych standardów. Dokładniejsze porównanie Ady z High Integrity C++ można znaleźć w tym artykule.
Podsumowanie
Ada jest bardzo przemyślanem językiem. Zawiera minimalny zestaw potrzebnych funkcji. Nowe elementy są dodawane z rozwagą, przez to nie pozostają w języku dziwne konstrukcje potrzebne do kompatybilności wstecznej, których nie należy używać. Inne języki takie jak C++, czy Rust wyraźnie wzorują się Adzie w pewnych kwestiach. To tyle tytułem wstępu, w kolejnej części wezmę się za napisanie czegoś w Adzie.
15 kwietnia 2019 at 08:52
Fajnie że zacząłeś ten temat!