Popularny post Matthew11 Napisano Kwiecień 7, 2021 Popularny post Udostępnij Napisano Kwiecień 7, 2021 Ten artykuł jest częścią serii "Kompilatory od podszewki" #1 - Wprowadzenie (właśnie to czytasz) #2 - Linker i biblioteki #3 - Automatyzacja procesu kompilacji, kompilatory a mikrokontrolery #4 - Programowanie mikrokontrolerów AVR z terminala O czym jest ta seria artykułów? Każdy większy projekt (o ile korzysta z jakiegoś kompilowanego języka), na pewnym etapie zacznie korzystać z jakiegoś dostępnego systemu budowania (o ile nie od razu). Który następnie zostanie zintegrowany z wykorzystanym IDE i... ta historia kończy się na kliknięciu przycisku Buduj. Ale czy zastanawiałeś się drogi czytelniku gdzie w tym procesie używany jest kompilator i co oznaczają te wszystkie argumenty, z którymi jest wywoływany? Jeśli nigdy nie zajrzałeś pod maskę to ta seria jest właśnie dla Ciebie! Z tej serii skorzystają zarówno początkujący jak również bardziej doświadczeni użytkownicy forum. Wszystkie przedstawione w tej części przykłady zostały przetestowane w systemie Windows (Windows 10) i Linux (Ubuntu 20.10). Logo kompilatora GCC. Dlaczego jest to ważne? Wiedza o tym do czego służy i co robi kompilator pozwala na pełne zrozumienie i kontrolowanie tego co robi wykorzystywane przez nas IDE. Posiadając taką wiedzę stajemy się bardziej świadomymi użytkownikami środowiska którego używamy, a gdy to zawiedzie będziemy w stanie poradzić sobie bez niego lub szybciej zrozumieć przyczynę problemu. Korzystanie z kompilatora z wiersza poleceń posiada kilka innych zalet: używanie kompilatora z wiersza poleceń działa tak samo niezależnie od platformy na której piszemy kod tj. będzie wyglądało tak samo na Windowsie, Linuxie czy macOSie i praktycznie nie różni się przy kompilacji natywnej i skrośnej, jak już raz nauczymy się korzystać z kompilatora to ta wiedza z nami zostanie - jak z nauką jazdy na rowerze - w przypadku IDE, z każdym nowym musimy uczyć się jego obsługi od początku, do dyspozycji mamy dodatkowe programy lub opcje, których IDE mogą nie posiadać, narzędzia konsolowe takie jak np. kompilator - rzadko się zmieniają lub rzadko zmieniają sposób ich użycia - więc nawet kilkuletni projekt powinien ponownie skompilować się bez większych problemów na nowszej wersji kompilatora, z kolei IDE zmieniają się dość dynamicznie i próba kompilacji kilkuletniego projektu na nowej wersji IDE może nie skończyć się sukcesem, wreszcie narzędzia konsolowe są łatwe w automatyzacji - możemy je łatwo zintegrować z systemem ciągłej integracji (ang. Continuous Integration - CI) Logo projektu LLVM - w skład projektu wchodzi kompilator Clang. Pierwszy program Po krótkim wprowadzeniu od razu przejdźmy do programu - jest to klasyczny przykład programu "Hello World!" napisanego w języku C++. Plik zawierający kod źródłowy programu ma nazwę main.cpp. // main.cpp #include <iostream> int main (int argc, char *argv[]) { std::cout << "Hello world!" << std::endl; return 0; } Jak z tego kodu otrzymać program, który możemy uruchomić? - Musimy posiadać kompilator i po prostu wystarczy uruchomić kompilację. W przypadku kompilatora GCC komenda ma następującą postać: $ g++ main.cpp Po jej wywołaniu w katalogu z kodem źródłowym pojawi się skompilowany program - który na Linuxie będzie nazywał się a.out a na Windowsie a.exe. Taki program możemy już uruchamiać jak każdy inny program. Powrócimy do tego tematu w dalszej części artykułu. Kompilator jest programem konsolowym - nie ma GUI - jego pracę dostosowujemy za pomocą przekazywanych w jego wywołaniu argumentów w powyższym przykładzie jedynym argumentem, który przekazaliśmy jest nazwa pliku z kodem źródłowym programu - main.cpp. Logo IDE Visual Studio - korzystającego z kompilatora MSVC - Microsoft Visual Studio Compiler. Jaki kompilator wybrać? Wiemy już jak kompilować programy, ale jaki kompilator powinniśmy wybrać? Pracując na Linuxie mamy do dyspozycji np. GCC i Clang. Na Windowsie do wyboru mamy MSVC (Microsoft Visual Studio Compiler), Clang lub MinGW (czyli port GCC na Windowsa). Istnieją inne kompilatory, jednak te wymienione wyżej należą do najpopularniejszej trójki kompilatorów. Wrócimy do tej kwestii w jednej z kolejnych części. Ta seria będzie korzystać z kompilatora GCC i Clang. Przykłady z użyciem jednego kompilatora (Clang) będzie można z powodzeniem (w większości przypadków) wykonać korzystając z drugiego kompilatora (GCC). Instalacja Clang i GCC - Ubuntu Dla Ubuntu instalację kompilatora Clang i GCC wykonujemy przez wywołanie w terminalu komendy: $ sudo apt-get install clang gcc g++ Instalacja Clang - Windows W przypadku Windows aby zainstalować kompilator Clang konieczne jest zainstalowanie narzędzi LLVM dla Windows - klikając w link "release page" następnie szukamy wersji dla Windows - dla wersji 32 bitowej: LLVM-x.y.z-win32.exe dla 64 bitowej: LLVM-x.y.z-win64.exe. W trakcie instalacji musimy zadbać o to, aby zaznaczyć opcję dodania LLVM do systemowej zmiennej środowiskowej PATH tak jak na poniższym zrzucie ekranu: Uwaga: w moim przypadku w trakcie różnych testów mimo zaznaczenia opcji o dodaniu wpisu do zmiennych środowiskowych instalator tego nie dokonywał. Dlatego po instalacji należy sprawdzić zawartość zmiennej PATH, a w przypadku braku wpisu dla LLVM należy go dodać, w tym celu: Należy otworzyć Windowsowy Start klikając na ikonę systemu (na pasku), następnie wpisujemy: Edytuj zmienne środowiskowe co przeniesienie nas do nowego okna i do zakładki Zaawansowane, gdzie w prawym dolnym rogu klikamy Zmienne środowiskowe, następnie pod oknem Zmienne systemowe wybieramy wpis Path i klikamy przycisk Edytuj następnie klikamy przycisk Nowy i w polu tekstowym podajemy ścieżkę gdzie zainstalowany został LLVM, u mnie przy standardowej instalacji był to katalog: C:\Program Files\LLVM\bin Akceptujemy zmiany i restartujemy komputer. Instalacja MinGW (GCC) - Windows W przypadku GCC a konkretnie portu GCC na Windows czyli MinGW - należy pobrać instalator a następnie zainstalować pamiętając o wybraniu architektury x86_64 w jednym z okien instalatora. Ostatnim krokiem jest dodanie do zmiennej środowiskowej PATH katalogu do miejsca z programem kompilatora - dokładnie tak samo jak w przypadku kompilatora Clang - w moim przypadku czyli standardowej instalacji do zmiennej PATH dodałem katalog: C:\Program Files\mingw-w64\x86_64-8.1.0-posix-seh-rt_v6-rev0\mingw64\bin. Instalacja - Podsumowanie Niezależnie od użytej platformy (Linux, Windows) jedno z wywołań: $ clang --version $ gcc --version powinno zwrócić wersję danego kompilatora i kilka dodatkowych informacji np.: Ubuntu clang version 11.0.0-2 Target: x86_64-pc-linux-gnu Thread model: posix InstalledDir: /usr/bin W przypadku błędów należy w zmiennej środowiskowej PATH sprawdzić dodane wpisy oraz katalog, który tam podajemy - czy faktycznie znajduje się tam program kompilatora. Loga języków C i C++. Praktyczne wykorzystanie i użycie kompilatora Jak wspomnieliśmy praca kompilatora może zostać dostosowana za pomocą przekazywanych przy wywołaniu argumentów / opcji. Czasami możemy się też spotkać z określeniem “przełączników” (ang. compiler switches) mówiąc w kontekście kompilatora. Dodatkowe argumenty/opcje/przełączniki pozwalają na dostosowanie procesu kompilacji - np. na rozszerzeniu listy ostrzeżeń przekazywanych przez kompilator, na poleceniu traktowania ostrzeżeń jako błędów, na ustawieniu poziomu optymalizacji czy na włączeniu opcji generowania assemblera itp. W tym momencie należy zwrócić uwagę na fakt, że w przykładzie ze wstępu kompilowaliśmy program napisany w C++. Dlatego nazwa stosowanego kompilatora ma dopisek “++”: clang++, g++. Gdybyśmy chcieli skompilować program napisany w C - wtedy zamiast clang++ i g++ użylibyśmy odpowiednio: clang i gcc. Warto dodać, że programy napisane w C możemy kompilować kompilatorami przeznaczonymi dla C++. Przeanalizujmy poniższy przykład wywołania w systemie Windows: $ clang++ -std=c++2a -O3 -o program.exe main.cpp Ten przykład od tego ze wstępu różni się tym, że przekazaliśmy kompilatorowi więcej argumentów. Poszczególne opcje oznaczają: -std pozwala ustawić wersję standardu języka C++ (lub C) wobec którego ma zostać skompilowany kod, w tym wypadku -std=c++2a oznacza wykorzystanie standardu C++20 - czyli obecnie najnowszego -O pozwala na ustawienie poziomu optymalizacji, w tym wypadku 3 - najwyższy -o pozwala na poleceniu kompilatorowi nadania konkretnej nazwy wynikowemu programowi - tutaj program został nazwany program.exe main.cpp - to ostatni argument - jest to nazwa pliku źródłowego (main.cpp), który chcemy skompilować W ten sposób możemy dodawać dodatkowe opcje w celu dostosowania pracy kompilatora do naszych potrzeb. Dodatkowe ostrzeżenia (warningi) Na pewno w trakcie jakiegoś kursu z C lub C++ spotkałeś się z podobnym przykładem do poniższego wywołania: $ g++ -Wall -Wextra main.cpp Co oznaczają opcje -Wall i -Wextra? Jest to rozkazanie kompilatorowi dokonywania dodatkowych sprawdzeń kodu - jest to taki “standardowy” zestaw flag ostrzegających przed potencjalnymi błędami, które warto stosować od samego początku. Szczegółowe informacje o tym jakie sprawdzenia zawierają się w konkretnych flagach znajdziemy w dokumentacji danego kompilatora. Tymczasem sprawdźmy ostrzeżenia w praktyce kompilując taki program: int main (int argc, char *argv[]) { int a; return 0; } za pomocą wywołania: $ clang++ -Wall -Wextra main.cpp W terminalu, kompilator poinformuje nas, że zmienna a oraz argumenty funkcji main() nie są używane w programie: clang++ -Wall -Wextra -o program main.cpp main.cpp:3:9: warning: unused variable 'a' [-Wunused-variable] int a; ^ main.cpp:1:14: warning: unused parameter 'argc' [-Wunused-parameter] int main(int argc, char* argv[]) ^ main.cpp:1:26: warning: unused parameter 'argv' [-Wunused-parameter] int main(int argc, char* argv[]) ^ 3 warnings generated. ~~~~~~^~~~~~ Wbrew pozorom flaga -Wall nie włącza wszystkich ostrzeżeń. Więcej informacji znajdziemy w artykule w którym autor przedstawia dodatkowe opcje i przykłady ich zastosowania, które pozwalają włączyć dodatkowe ostrzeżenia dla różnego rodzaju problemów które mogą występować w kodzie. Dobrą praktyką jest rozpoczęcie projektu z jak największym zestawem ostrzeżeń, a nawet traktować ostrzeżenia jako błędy - im bardziej zabezpieczymy się na samym początku projektu tym mniej czasu spędzimy później na poszukiwaniu źródła problemu. Łatwiej jest usuwać dodane przez nas na początku dodatkowe ostrzeżenia niż dostosować już istniejący kod do nowych wymagań. W tej części to wszystko W kolejnej części porozmawiamy nieco więcej o procesie budowania i linkowania oraz o tworzeniu bibliotek. 9 Cytuj Link do komentarza Share on other sites More sharing options...
Treker (Damian Szymański) Kwiecień 8, 2021 Udostępnij Kwiecień 8, 2021 @Matthew11 artykuł został właśnie zaakceptowany i jest już dostępny publicznie 🚀 Cytuj Link do komentarza Share on other sites More sharing options...
Szern Grudzień 26, 2023 Udostępnij Grudzień 26, 2023 Dziękuję! Bardzo brakowało mi takich artykułów. Kiedyś pisałem w Turbo Pascalu, a teraz uczę się C. Twoje artykułu pomogły mi zrozumieć różnice w procesie na poziomie kompilatora i linkera. Nie ma zbyt wielu informacji na ten temat w sieci. 1 Cytuj Link do komentarza Share on other sites More sharing options...
Pomocna odpowiedź
Dołącz do dyskusji, napisz odpowiedź!
Jeśli masz już konto to zaloguj się teraz, aby opublikować wiadomość jako Ty. Możesz też napisać teraz i zarejestrować się później.
Uwaga: wgrywanie zdjęć i załączników dostępne jest po zalogowaniu!