Korzystając z C++ na systemach embedded najczęściej wyłączamy obsługę exceptionów. W tym artykule wyjaśnię dlaczego tak robimy oraz jakie zagrożenia z tym się wiążą.
Dlaczego w embedded nie używamy exceptionów?
Systemy embedded posiadają ograniczenia rozmiaru programu, wykorzystywanej pamięci i czasu wykonywania poszczególnych operacji. Wykorzystanie wyjątków rodzi następujące problemy:
- Czas obsługi wyjątku jest niedeterministyczny i stosunkowo długi – niektóre systemy nie mogą sobie pozwolić na długie oczekiwanie i nieznany czas operacji. Dlatego w systemach hard real time na pewno nie możemy skorzystać z exceptionów.
- Zwiększa rozmiar programu dodając kod do obsługi exceptionów. – wynikowa binarka jest zdecydowanie większa niż analogiczny program w C. Jeżeli dysponujemy naprawdę małym mikrokontrolerem jest to poważny problem. Jednak nowe procesory mają coraz więcej FLASHu, dlatego ma to coraz mniejsze znaczenie. Problem często jest bardziej w głowach inżynierów przyzwyczajonych, aby zawsze żyłować na maksa rozmiar programu. W ten sposób także rodzą się mity o tym jak C++ nie nadaje się do embedded.
W gcc możemy użyć flagi -fno-exception aby wyłączyć obsługę wyjątków.
Mozilla Firefox – program na PC bez exceptionów
Czasemwyjątki są wyłączane rónież w programach na PC. Przyczyną jest zwykle chęć poprawienia wydajności. Ogólna zasada mówi, że nie powinniśmy tego robić. Dobrze napisany kod z wyjątkami nie spowalnia specjalnie programu. Problemy z performancem zwykle są gdzie indziej. A wyłączając exceptiony możemy sobie łatwo strzelić w stopę. Przykładem programu na PC napisanego bez obsługi wyjątków jest Mozilla Firefox. Można o tym przeczytać w ich informacji o dozwolonych konstrukcjach języka. Jak można się domyślać, zrodziło to pewne trudności, o których można przeczytać tutaj.
Na co trzeba uważać?
Czai się tu na nas pewna pułapka. To, że skompilowaliśmy nasz kod z flagą -fno-exception nie oznacza wcale, że zewnętrzne biblioteki również są skompilowane z tą flagą. Największym zagrożeniem są tutaj biblioteki standardowe. Kompilator dołącza prekompilowane wersje wykorzystywanych funkcji, które oczywiście mogą rzucać wyjątki. Teoretycznie możemy samemu skompilować bibliotekę standardową z odpowiednimi flagami, ale nie jest ona dostosowana do pracy bez exceptionów.
Co więcej takich wyjątków rzuconych przez bibliotekę standardową nie możemy złapać w naszym kodzie. Flaga -fno-exception powoduje, że użycie bloku try/catch jest traktowane jako błąd kompilacji. Czyli nie ma możliwości przechwycenia exceptiona z STLa.
Operator new bez exceptionów
Przykładowym miejscem, gdzie coś może pójść nie tak jest operator new, który w przypadku błędu alokacji zwraca wyjątek bad_alloc. Alternatywą może tu być placement new ze składnią:
MyClass *tmp = new(0xA5A5A5A5) MyClass();
Gdzie w nawiasie podajemy adres pod którym ma zostać utworzony obiekt. Kolejną opcją jest nothrow new:
MyClass *tmp = new (std::nothrow) MyClass();
W tym wypadku otrzymamy wskaźnik do zaalokowanego obiektu w przypadku sukcesu lub null pointer w przypadku błędu.
Uwagi końcowe
W systemach embedded nie zaleca się korzystania z heapa, a więc i używać operatora new. Tak samo zamiast algorytmów i kontenerów z STLa warto korzystać ze specjalnych bibliotek na embedded. Więcej na ten temat pisałem w niedawnym artykule:
20 września 2018 at 17:04
Ciekawostka jest taka, że główny wspomniany w artykule problem – czyli to, że po wyłączeniu wyjątków w aplikacji, wciąż mogą być one obecne w bibliotece standardowej toolchaina – jest w sumie jedną z głównych przyczyn istnienia mojego wariantu toolchaina dla ARM, czyli bleeding-edge-toolchain.
https://github.com/FreddieChopin/bleeding-edge-toolchain
W przypadku bleeding-edge-toolchain biblioteki te są właśnie kompilowane z wyłączonymi wyjątkami C++, dzięki czemu problem w sumie przestaje istnieć (;