W poprzednim artykule opisywałem ogólne techniki zalecane przy developmencie systemów safety-critical. Dzisiaj natomiast przyjrzymy się bliżej zaleceniom dotyczącym języków programowania. Wiemy już, iż język używany w tego typu systemach powinien być kompilowany i silnie typowany. Jakie jeszcze wymagania powinien spełniać? Jakie języki wykorzystywane są w praktyce? Poniżej znajdziesz odpowiedzi na te pytania.
Pożądane cechy języka
Norma EN 50128 definiuje cechy idealnego języka programowania dla systemów safety-critical. Powinien on posiadać wbudowane funkcje utrudniające lub uniemożliwiające popełnienie błędów. Przykładem może być tu sprawdzanie typów w trakcie kompilacji. Język powinien również mieć przyjazną składnię pozwalającą na bezproblemowy development, weryfikację i utrzymanie. Kolejną istotną cechą jest jednoznaczność, czyli brak zachowań niezdefiniowanych lub zależnych od implementacji.
Najlepiej, o ile język jest również popularny, a co za tym idzie mamy do dyspozycji odpowiedniej jakości narzędzia takie jak kompilatory, debugery, IDE, czy zewnętrzne biblioteki. Niemile widziane elementy języka to instrukcje skoku bezwarunkowego (czyli niesławne goto), rekurencja, wskaźniki, dynamiczna alokacja pamięci, czy niejawna deklaracja i inicjalizacja zmiennych.
Stawiane wymagania nie są możliwe do pełnego zrealizowania przez istniejące języki programowania. Zwiększenie bezpieczeństwa stoi często w sprzeczności z łatwością implementacji. Dlatego niezbędny jest pewien kompromis i nie powstał jeszcze język, który idealnie spełnia wszystkie wymagania. W normie możemy znaleźć jednak tabelę przedstawiającą rekomendowane języki.
1 | ADA | R | HR | HR | HR | HR |
2 | Modula-2 | R | HR | HR | HR | HR |
3 | PASCAL | R | HR | HR | HR | HR |
4 | C or C++ | R | R | R | R | R |
5 | BASIC | R | NR | NR | NR | NR |
6 | Assembler | R | R | R | R | R |
7 | C# | R | R | R | R | R |
8 | Java | R | R | R | R | R |
W rubrykach znajdują się stopnie rekomendacji: R- recommended, HR – highly recommended, NR – not recommended. Tabela zawiera kilka starych języków, które zdążyły już wypaść z powszechnego użytku. Natomiast niektóre nowsze języki takie jak Rust, czy Go nie zostały w ogóle uwzględnione. Poza tym kilka pozycji wymaga większego komentarza.
Dlaczego Ada jest zalecana?
Ada to język stworzony na potrzeby amerykańskiego wojska do prac nad systemami safety-critical takimi jak np. myśliwce. Głównym założeniem projektowym Ady jest umożliwienie tworzenia bezpiecznych systemów. Dlatego znajdziemy w niej między innymi:
- możliwość określania zakresów zmiennych,
- sprawdzanie przepełnienia buforów i wyjścia poza zakres tablicy w runtime,
- wbudowane wsparcie dla współbieżności (umożliwia np. wykrywanie deadlocków),
- design by contract,
- wykrywanie błędów alokacji pamięci.
Zwiększone bezpieczeństwo programów pisanych w Adzie wiąże się z nieco mniejszą wydajnością. Jednak jest to tylko pozorny problem, ponieważ w innych językach musielibyśmy zaimplementować defensywny kod manualnie. Więcej o Adzie można przeczytać tu i tu.
Funkcje oferowane przez Adę najlepiej wpisują się w wymagania systemów safety-critical. Dlatego nie jest niespodzianką, iż norma szczególnie poleca właśnie Adę. Dziwić natomiast może uznanie za tak samo zalecane języków Pascal i Modula-2. W latach 80-tych i 90-tych były one wykorzystywane także w programowaniu embedded, jednak w dzisiejszych czasach nie są już używane. Posiadają pewne cechy, które czynią je bardziej bezpiecznymi niż na przykład najpopularniejsze w embedded C. Szczególnie system typów jest bardziej restrykcyjny – na przykład typ char nie jest konwertowany domyślnie do inta. Pascal i Modula-2 nie nadają się jednak do pisania nowoczesnych systemów. Nie są już aktywnie rozwijane i brakuje im pewnych funkcjonalności. Jednak Ada może być uznawana za spadkobiercę tych języków. Zapożyczyła część ich składni i jest modernizowana o nowe funkcjonalności wraz z kolejnymi standardami.
C w safety-critical
Najpopularniejszym językiem w systemach embedded jest C. Mimo, iż Ada jest bezpieczniejsza, w systemach safety-critical również C jest szeroko wykorzystywane. Głównym założeniem C jest, iż programista wie co robi i prawie wszystko jest dozwolone. Dlatego umożliwia napisanie dziwnego, trudnego do zrozumienia i potencjalnie niebezpiecznego, ale za to działającego kodu. Przykładem może być choćby Duff’s Device, czyli kreatywne podejście do składni switch-case i do-while. Poza tym C zawiera tzw. undefined behavior, czyli niektóre błędne operacje nie są zgłaszane w czasie kompilacji, ale ich wynik nie jest zdefiniowany. Dzięki temu wynikowy kod może być bardziej optymalny. Oczywiście powyższe cechy C stoją w sprzeczności z oczekiwaniami normy wobec języka programowania.
Dlatego aby stosować C w systemach safety-critical powinniśmy zdefiniować „Language Subset”, czyli wydzielić z języka potencjalnie niebezpieczną część i jej nie używać. Ogólnie przyjętym standardem w safety-critical jest MISRA-C. Została ona stworzona dla branży samochodowej w latach 90-tych, ale jest również chętnie wykorzystywana w systemach medycznych, wojskowych, czy lotniczych. Zasady zdefiniowane w MISRA-C są skonstruowane tak, aby ich stosowanie mogły wymuszać narzędzia do statycznej analizy. Zarówno treść standardu MISRA-C jak i większość narzędzi są płatne. realizowane są jednak prace nad wsparciem w darmowym Cppchecku. Standard MISRA-C jest szerszym tematem i wpis na ten temat jeszcze się pojawi.
Jak to jest z C++?
W tabeli z normy C i C++ zostały potraktowane wspólnie, jednak występują między nimi znaczące różnice. W niektórych aspektach C++ zwiększa bezpieczeństwo (po raz kolejny system typów), ale dodaje również nowe funkcjonalności utrudniające wnioskowanie na temat kodu np. niejawnie wykonywany kod konstruktorów, templaty i wiele innych. Dobrze zastosowany C++ może zwiększyć zarówno bezpieczeństwo kodu jak i jego czytelność. Jednak użyty niewłaściwie będzie miał efekt odwrotny. Dodatkowo C++ rozwija się dużo aktywniej od C i co 3 lata powstaje nowy standard. Aby bezpiecznie korzystać z C++ w safety-critical, potrzebny jest podobny standard jak MISRA-C, a na to potrzeba czasu. Podejmowane są próby standaryzacji takie jak:
- JSF++ – Coding Guideline na potrzebny Lockheed Martin do prac nad myśliwcami. Powstał na podstawie MISRA-C98 i w pracach brał udział sam Bjarne Stroustup. Zasady dla C++03. Po więcej informacji odsyłam do prezentacji na CppCon:
- MISRA-C++2008 – Powstał w dużej mierze w oparciu o JSF++, również zawiera zasady dla C++03 i brakuje aktualizacji standardu dla modern C++.
- AUTOSAR-C++14 – nowszy standard dla branży automotive wydany w 2017, darmowy i obejmujący C++14. Zasady są wzorowane na MISRA-C++, C++ Core Guidelines, czy książce Effective Modern C++.
Jednak nie mają one szans nadążyć za tempem pojawiania się nowych wersji C++.
Java i C#
Co interesujące Java i C# w tabeli wcale nie są mniej zalecane niż C, C++ czy Asembler. W końcu te języki wyparły C++, bo są łatwiejsze w użyciu i bezpieczniejsze. Jednak w przypadku Javy i C# główne problemy są związane z zarządzaniem pamięcią. Bazują one na dynamicznej alokacji, która nie jest mile widziana w safety-critical. Poza tym posiadają garbage collector wprowadzający do systemu niedeterministyczność. Systemy czasu rzeczywistego muszą mieć zapewnione czasy (na poziomie mili czy choćby mikrosekund) wykonania pewnych operacji IO takich jak np. odcięcie zasilania sterowanego silnika w przypadku awarii.
Kolejnym ograniczeniem Javy i C# jest zależność od systemu operacyjnego. W systemach embedded nie mamy do dyspozycji Windowsa, czy Linuxa. Najczęściej działamy na systemach operacyjnych czasu rzeczywistego (RTOSach), albo w ogóle bez żadnego systemu, czyli mamy aplikację bare-metal. Podejmowane są próby wykorzystania tych języków w systemach embedded, ale raczej są to projekty eksperymentalne.
Jednak systemy safety-critical nie ograniczają się tylko do systemów embedded. Owszem – zwykle sterowniki odpowiedzialne za najbardziej krytyczne operacje to systemy embedded, jednak istnieją też systemy wyższego poziomu, które mogą na przykład zajmować się nadzorem, czy być interfejsem użytkownika dla operatorów. Tutaj mogą sprawdzić się już standardowe aplikacje desktopowe, które jak najbardziej można pisać w Javie albo C#.
Co z Rustem?
Bardzo ciekawą alternatywą dla języków aktualnie wykorzystywanych w systemach safety-critical ostatnio staje się Rust. Podobnie jak Ada jest on tworzony z myślą o bezpieczeństwie. Z kolei składnia jest trochę wzorowana na C++. Rust między innymi uniemożliwia używanie null pointerów i zapobiega problemom ze współbieżnością takim jak wyścigi. Rust posiada dwa tryby – safe i unsafe. W trybie safe niedozwolone są pewne operacje mogące skutkować niezdefiniowanym zachowaniem. o ile natomiast chcemy np. odnosić się bezpośrednio do szczegółów hardware’owych – możemy skorzystać z trybu unsafe.
Możliwości Rusta czynią go bardzo kuszącym wyborem do nowych projektów, niestety w safety-critical pozostało zbyt wcześnie, żeby go używać. Dopiero niedawno Rust stał się w miarę stabilny, a nowe wersje dalej wychodzą co kilka tygodni. Musimy również poczekać na biblioteki, narzędzia i wsparcie od producentów procesorów. Dlatego Rust bez wątpienia znajdzie zastosowanie w safety-critical, ale pewnie dopiero za kilka lat. Najpierw musi dojrzeć i sprawdzić się w różnych mniej wymagających projektach.