Dlaczego ISO / ANSI nie ustandaryzowało C ++ na poziomie binarnym? Istnieje wiele problemów z przenośnością w C ++, co wynika tylko z braku jego standaryzacji na poziomie binarnym.
Don Box pisze (cytując z książki Essential COM , rozdział COM As A Better C ++ )
C ++ i przenośność
Po podjęciu decyzji o dystrybucji klasy C ++ jako DLL, stoi przed jedną z podstawowych słabości C ++ , to znaczy brakiem standaryzacji na poziomie binarnym . Chociaż dokument roboczy ISO / ANSI C ++ próbuje skodyfikować, które programy będą się kompilować i jakie będą semantyczne efekty ich uruchomienia, nie podejmuje próby standaryzacji binarnego modelu środowiska wykonawczego C ++. Po raz pierwszy ten problem stanie się widoczny, gdy klient spróbuje połączyć się z biblioteką importu DLL FastString ze środowiska programistycznego C ++ innego niż to, z którego zbudowano bibliotekę FastString.
Czy są jakieś dodatkowe korzyści lub utrata tego braku binarnej standaryzacji?
Odpowiedzi:
Języki o skompilowanej formie binarnej to stosunkowo nowa faza [*], na przykład środowiska wykonawcze JVM i .NET. Kompilatory C i C ++ zwykle emitują kod macierzysty.
Zaletą jest to, że nie ma potrzeby JIT, interpretera kodu bajtowego, maszyny wirtualnej ani żadnej innej takiej rzeczy. Na przykład nie możesz napisać kodu ładowania początkowego, który działa przy uruchamianiu komputera, jako ładnego, przenośnego kodu bajtowego Java, chyba że maszyna może natywnie wykonać kod bajtowy Java lub masz jakiś konwerter z Java na rodzimy niezgodny binarnie kod wykonywalny (teoretycznie: nie jestem pewien, czy może być to zalecane w praktyce dla kodu bootstrap). Możesz napisać go mniej więcej w C ++, choć nie w przenośnym C ++, nawet na poziomie źródłowym, ponieważ spowoduje to wiele bałaganu z magicznymi adresami sprzętowymi.
Wadą jest to, że oczywiście natywny kod działa w ogóle tylko na architekturze, dla której został skompilowany, a pliki wykonywalne mogą być ładowane tylko przez moduł ładujący, który rozumie ich format wykonywalny, oraz łączą się z innymi plikami wykonywalnymi i wywołują je dla tej samej architektury i ABI.
Nawet jeśli posuniesz się tak daleko, połączenie dwóch plików wykonywalnych ze sobą będzie działało poprawnie tylko wtedy, gdy: (a) nie naruszysz reguły One Definition, co jest łatwe do zrobienia, jeśli zostały skompilowane z różnymi kompilatorami / opcjami / czymkolwiek, tak, że używali różnych definicji tej samej klasy (albo w nagłówku, albo dlatego, że każda statycznie łączyła się z różnymi implementacjami); oraz (b) wszystkie istotne szczegóły implementacji, takie jak układ struktury, są identyczne zgodnie z opcjami kompilatora obowiązującymi podczas kompilacji każdego z nich.
Aby zdefiniować to wszystko w standardzie C ++, usunęłoby to wiele swobód dostępnych obecnie dla implementatorów. Implementatorzy korzystają z tych swobód, szczególnie podczas pisania kodu bardzo niskiego poziomu w C ++ (i C, który ma ten sam problem).
Jeśli chcesz napisać coś, co wygląda trochę jak C ++, dla binarnie przenośnego celu, istnieje C ++ / CLI, który jest ukierunkowany na .NET i Mono, dzięki czemu możesz (miejmy nadzieję) uruchomić .NET w innym miejscu niż Windows. Myślę, że można przekonać kompilator MS do stworzenia czystych zestawów CIL, które będą działały na Mono.
Istnieją również potencjalnie rzeczy, które można zrobić za pomocą LLVM, aby utworzyć binarnie przenośne środowisko C lub C ++. Nie wiem jednak, czy pojawił się jakikolwiek powszechny przykład.
Ale wszystkie polegają na naprawie wielu rzeczy, które C ++ uzależnia od implementacji (takich jak rozmiary typów). Następnie środowisko, które rozumie przenośne pliki binarne, musi być dostępne w systemie, w którym ma zostać uruchomiony kod. Zezwalając na nieprzenośne pliki binarne, C i C ++ mogą wchodzić w miejsca, w których przenośne pliki binarne nie mogą, i dlatego standard nic nie mówi o plikach binarnych.
Następnie na dowolnej platformie implementacje zwykle nadal nie zapewniają binarnej zgodności między różnymi zestawami opcji, chociaż standard ich nie powstrzymuje. Jeśli Don Box nie podoba się, że kompilatory Microsoftu mogą tworzyć niekompatybilne pliki binarne z tego samego źródła, zgodnie z opcjami kompilatora, to na zespół kompilatorów musi narzekać. Język C ++ nie zabrania kompilatorowi ani systemowi operacyjnemu sprecyzowania wszystkich niezbędnych szczegółów, więc po ograniczeniu się do systemu Windows nie jest to podstawowy problem z C ++. Microsoft postanowił tego nie robić.
Różnice często objawiają się jako jeszcze jedna rzecz, w której możesz popełnić błąd i spowodować awarię programu, ale może wystąpić znaczny wzrost wydajności między, na przykład, niezgodną wersją debugowania a wersjami dll.
[*] Nie jestem pewien, kiedy ten pomysł został po raz pierwszy wymyślony, prawdopodobnie 1642 czy coś takiego, ale ich obecna popularność jest stosunkowo nowa, w porównaniu do czasu, kiedy C ++ podjął decyzje projektowe, które uniemożliwiają zdefiniowanie przenośności binarnej.
źródło
Kompatybilność między platformami i kompilatorami nie była głównym celem C i C ++. Urodzili się w epoce i są przeznaczone do celów, w których minimalizowanie czasu i przestrzeni dla platformy i kompilatora było kluczowe.
Z „Design and Evolution of C ++” Stroustrupa:
źródło
To nie jest błąd, to jest funkcja! Daje to implementatorom swobodę optymalizacji ich implementacji na poziomie binarnym. Little-endian i386 i jego potomstwo nie są jedynymi procesorami, które istnieją lub istnieją.
źródło
Problem opisany w cytacie jest spowodowany dość umyślnym unikaniem standaryzacji schematów manipulacji nazwami symboli (myślę, że „ standaryzacja na poziomie binarnym ” jest pod tym względem mylącym zwrotem, chociaż kwestia ta dotyczy interfejsu binarnego aplikacji kompilatora ( ABI).
C ++ koduje funkcję lub dane obiektu oraz informacje o typie i członkostwo w jego klasie / przestrzeni nazw w nazwie symbolu, a różne kompilatory mogą używać różnych schematów. W związku z tym symbol w bibliotece statycznej, bibliotece DLL lub pliku obiektowym nie będzie łączył się z kodem skompilowanym przy użyciu innego kompilatora (lub nawet innej wersji tego samego kompilatora).
Problem został opisany i wyjaśniony prawdopodobnie lepiej niż tutaj , z przykładami schematów używanych przez różne kompilatory.
Przyczyny zamierzonego braku standaryzacji są również wyjaśnione tutaj .
źródło
Celem ISO / ANSI była standaryzacja języka C ++, który wydaje się na tyle skomplikowany, że wymaga lat na aktualizację standardów językowych i obsługę kompilatora.
Kompatybilność binarna jest znacznie bardziej złożona, biorąc pod uwagę, że pliki binarne muszą działać na różnych architekturach procesorów i różnych środowiskach systemu operacyjnego.
źródło
Jak powiedział Andy, zgodność między platformami nie była wielkim celem, podczas gdy celem była szeroka implementacja platformy i sprzętu, w wyniku czego można napisać zgodne implementacje dla bardzo szerokiego wyboru systemów. Standaryzacja binarna sprawiłaby, że byłby to praktycznie nieosiągalny.
Kompatybilność z C była również ważna i znacznie ją skomplikowałaby.
Następnie podjęto pewne wysiłki w celu standaryzacji ABI dla podzbioru wdrożeń.
źródło
Myślę, że brak standardu dla C ++ jest problemem w dzisiejszym świecie oddzielnego, modułowego programowania. Musimy jednak zdefiniować, czego chcemy od takiego standardu.
Nikt przy zdrowych zmysłach nie chce zdefiniować implementacji lub platformy dla pliku binarnego. Więc nie możesz pobrać biblioteki DLL systemu Windows x86 i zacząć używać go na platformie Linux x86_64. To by było trochę za dużo.
Jednak to, czego ludzie chcą, to to samo, co my z modułami C - znormalizowany interfejs na poziomie binarnym (tj. Po skompilowaniu). Obecnie, jeśli chcesz załadować bibliotekę DLL w aplikacji modułowej, eksportujesz funkcje C i łączysz się z nimi w czasie wykonywania. Nie możesz tego zrobić z modułem C ++. Byłoby wspaniale, gdybyś mógł, co oznaczałoby również, że biblioteki DLL napisane przy użyciu jednego kompilatora mogą być ładowane przez inny. Oczywiście nadal nie będziesz mógł załadować biblioteki DLL zbudowanej dla niezgodnej platformy, ale nie jest to problem wymagający naprawy.
Gdyby więc organ normalizacyjny zdefiniował interfejs ujawniony przez moduł, mielibyśmy znacznie większą elastyczność w ładowaniu modułów C ++, nie musielibyśmy ujawniać kodu C ++ jako kodu C i prawdopodobnie mielibyśmy o wiele więcej zastosowań C ++ w językach skryptowych.
Nie musielibyśmy również cierpieć z powodu takich problemów, jak COM, które są próbą rozwiązania tego problemu.
źródło
Nie sądzę, że to takie proste. Dostarczone odpowiedzi już stanowią doskonałe uzasadnienie braku koncentracji na standaryzacji, ale C ++ może być zbyt bogaty w język, aby nadawał się do autentycznego konkurowania z C jako standardem ABI.
Możemy wejść w mangowanie nazw wynikające z przeciążenia funkcji, niezgodności vtable, niezgodności z wyjątkami, które przekraczają granice modułów itp. Wszystko to jest prawdziwym problemem i żałuję, że nie mogą przynajmniej znormalizować układów vtable.
Ale standard ABI nie polega tylko na tworzeniu dylibów C ++ wyprodukowanych w jednym kompilatorze, który może być używany przez inny plik binarny zbudowany przez inny kompilator. ABI jest używany w wielu językach . Byłoby miło, gdyby mogli przynajmniej pokryć pierwszą część, ale nie ma mowy, żeby C ++ kiedykolwiek naprawdę konkurował z C na poziomie uniwersalnego ABI, tak kluczowym dla tworzenia najbardziej kompatybilnych dylibów.
Wyobraź sobie prostą parę eksportowanych funkcji:
... i wyobraź sobie,
Foo
iBar
były klasy ze sparametryzowanymi konstruktorami, konstruktorami kopiującymi, konstruktorami ruchów i nietrywialnymi destruktorami.Następnie weźmy scenariusz Python / Lua / C # / Java / Haskell / etc. programista próbuje zaimportować ten moduł i użyć go w swoim języku.
Najpierw potrzebowalibyśmy standardu zmieniającego nazwy, aby wyeksportować symbole wykorzystujące przeciążenie funkcji. To jest łatwiejsza część. Jednak tak naprawdę nie powinna to być nazwa „mangling”. Ponieważ użytkownicy dylib muszą wyszukiwać symbole według nazwy, przeciążenia tutaj powinny prowadzić do nazw, które nie wyglądają jak kompletny bałagan. Może nazwy symboli mogą być podobne
"f_Foo"
"f_Bar_int"
lub coś w tym rodzaju. Musielibyśmy mieć pewność, że nie mogą kolidować z nazwą faktycznie zdefiniowaną przez programistę, być może rezerwując niektóre symbole / znaki / konwencje na użytek ABI.Ale teraz trudniejszy scenariusz. Jak na przykład programista Python wywołuje konstruktory przenoszenia, konstruktory kopiowania i destruktory? Może moglibyśmy je wyeksportować jako część dylib. Ale co, jeśli
Foo
iBar
są eksportowane w różnych modułach? Czy powinniśmy powielać symbole i implementacje związane z tą wersją, czy nie? Sugeruję, abyśmy zrobili, ponieważ może to być naprawdę irytujące bardzo szybko, w przeciwnym razie zacznie się zaplątać w wiele interfejsów dylib tylko po to, aby utworzyć tutaj obiekt, przekazać go tutaj, skopiować tutaj, zniszczyć tutaj. Podczas gdy ta sama podstawowa obawa może w pewnym stopniu dotyczyć C (tylko bardziej ręcznie / jawnie), C ma tendencję do unikania tego właśnie ze względu na sposób, w jaki ludzie to programują.To tylko mała próbka niezręczności. Co się stanie, gdy jedna z
f
powyższych funkcji wrzuciBazException
( a także klasę C ++ z konstruktorami i destruktorami i wyprowadzając std :: wyjątek) do JavaScript?W najlepszym razie myślę, że możemy jedynie mieć nadzieję na standaryzację ABI, który działa z jednego pliku binarnego produkowanego przez jeden kompilator C ++ na inny plik binarny produkowany przez inny. Oczywiście byłoby świetnie, ale chciałem tylko to podkreślić. Zazwyczaj towarzyszy temu obawa związana z rozpowszechnianiem uogólnionej biblioteki, która działa między kompilatorami, jest często chęć, aby była ona naprawdę uogólniona i kompatybilna z wieloma językami.
Sugerowane rozwiązanie
Moje zasugerowane rozwiązanie po wielu latach starań o znalezienie sposobów używania interfejsów C ++ dla interfejsów API / ABI z interfejsami typu COM to po prostu zostać programistą „pun / C ++”.
Użyj C, aby utworzyć te uniwersalne ABI, a C ++ do implementacji. Nadal możemy robić takie funkcje, jak funkcje eksportu, które zwracają wskaźniki do nieprzezroczystych klas C ++ z jawnymi funkcjami do tworzenia i niszczenia takich obiektów na stercie. Spróbuj zakochać się w estetyce C z perspektywy ABI, nawet jeśli do implementacji całkowicie używamy C ++. Abstrakcyjne interfejsy można modelować za pomocą tabel wskaźników funkcji. Pakowanie tego typu rzeczy do C API jest żmudne, ale korzyści i kompatybilność dostarczanej z nim dystrybucji sprawią, że będzie to bardzo opłacalne.
Jeśli więc nie lubimy bezpośrednio używać tego interfejsu (prawdopodobnie nie powinniśmy, przynajmniej z powodów RAII), możemy owinąć to wszystko, czego chcemy, w statycznie połączonej bibliotece C ++ dostarczanej z SDK. Klienci C ++ mogą z tego korzystać.
Klienci w języku Python nie będą chcieli korzystać bezpośrednio z interfejsu C lub C ++, ponieważ nie ma sposobu, aby zrobić z nich pythoniego. Będą chcieli zawinąć to w swoje własne interfejsy pytoniczne, więc tak naprawdę to dobrze, że eksportujemy tylko minimalne C API / ABI, aby było to tak proste, jak to możliwe.
Myślę, że dużo branży C ++ skorzystałoby na tym bardziej, niż uporczywie wysyłając interfejsy w stylu COM i tak dalej. Ułatwiłoby to również całe nasze życie, ponieważ użytkownicy tych dylibów nie musieliby się martwić o niezręczne ABI. C sprawia, że jest to proste, a jego prostota z perspektywy ABI pozwala nam tworzyć interfejsy API / ABI, które działają w sposób naturalny i minimalizujący dla wszystkich rodzajów FFI.
źródło
Nie wiem, dlaczego nie standaryzuje się na poziomie binarnym. Ale wiem, co z tym robię. W systemie Windows deklaruję funkcję zewnętrzną „C” BOOL WINAPI. (Oczywiście zastąp BOOL dowolnym rodzajem funkcji.) I są eksportowane w czysty sposób.
źródło
extern "C"
, użyje C ABI, który de facto jest standardem na zwykłym sprzęcie komputerowym, nawet jeśli nie jest narzucony przez żaden komitet.Użyj,
unzip foo.zip && make foo.exe && foo.exe
jeśli chcesz przenosić swoje źródło.źródło