C ++: Brak standaryzacji na poziomie binarnym

14

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?

Nawaz
źródło
Czy lepiej zadać to na stronie programmers.stackexchange.com , widząc, że jest to bardziej subiektywne pytanie?
Stephen Furlani,
1
Moje
4
Don Box jest fanatykiem. Zignoruj ​​go.
John Dibling,
8
Cóż, C nie jest standaryzowane przez ANSI / ISO na poziomie binarnym; OTOH C ma de facto standardowy ABI, a nie de jure . C ++ nie ma tak znormalizowanego ABI, ponieważ różni producenci mieli różne cele w swoich implementacjach. Na przykład wyjątki w VC ++ na barana na Windows SEH. POSIX nie ma SEH i dlatego przyjęcie tego modelu nie miałoby sensu (więc G ++ i MinGW nie używają tego modelu).
Billy ONeal,
3
Widzę to jako cechę, a nie słabość. Jeśli powiążesz implementację z konkretnym ABI, nigdy nie będziemy mieć innowacji, a nowy sprzęt będzie związany z projektowaniem języka (a ponieważ między każdą nową wersją jest 15 lat, co jest długim okresem w branży sprzętowej) i przez duszenie nie będą wprowadzane innowacyjne pomysły, aby kod działał wydajniej. Cena jest taka, że ​​cały kod w pliku wykonywalnym musi być zbudowany przez ten sam kompilator / wersję (problem, ale nie poważny).

Odpowiedzi:

16

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.

Steve Jessop
źródło
@ Steve Ale C ma dobrze zdefiniowany ABI na i386 i AMD64, więc mogę przekazać wskaźnik do funkcji skompilowanej przez GCC w wersji X do funkcji skompilowanej przez MSVC w wersji Y. Zrobienie tego z funkcją C ++ jest niemożliwe.
user877329,
7

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:

„Wyraźnym celem było dopasowanie C pod względem czasu działania, zwięzłości kodu i zwięzłości danych. ... Ideałem - który został osiągnięty - było to, że C z klasami można było zastosować do dowolnego C.”

Andy Thomas
źródło
1
+1 - dokładnie. Jak zbudować standardowy ABI, który działałby zarówno na urządzeniach ARM, jak i Intel? To nie miałoby sensu!
Billy ONeal,
1
niestety to się nie udało. Możesz robić wszystko, co robi C ... z wyjątkiem dynamicznego ładowania modułu C ++ w czasie wykonywania. musisz „powrócić” do korzystania z funkcji C w otwartym interfejsie.
gbjbaanb
6

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
6

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 .

Clifford
źródło
3

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
To prawda, ale problem opisany w cytacie w rzeczywistości nie ma nic wspólnego z „kompatybilnością na poziomie binarnym” (pomimo użycia tego terminu przez autora) w jakimkolwiek sensie innym niż takie rzeczy są zdefiniowane w czymś zwanym „binarnym interfejsem aplikacji”. W rzeczywistości opisuje problem niekompatybilnych schematów manglingu nazw.
@Clifford: schemat zmiany nazwy jest tylko podzbiorem zgodności na poziomie binarnym. ten drugi jest bardziej terminem parasolowym!
Nawaz
Wątpię, aby wystąpił problem z próbą uruchomienia pliku binarnego systemu Linux na komputerze z systemem Windows. Byłoby o wiele lepiej, gdyby istniał ABI na platformę, ponieważ przynajmniej wtedy język skryptowy mógłby dynamicznie ładować i uruchamiać plik binarny na tej samej platformie lub aplikacje mogły korzystać z komponentów zbudowanych z innego kompilatora. Nie możesz dziś używać dll C na Linuksie i nikt nie narzeka, ale dll C może być nadal ładowany przez aplikację python, w której gromadzi się korzyść.
gbjbaanb
2

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ń.

Flexo
źródło
Cholera, zapomniałem kompatybilności z C. Dobra uwaga, +1!
Andy Thomas
1

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.

gbjbaanb
źródło
1
+1. Tak! Zgadzam się. Inne odpowiedzi tutaj w zasadzie rozwiązują problem, mówiąc, że binarna standaryzacja zabraniałaby optymalizacji specyficznych dla architektury. Ale nie o to chodzi. Nikt nie popiera jakiegoś wieloplatformowego binarnego formatu wykonywalnego. Problem polega na tym, że nie ma standardowego interfejsu do dynamicznego ładowania modułów C ++.
Charles Salvia
1

Istnieje wiele problemów z przenośnością w C ++, co wynika tylko z braku jego standaryzacji na poziomie binarnym.

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:

void f(Foo foo);
void f(Bar bar, int val);

... i wyobraź sobie, Fooi Barbył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 Fooi Barsą 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 fpowyższych funkcji wrzuci BazException( 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
1
„Użyj C, aby utworzyć te uniwersalne ABI, z C ++ do implementacji.” ... robię to samo, jak wiele innych!
Nawaz
-1

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.

Mike Jones
źródło
2
Ale jeśli to zadeklarujesz 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.
Billy ONeal,
-3

Użyj, unzip foo.zip && make foo.exe && foo.exejeśli chcesz przenosić swoje źródło.

Sjoerd
źródło