C ++: Metaprogramowanie z API kompilatora zamiast z funkcjami C ++

10

Zaczęło się od pytania SO, ale zdałem sobie sprawę, że jest to dość niekonwencjonalne i oparte na rzeczywistym opisie na stronach internetowych, może być lepiej dostosowane do programistów.se, ponieważ pytanie ma wiele pojęć koncepcyjnych.

Uczyłem się języka LibTooling i jest to bardzo potężne narzędzie, które może w przyjazny sposób ujawnić cały „drobiazgowy” charakter kodu, to znaczy semantycznie , a nie zgadując. Jeśli clang może skompilować kod, to clang jest pewien semantyki każdego pojedynczego znaku w tym kodzie.

Pozwól mi teraz cofnąć się na chwilę.

Istnieje wiele praktycznych problemów, które pojawiają się, gdy ktoś angażuje się w metaprogramowanie szablonów C ++ (a zwłaszcza gdy wyrusza poza szablony na terytorium sprytnych, choć przerażających makr). Szczerze mówiąc, dla wielu programistów, w tym mnie, wiele zwykłych zastosowań szablonów jest również nieco przerażających.

Wydaje mi się, że dobrym przykładem byłyby łańcuchy czasu kompilacji . To pytanie ma już ponad rok, ale jasne jest, że C ++ w tej chwili nie ułatwia tego zwykłym śmiertelnikom. Chociaż patrzenie na te opcje nie wystarcza, aby wywołać u mnie mdłości, niemniej jednak nie jestem przekonany, czy jestem w stanie stworzyć magiczny, maksymalnie wydajny kod maszynowy, pasujący do każdej fantazyjnej aplikacji, którą mam dla mojego oprogramowania.

Spójrzmy prawdzie w oczy, ludzie, łańcuchy są dość proste i podstawowe. Niektórzy z nas chcą po prostu wygodnego sposobu na wyemitowanie kodu maszynowego, który ma pewne „ciągłe” ciągi znaków znacznie więcej niż dostajemy, gdy kodujemy go w prosty sposób. W naszym kodzie C ++.

Wpisz clang i LibTooling, który udostępnia abstrakcyjne drzewo składniowe (AST) kodu źródłowego i pozwala prostej niestandardowej aplikacji C ++ poprawnie i niezawodnie manipulować surowym kodem źródłowym (za pomocą Rewriter) wraz z bogatym semantycznym obiektowym modelem zorientowanym na wszystko w AST. Obsługuje wiele rzeczy. Wie o rozszerzeniach makr i pozwala śledzić te łańcuchy. Tak, mówię o transformacji kodu źródłowego lub tłumaczeniu.

Moją podstawową tezą jest to, że clang pozwala nam teraz tworzyć pliki wykonywalne, które same mogą działać jako idealne niestandardowe etapy preprocesora dla naszego oprogramowania C ++, a my możemy zaimplementować te etapy metaprogramowania w C ++. Jesteśmy po prostu ograniczeni przez fakt, że ten etap musi pobierać dane wejściowe, które są poprawnym kodem C ++ i produkować jako wynik bardziej poprawny kod C ++. Plus wszelkie inne ograniczenia, które stosuje system kompilacji.

Dane wejściowe muszą być co najmniej bardzo zbliżone do poprawnego kodu C ++, ponieważ w końcu clang jest frontonem kompilatora, a my po prostu szukamy i jesteśmy kreatywni dzięki jego interfejsowi API. Nie wiem, czy istnieje jakikolwiek przepis umożliwiający zdefiniowanie nowej składni do użycia, ale najwyraźniej musimy opracować sposoby jej prawidłowej analizy i dodać ją do projektu clang, aby to zrobić. Oczekiwać więcej, to mieć coś w projekcie clang, który jest poza zakresem.

Żaden problem. Wyobrażam sobie, że niektóre funkcje makr bez obsługi poradzą sobie z tym zadaniem.

Innym sposobem spojrzenia na to, co opisuję, jest zaimplementowanie konstrukcji metaprogramowania przy użyciu środowiska wykonawczego C ++ poprzez manipulowanie AST naszego kodu źródłowego (dzięki clang i jego API) zamiast implementowania ich przy użyciu bardziej ograniczonych narzędzi dostępnych w samym języku. Ma to również wyraźne zalety w zakresie wydajności kompilacji (nagłówki z dużym szablonem spowalniają kompilację proporcjonalnie do tego, jak często ich używasz. Wiele skompilowanych rzeczy jest następnie starannie dopasowywanych i wyrzucanych przez linker).

Jest to jednak kosztem wprowadzenia dodatkowego kroku lub dwóch w procesie kompilacji, a także wymogu napisania nieco (co prawda) nieco bardziej szczegółowego oprogramowania (ale przynajmniej jest to proste środowisko uruchomieniowe C ++) jako część naszego narzędzia .

To nie jest cały obraz. Jestem pewien, że generowanie kodu jest o wiele większe niż w przypadku podstawowych funkcji językowych. W C ++ możesz napisać szablon lub makro lub szaloną kombinację obu, ale w narzędziu clang możesz modyfikować klasy i funkcje w KAŻDY sposób, który możesz osiągnąć w C ++, w czasie wykonywania , mając jednocześnie pełny dostęp do treści semantycznej, oprócz szablonu i makr oraz wszystkiego innego.

Zastanawiam się więc, dlaczego wszyscy już tego nie robią. Czy to dlatego, że ta funkcja clang jest tak nowa i nikt nie zna ogromnej hierarchii klas AST klanu? To nie może być to.

Być może po prostu trochę nie doceniam trudności, ale „manipulowanie ciągiem znaków w czasie kompilacji” za pomocą narzędzia clang jest prawie kryminalnie proste. Jest pełny, ale niesamowicie prosty. Wszystko, czego potrzeba, to kilka makropoleceń, które odwzorowują rzeczywiste std::stringoperacje. Wtyczka clang implementuje to, pobierając wszystkie odpowiednie makropolecenia no-op i wykonuje operacje na łańcuchach. To narzędzie jest następnie wstawiane jako część procesu kompilacji. Podczas kompilacji te nie wywoływane funkcje makr są automatycznie analizowane w wynikach, a następnie wstawiane z powrotem jako zwykłe stare ciągi czasu kompilacji w programie. Program można następnie skompilować jak zwykle. W rzeczywistości ten wynikowy program jest również znacznie bardziej przenośny, nie wymagając wymyślnego nowego kompilatora obsługującego C ++ 11.

Steven Lu
źródło
To niezwykle długie pytanie. Czy mógłbyś skondensować to do swoich najistotniejszych punktów?
amon
Publikuję wiele długich pytań. Myślę jednak, że szczególnie w tym przypadku wszystkie części pytania są ważne. Może pomiń pierwsze 6 akapitów? Ha ha.
Steven Lu
3
Brzmi bardzo podobnie do makr syntaktycznych zapoczątkowanych w Lisp i ostatnio wybranych przez Haxe, Nemerle, Scala i podobne języki. Istnieje sporo informacji na temat tego, dlaczego makra Lisp są uważane za szkodliwe. Chociaż nie słyszałem jeszcze przekonujących argumentów, możesz znaleźć powody, dla których ludzie niechętnie dodawali je do każdego języka (poza tym, że niekoniecznie jest to proste).
back2dos
Tak, to meta-ifying C ++. Co może oznaczać lepszy, szybszy kod. Co do tych języków. Cóż, od czego mam zacząć. Jaka jest gra wideo o wartości wielu milionów dolarów zaimplementowana w jednym z tych języków? Jaka jest nowoczesna przeglądarka internetowa zaimplementowana w jednym z tych języków? Jądro systemu operacyjnego? W porządku, wygląda na to, że Haxe ma pewną przyczepność, ale masz pomysł.
Steven Lu
1
@ nwp, cóż, nie mogę nic poradzić na to, że zauważyłem, że chyba przegapiłeś cały punkt wpisu. Ciągi czasu kompilacji to po prostu najbardziej wymyślny i minimalny konkretny przykład możliwości, które są nam teraz dostępne.
Steven Lu

Odpowiedzi:

7

Tak, Virginia, jest Święty Mikołaj.

Pojęcie używania programów do modyfikowania programów istnieje już od dawna. Pierwotny pomysł przyszedł od Johna von Neumanna w postaci komputerów z programami przechowywanymi. Ale modyfikowanie kodu maszynowego w dowolny sposób jest dość niewygodne.

Ludzie zazwyczaj chcą modyfikować kod źródłowy . Jest to głównie realizowane w postaci systemów transformacji programu (PTS) .

Ogólnie rzecz biorąc, PTS oferuje, dla co najmniej jednego języka programowania, możliwość analizowania AST, manipulowania nim i regenerowania poprawnego tekstu źródłowego. Jeśli faktycznie przekopujesz się, w większości popularnych języków ktoś zbudował takie narzędzie (Clang jest przykładem dla C ++, kompilator Java oferuje tę funkcję jako API, Microsoft oferuje Rosyln, JDT Eclipse, ...) z procedurami Interfejs API, który jest całkiem przydatny. W szerszej społeczności prawie każda społeczność specyficzna dla języka może wskazywać na coś takiego, wdrażanego z różnym poziomem dojrzałości (zwykle skromnym, wielu „tylko parserów produkujących AST”). Szczęśliwego metaprogramowania.

[Jest odbiciem zorientowane na społeczność, która próbuje zrobić metaprogramowanie od wewnątrz języka programowania, ale tylko osiągnąć „wykonania” zachowanie modifiation, i tylko w takim zakresie, że kompilatory język jakąś dostępną przez odbicie informacje. Z wyjątkiem LISP, zawsze są szczegóły dotyczące programu, które nie są dostępne przez odbicie („Luke, potrzebujesz źródła”), które zawsze ograniczają możliwości odbicia.]

Bardziej interesujące PTS robią to dla dowolnych języków (podajesz narzędziu opis języka jako parametr konfiguracyjny, w tym przynajmniej BNF). Taki PTS pozwala także na transformację „źródło do źródła”, np. Określenie wzorów bezpośrednio przy użyciu składni powierzchniowej języka docelowego; za pomocą takich wzorców możesz kodować interesujące fragmenty i / lub znajdować i zamieniać fragmenty kodu. Jest to o wiele wygodniejsze niż programowanie API, ponieważ nie musisz znać wszystkich mikroskopijnych szczegółów dotyczących AST, aby wykonać większość swojej pracy. Potraktuj to jako meta-metaprogramowanie: -}

Wada: jeśli PTS nie oferuje różnego rodzaju przydatnych analiz statycznych (tabele symboli, analizy i analizy przepływu danych), trudno jest w ten sposób napisać naprawdę interesujące transformacje, ponieważ trzeba sprawdzać typy i weryfikować przepływ informacji dla większości praktycznych zadań. Niestety, ta funkcja jest w rzeczywistości rzadka w ogólnym PTS. (Jest to zawsze niedostępne w przypadku zawsze proponowanego „Gdybym tylko miał parser ...” Zobacz moją biografię, aby uzyskać dłuższą dyskusję na temat „Life After Parsing”).

Istnieje twierdzenie, które mówi, że jeśli możesz przerabiać napisy [a więc przepisywać drzewo], możesz wykonać dowolną transformację; a więc wielu PTS opiera się na tym, aby twierdzić, że można metaprogramować wszystko za pomocą tylko przepisanych drzewek. Chociaż twierdzenie to jest satysfakcjonujące w tym sensie, że jesteś pewien, że możesz zrobić wszystko, nie jest satysfakcjonujące w ten sam sposób, w jaki zdolność maszyny Turinga do robienia czegokolwiek nie czyni programowania maszyny Turinga wybraną metodą. (To samo dotyczy systemów z tylko proceduralnymi interfejsami API, jeśli pozwolą one na wprowadzanie dowolnych zmian w AST [i faktycznie myślę, że nie jest tak w przypadku Clanga]).

To, czego chcesz, to najlepsze z obu światów, system, który oferuje ogólność sparametryzowanego języka PTS (nawet obsługując wiele języków), z dodatkowymi analizami statycznymi, możliwością łączenia transformacji między źródłami a procedurami Pszczoła. Znam tylko dwa , które to robią:

  • Język metaprogramowania Rascal (MPL)
  • nasz zestaw Deng Software Reengineering Toolkit

O ile nie chcesz samodzielnie pisać opisów języków i analizatorów statycznych (dla C ++ jest to ogromna ilość pracy, dlatego Clang został zbudowany zarówno jako kompilator, jak i jako podstawa metaprogramowania proceduralnego), będziesz potrzebować PTS z dojrzałymi opisami języków już dostępny. W przeciwnym razie poświęcisz cały swój czas na konfigurowanie PTS i nikt nie wykona pracy, którą naprawdę chciałeś wykonać. [Jeśli wybierzesz przypadkowy, niemainstreamowy język, tego kroku bardzo trudno uniknąć].

Rascal próbuje to zrobić, wybierając opcję „OPP” (parsery innych osób), ale to nie pomaga w części dotyczącej analizy statycznej. Wydaje mi się, że mają całkiem dobrą Javę, ale jestem pewien, że nie używają C ani C ++. Ale jest narzędziem akademickim; ciężko ich winić.

Podkreślam, że nasze [komercyjne] narzędzie DMS ma dostępne pełne interfejsy Java, C, C ++. W przypadku C ++ obejmuje prawie wszystko w C ++ 14 dla GCC, a nawet warianty Microsoftu (i teraz dopracowujemy), ekspansję makr i zarządzanie warunkowe oraz kontrolę na poziomie metody i analizę przepływu danych. I tak, można określić gramatyki zmiany w praktyczny sposób; zbudowaliśmy niestandardowy system VectorC ++ dla klienta, który radykalnie rozszerzył C ++, aby wykorzystać ilość operacji na równoległych tablicach danych F90 / APL. DMS został wykorzystany do wykonywania innych masowych zadań metaprogramowania na dużych systemach C ++ (np. Przekształcanie architektury aplikacji). (Jestem architektem DMS).

Szczęśliwego meta-metaprogramowania.

Ira Baxter
źródło
Fajnie, myślę, że Clang i DMS, choć mają pewne nakładające się możliwości, są programami, które tak naprawdę nie należą do tej samej kategorii. Chodzi mi o to, że jeden jest prawdopodobnie absurdalnie drogi i prawdopodobnie nigdy nie uzasadnię zasobów potrzebnych do uzyskania dostępu do niego, a drugi to nieograniczone bezpłatne oprogramowanie typu open source. To ogromna różnica ... Częścią tego, co mnie ekscytuje tymi ekscytującymi możliwościami metaprogramowania, jest fakt, że dozwolone jest nie tylko swobodne korzystanie z nich, ale także swobodne rozpowszechnianie narzędzi binarnych opartych na clang.
Steven Lu
Wszystko sprzedawane na rynku jest „drogo drogie” w porównaniu do bezpłatnych. Surowy koszt nie jest problemem; ważne jest to, że dla niektórych osób zwrot z tytułu zakupu produktu komercyjnego jest wyższy niż zwrot z darmowego artefaktu, w przeciwnym razie nie byłoby oprogramowania komercyjnego. To oczywiście zależy od twoich konkretnych potrzeb. Clang jest interesującym punktem w przestrzeni narzędzi i na pewno będzie miał przydatne punkty. Chciałbym pomyśleć (ponieważ jestem architektem DMS), że DMS ma szersze możliwości. Na przykład Clang raczej nie obsługuje języków innych niż C ++.
Ira Baxter,
Na pewno. Nie ma wątpliwości, że DMS jest niesamowicie potężny, prawie do magii (à la Arthur C. Clarke), i chociaż clang jest świetny, tak naprawdę jest to po prostu dobrze napisana nakładka C ++, której jest wiele. To bardzo wiele małych kroków naprzód, ale porównywanie go do DMS byłoby niesprawiedliwe. Niestety, nawet przy tak potężnych narzędziach, którymi dysponujemy, działające oprogramowanie nie pisze się samo. Musi nadal istnieć poprzez staranne tłumaczenie przy użyciu narzędzi lub (prawie zawsze najlepsza opcja) napisane na nowo.
Steven Lu
Nie możesz sobie pozwolić na tworzenie narzędzi takich jak Clang lub DMS od nowa. Zazwyczaj nie możesz sobie pozwolić na rzucenie aplikacji napisanej przez zespół 10 osób w ciągu 5 lat. Będziemy potrzebować takich narzędzi coraz częściej, ponieważ rozmiary oprogramowania i okresy jego użytkowania stale rosną.
Ira Baxter,
@StevenLu: Cóż, DMS dziękuje za komplement, ale nie ma w tym nic magicznego. DMS ma zalety prawie 2 liniowych dekad inżynierii i czystej platformy architektonicznej (aw, shucks, YMMV), która dobrze sobie radzi. Podobnie, Clang ma w sobie wiele dobrej inżynierii. Zgadzam się, nie zostały one zaprojektowane w celu rozwiązania dokładnie tego samego problemu ... Zakres DMS jest wyraźnie przewidziany jako większy, jeśli chodzi o symboliczne manipulowanie programem, i znacznie mniejszy, jeśli chodzi o bycie kompilatorem produkcyjnym.
Ira Baxter,
4

Metaprogramowanie w C ++ z API kompilatorów (zamiast szablonów) jest rzeczywiście interesujące i praktycznie możliwe. Ponieważ metaprogramowanie nie jest (jeszcze) znormalizowane, będziesz przywiązany do konkretnego kompilatora, co nie ma miejsca w przypadku szablonów.

Zastanawiam się więc, dlaczego wszyscy już tego nie robią. Czy to dlatego, że ta funkcja clang jest tak nowa i nikt nie zna ogromnej hierarchii klas AST klanu? To nie może być to.

Wiele osób to robi, ale w innych językach. Moim zdaniem większość programistów C ++ (lub Java lub C) nie widzi potrzeby (być może słusznie) lub nie jest przyzwyczajona do metod metaprogramowania; Myślę też, że są zadowoleni z funkcji refaktoryzacji / generowania kodu w swoim IDE i że każdy fanator może być postrzegany jako zbyt skomplikowany / trudny do utrzymania / trudny do debugowania. Bez odpowiednich narzędzi może to być prawda. Należy również wziąć pod uwagę bezwładność i inne problemy pozatechniczne, takie jak zatrudnianie i / lub szkolenie ludzi.

Nawiasem mówiąc, ponieważ wspominamy o Common Lisp i jego makrosystemie (patrz odpowiedź Basile'a), muszę powiedzieć, że zaledwie wczoraj Clasp zostało wydane (nie jestem związany):

Clasp ma być zgodną implementacją Common Lisp, która kompiluje się z LLVM IR. Ponadto udostępnia programistom biblioteki Clanga (AST, Matcher).

  • Po pierwsze, oznacza to, że możesz pisać w CL i nie używać C ++, z wyjątkiem korzystania z jego bibliotek (i jeśli potrzebujesz makr, użyj makr CL).

  • Po drugie, możesz pisać narzędzia w języku CL dla istniejącego kodu C ++ (analiza, refaktoryzacja, ...).

rdzeń rdzeniowy
źródło
3

Kilka kompilatorów C ++ ma mniej lub bardziej udokumentowane i stabilne API, w szczególności większość kompilatorów wolnego oprogramowania.

Clang / LLVM to przeważnie duży zestaw bibliotek i można z nich korzystać.

Najnowsze GCC akceptuje wtyczki . W szczególności możesz go rozszerzyć za pomocą MELT (który sam w sobie jest meta-wtyczką, która udostępnia język specyficzny dla domeny wyższego poziomu w celu rozszerzenia GCC).

Zauważ, że składnia C ++ nie jest łatwo rozszerzalna w GCC (i prawdopodobnie nie w Clang), ale możesz dodać własne pragmy, wbudowane atrybuty i przepustki kompilatora, aby zrobić to, co chcesz (być może także udostępniając niektóre makra preprocesora wywołujące te rzeczy w celu uzyskania przyjaznej dla użytkownika składni).

Być może zainteresują Cię wieloetapowe języki i kompilatory, patrz np. Wdrażanie języków wieloetapowych za pomocą AST, Gensym i Refleksji autorstwa C.Calcagno i in. i obejdź MetaOcaml . Z pewnością powinieneś zajrzeć do obiektów makro Common Lisp . Biblioteki JIT, takie jak libjit , błyskawica GNU , a nawet LLVM , lub po prostu -w czasie wykonywania mogą Cię zainteresować - wygeneruj kod C ++, rozwidlaj jego kompilację w bibliotece dynamicznej obiektów współdzielonych, a następnie dlopen (3), który udostępnił obiekt. Blog J.Pitrat jest również powiązany z takimi refleksyjnymi podejściami. A także RefPerSys .

Basile Starynkevitch
źródło
Ciekawy. To bardzo dobrze widzieć, że GCC nadal ewoluuje tutaj. To nie jest odpowiedź na wszystko, o co prosiłem, ale mimo to mi się podoba.
Steven Lu
Re: twoje nowe edycje ... To dobry punkt na temat samego przepisywania kodu. To faktycznie zaczyna przynosić takie możliwości metaprogramu również w C ++, o wiele bardziej dostępne niż wcześniej, co jest również dość interesujące.
Steven Lu