Dlaczego nie można zmienić nazwy funkcji w C?


136

Niedawno przeprowadziłem wywiad i zadałem jedno pytanie, jakie jest zastosowanie extern "C"w kodzie C ++. Odpowiedziałem, że jest to użycie funkcji C w kodzie C ++, ponieważ C nie używa zniekształcania nazw. Zapytano mnie, dlaczego C nie używa przekłamywania nazwisk i szczerze mówiąc, nie mogłem odpowiedzieć.

Rozumiem, że gdy kompilator C ++ kompiluje funkcje, nadaje funkcji specjalną nazwę, głównie dlatego, że możemy mieć przeciążone funkcje o tej samej nazwie w C ++, które muszą zostać rozwiązane w czasie kompilacji. W języku C nazwa funkcji pozostanie taka sama, a może z _ przed nią.

Moje zapytanie brzmi: co jest złego w zezwalaniu kompilatorowi C ++ na manipulowanie funkcjami C również? Przypuszczałbym, że nie ma znaczenia, jakie nazwy nada im kompilator. Funkcje wywołujemy w ten sam sposób w C i C ++.


75
C nie musi zmieniać nazw, ponieważ nie ma przeciążenia funkcji.
EOF

9
Jak połączyć biblioteki C z kodem C ++, jeśli kompilator C ++ zmienia nazwy funkcji?
Mat

6
„Odpowiedziałem, że jest to użycie funkcji C w kodzie C ++, ponieważ C nie używa zniekształcania nazw”. - Myślę, że jest odwrotnie. Extern "C" sprawia, że ​​funkcje C ++ są użyteczne w kompilatorze C. źródło
rozina

3
@ Engineer999: A jeśli skompilujesz podzbiór C, który jest również C ++, za pomocą kompilatora C ++, nazwy funkcji rzeczywiście zostaną zniekształcone. Ale jeśli chcesz mieć możliwość łączenia plików binarnych utworzonych za pomocą różnych kompilatorów, nie chcesz zmieniać nazw.
EOF

13
C zmienia nazwy. Zwykle zniekształcona nazwa to nazwa funkcji poprzedzona podkreśleniem. Czasami jest to nazwa funkcji, po której następuje podkreślenie. extern "C"mówi, aby zmienić nazwę w taki sam sposób, jak zrobiłby to „” kompilator C.
Pete Becker,

Odpowiedzi:


187

Odpowiedzi udzielono powyżej, ale spróbuję umieścić rzeczy w kontekście.

Najpierw C. był pierwszy. W związku z tym to, co robi C, jest niejako „domyślne”. Nie zmienia nazw, bo po prostu tego nie robi. Nazwa funkcji to nazwa funkcji. Globalny jest globalny i tak dalej.

Potem pojawił się C ++. C ++ chciał móc używać tego samego linkera co C i móc łączyć się z kodem napisanym w C. Ale C ++ nie mógł pozostawić „zniekształcenia” C (lub jego braku) takim, jakim jest. Sprawdź następujący przykład:

int function(int a);
int function();

W C ++ są to odrębne funkcje z różnymi treściami. Jeśli żaden z nich nie zostanie zniekształcony, oba będą nazywane „funkcją” (lub „_funkcją”), a linker będzie narzekał na przedefiniowanie symbolu. Rozwiązanie C ++ polegało na zmianie typów argumentów na nazwę funkcji. Tak więc, jeden jest wywoływany, _function_inta drugi jest wywoływany _function_void(nie jest to rzeczywisty schemat zniekształcenia), co pozwala uniknąć kolizji.

Teraz mamy problem. Jeśli int function(int a)został zdefiniowany w module C, a my po prostu pobieramy jego nagłówek (tj. Deklarację) w kodzie C ++ i używamy go, kompilator wygeneruje instrukcję dla konsolidatora do zaimportowania _function_int. Kiedy funkcja została zdefiniowana, w module C nie była tak nazywana. Nazywało się _function. Spowoduje to błąd konsolidatora.

Aby uniknąć tego błędu, podczas deklarowania funkcji mówimy kompilatorowi, że jest to funkcja zaprojektowana do łączenia lub kompilowania przez kompilator C:

extern "C" int function(int a);

Kompilator C ++ wie teraz, że _functionzamiast importować _function_int, i wszystko jest w porządku.


1
@ShacharShamesh: Pytałem o to gdzie indziej, ale co z linkowaniem w skompilowanych bibliotekach C ++? Kiedy kompilator przechodzi krok po kroku i kompiluje mój kod, który wywołuje jedną z funkcji w skompilowanej bibliotece C ++, skąd wie, jaką nazwę zmienić lub nadać funkcji, widząc tylko jej deklarację lub wywołanie funkcji? Skąd wiedzieć, że tam, gdzie jest zdefiniowane, nazwa jest zniekształcona na coś innego? Więc musi istnieć standardowa metoda manipulowania nazwami w C ++?
Inżynier

2
Każdy kompilator robi to na swój własny sposób. Jeśli kompilujesz wszystko tym samym kompilatorem, nie ma to znaczenia. Ale jeśli spróbujesz użyć, powiedzmy, biblioteki skompilowanej za pomocą kompilatora firmy Borland, z programu, który tworzysz kompilatorem Microsoftu, cóż ... powodzenia; będziesz go potrzebować :)
Mark VY

6
@ Engineer999 Czy zastanawiałeś się kiedyś, dlaczego nie ma czegoś takiego jak przenośne biblioteki C ++, ale albo dokładnie określają, jakiej wersji (i flag) kompilatora (i biblioteki standardowej) masz użyć, albo po prostu eksportują C API? Proszę bardzo. C ++ jest najmniej przenośnym językiem, jaki kiedykolwiek wynaleziono, podczas gdy C jest dokładnie odwrotnym. Są w tym względzie wysiłki, ale na razie, jeśli chcesz czegoś naprawdę przenośnego, trzymaj się C.
Voo

1
@Voo Cóż, w teorii powinieneś być w stanie napisać przenośny kod, stosując się np. Do standardu -std=c++11i unikać używania czegokolwiek spoza standardu. To to samo, co deklarowanie wersji Javy (chociaż nowsze wersje Javy są wstecznie kompatybilne). Nie jest to wina standardów, które ludzie używają rozszerzeń specyficznych dla kompilatora i kodu zależnego od platformy. Z drugiej strony nie można ich za to winić, ponieważ w standardzie brakuje wielu rzeczy (szczególnie IO, takich jak gniazda). Wydaje się, że komisja powoli to dogania. Popraw mnie, jeśli coś przeoczyłem.
mucaho

14
@mucaho: mówisz o przenośności / zgodności źródła. czyli API. Voo mówi o kompatybilności binarnej bez ponownej kompilacji. Wymaga to kompatybilności ABI . Kompilatory C ++ regularnie zmieniają swoje ABI między wersjami. (np. g ++ nie próbuje nawet mieć stabilnego ABI. Zakładam, że nie łamią ABI tylko dla zabawy, ale nie unikają zmian, które wymagają zmiany ABI, gdy jest coś do zdobycia i nie ma innego dobrego sposobu aby to zrobić.).
Peter Cordes

45

Nie chodzi o to, że „nie mogą” , w ogóle nie są .

Jeśli chcesz wywołać funkcję w wywoływanej bibliotece C foo(int x, const char *y), nie jest dobrze pozwalać kompilatorowi C ++ zmienić ją na foo_I_cCP()(lub cokolwiek, po prostu wymyślił schemat zniekształcenia na miejscu) tylko dlatego, że może.

Ta nazwa nie zostanie rozwiązana, funkcja jest w C, a jej nazwa nie zależy od listy typów argumentów. Dlatego kompilator C ++ musi to wiedzieć i oznaczyć tę funkcję jako C, aby uniknąć zniekształcenia.

Pamiętaj, że wspomniana funkcja C może znajdować się w bibliotece, której kodu źródłowego nie masz, wszystko, co masz, to wstępnie skompilowany plik binarny i nagłówek. Więc Twój kompilator C ++ nie może zrobić „swojej własnej rzeczy”, nie może w końcu zmienić tego, co jest w bibliotece.


To jest ta część, której mi brakuje. Dlaczego kompilator C ++ miałby zmieniać nazwę funkcji, gdy widzi jej deklarację lub widzi ją wywoływaną. Czy nie zmienia po prostu nazw funkcji, gdy widzi ich implementację? Miałoby to dla mnie większy sens
Inżynier

13
@ Engineer999: Jak możesz mieć jedną nazwę dla definicji, a drugą dla deklaracji? „Jest funkcja o nazwie Brian, którą możesz wywołać”. - Okej, zadzwonię do Briana. „Przepraszamy, nie ma funkcji o nazwie Brian”. Okazuje się, że nazywa się Graham.
Wyścigi lekkości na orbicie

A co z linkowaniem w skompilowanych bibliotekach C ++? Kiedy kompilator przechodzi krok po kroku i kompiluje nasz kod, który wywołuje jedną z funkcji w skompilowanej bibliotece C ++, skąd wie, jaką nazwę zmienić lub nadać funkcji, widząc tylko jej deklarację lub wywołanie funkcji?
Inżynier

1
@ Engineer999 Obaj muszą zgodzić się na to samo zniekształcenie. Widzą więc plik nagłówkowy (pamiętaj, że w natywnych bibliotekach DLL jest bardzo mało metadanych - nagłówki są tymi metadanymi) i mówią „Ach, tak, Brian naprawdę powinien być Grahamem”. Jeśli to nie zadziała (np. Z dwoma niekompatybilnymi schematami zniekształcania), nie otrzymasz poprawnego linku, a Twoja aplikacja zawiedzie. C ++ ma wiele takich niezgodności. W praktyce musisz wtedy jawnie użyć zniekształconej nazwy i wyłączyć zniekształcanie po swojej stronie (np. Nakazujesz swojemu kodowi wykonanie Grahama, a nie Briana). W rzeczywistej praktyce ... extern "C":)
Luaan

1
@ Engineer999 Mogę się mylić, ale czy może masz doświadczenie z językami takimi jak Visual Basic, C # lub Java (a nawet w pewnym stopniu Pascal / Delphi)? Dzięki nim interop wydaje się niezwykle prosty. W C, a zwłaszcza w C ++, to nic innego. Istnieje wiele konwencji wywoływania, których musisz przestrzegać, musisz wiedzieć, kto jest odpowiedzialny za jaką pamięć, i musisz mieć pliki nagłówkowe, które informują o deklaracjach funkcji, ponieważ same biblioteki DLL nie zawierają wystarczającej ilości informacji - szczególnie w przypadku czyste C. Jeśli nie masz pliku nagłówkowego, zazwyczaj musisz zdekompilować bibliotekę DLL, aby jej użyć.
Luaan,

32

co jest złego w zezwalaniu kompilatorowi C ++ na modyfikowanie funkcji języka C?

Nie byłyby już funkcjami C.

Funkcja to nie tylko podpis i definicja; sposób działania funkcji zależy w dużej mierze od czynników, takich jak konwencja wywoływania. „Interfejs binarny aplikacji” przeznaczony do użycia na Twojej platformie opisuje, w jaki sposób systemy komunikują się ze sobą. ABI języka C ++ używany przez system określa schemat zniekształcania nazw, dzięki czemu programy w tym systemie wiedzą, jak wywoływać funkcje w bibliotekach i tak dalej. (Świetny przykład można znaleźć w C ++ Itanium ABI. Szybko przekonasz się, dlaczego jest to konieczne).

To samo dotyczy C ABI w twoim systemie. Niektóre C ABI faktycznie mają schemat zniekształcania nazw (np. Visual Studio), więc nie chodzi tu o „wyłączanie zniekształcania nazw”, a więcej o przełączanie się z C ++ ABI na C ABI dla niektórych funkcji. Oznaczamy funkcje C jako funkcje C, do których odnosi się C ABI (zamiast C ++ ABI). Deklaracja musi pasować do definicji (czy to w tym samym projekcie, czy w jakiejś zewnętrznej bibliotece), w przeciwnym razie deklaracja nie ma sensu. Bez tego twój system po prostu nie będzie wiedział, jak zlokalizować / wywołać te funkcje.

Jeśli chodzi o to, dlaczego platformy nie definiują interfejsów ABI C i C ++ jako takich samych i pozbywają się tego „problemu”, jest to częściowo historyczne - oryginalne ABI C nie były wystarczające dla C ++, który ma przestrzenie nazw, klasy i przeciążenie operatorów, wszystko które muszą być w jakiś sposób przedstawione w nazwie symbolu w sposób przyjazny dla komputera - ale można również argumentować, że dostosowywanie programów C teraz do C ++ jest niesprawiedliwe dla społeczności C, która musiałaby znosić znacznie bardziej skomplikowane ABI tylko ze względu na innych ludzi, którzy chcą interoperacyjności.


2
+int(PI/3), ale z jednym przymrużeniem oka: byłbym bardzo ostrożny, mówiąc o „C ++ ABI” ... AFAIK, są próby zdefiniowania C ++ ABI, ale nie ma rzeczywistych standardów de facto / de iure - jak isocpp.org/files /papers/n4028.pdf stwierdza (i całkowicie się z tym zgadzam), cytuję, to głęboko ironiczne, że C ++ faktycznie zawsze wspierał sposób publikowania API ze stabilnym binarnym ABI - uciekając się do podzbioru C C ++ poprzez zewnętrzne „C ”. . C++ Itanium ABIjest tylko to - trochę C ++ ABI dla Itanium ... jak omówiono na stackoverflow.com/questions/7492180/c-abi-issues-list

3
@vaxquis: Tak, nie „ABI w C ++”, ale „ABI w C ++” w taki sam sposób, w jaki mam „klucz domu”, który nie działa w każdym domu. Chyba mogłoby to być jaśniejsze, chociaż starałem się, aby było to tak jasne, jak to tylko możliwe, zaczynając od frazy „C ++ ABI używany przez Twój system . Zrezygnowałem z doprecyzowania w późniejszych wypowiedziach dla zwięzłości, ale zaakceptuję edycję, która zmniejszy tutaj zamieszanie!
Wyścigi lekkości na orbicie

1
AIUI C abi były zwykle własnością platformy, podczas gdy C ++ ABI były zwykle własnością pojedynczego kompilatora, a często nawet własnością pojedynczej wersji kompilatora. Więc jeśli chciałeś łączyć moduły zbudowane z narzędziami różnych dostawców, musiałeś użyć C abi jako interfejsu.
plugwash

Stwierdzenie „funkcje o zniekształconej nazwie nie byłyby już funkcjami C” jest przesadzone - jest całkowicie możliwe wywołanie funkcji o zniekształconej nazwie ze zwykłego C, jeśli zniekształcona nazwa jest znana. To, że zmiana nazwy nie czyni jej mniej związaną z C ABI, tj. Nie czyni z niej mniej funkcji C. Odwrotna sytuacja
Peter - Przywróć Monikę

@ PeterA.Schneider: Tak, nagłówek jest przesadzony. Cała reszta odpowiedź zawiera istotnych szczegółów faktycznych.
Wyścigi lekkości na orbicie

21

W rzeczywistości MSVC zmienia nazwy w C, chociaż w prosty sposób. Czasami dołącza @4lub inna mała liczba. Dotyczy to konwencji wywoływania i potrzeby czyszczenia stosu.

Więc założenie jest po prostu błędne.


2
To nie jest tak naprawdę zniekształcone imię. Jest to po prostu konwencja nazewnictwa (lub nadawania nazw) specyficzna dla dostawcy, aby zapobiec problemom z plikami wykonywalnymi, które są łączone z bibliotekami DLL zbudowanymi za pomocą funkcji mających różne konwencje wywoływania.
Peter,

2
A co z dodawaniem na początku _?
OrangeDog,

12
@Peter: Dosłownie to samo.
Wyścigi lekkości na orbicie

5
@Frankie_C: „Caller czyści stos” nie jest określony przez żaden standard C: żadna z konwencji wywoływania nie jest bardziej standardowa niż druga z punktu widzenia języka.
Ben Voigt,

2
Z punktu widzenia MSVC wybierasz właśnie „standardową konwencję wywoływania” /Gd, /Gr, /Gv, /Gz. (Oznacza to, że używana jest standardowa konwencja wywoływania, chyba że deklaracja funkcji wyraźnie określa konwencję wywoływania). Zastanawiasz się, __cdeclktóra jest domyślną standardową konwencją wywoływania.
MSalters

13

Bardzo często istnieją programy, które są częściowo napisane w C, a częściowo napisane w innym języku (często w języku asemblerowym, ale czasami w Pascalu, FORTRAN lub w czymś innym). Często zdarza się, że programy zawierają różne komponenty napisane przez różne osoby, które mogą nie mieć kodu źródłowego wszystkiego.

Na większości platform istnieje specyfikacja - często nazywana ABI [Application Binary Interface], która opisuje, co kompilator musi zrobić, aby utworzyć funkcję o określonej nazwie, która przyjmuje argumenty określonego typu i zwraca wartość określonego typu. W niektórych przypadkach ABI może zdefiniować więcej niż jedną „konwencję wywoływania”; kompilatory dla takich systemów często zapewniają środki wskazujące, która konwencja wywoływania powinna być używana dla określonej funkcji. Na przykład na komputerach Macintosh większość procedur Toolbox używa konwencji wywoływania Pascal, więc prototyp czegoś takiego jak „LineTo” wyglądałby tak:

/* Note that there are no underscores before the "pascal" keyword because
   the Toolbox was written in the early 1980s, before the Standard and its
   underscore convention were published */
pascal void LineTo(short x, short y);

Gdyby cały kod w projekcie został skompilowany przy użyciu tego samego kompilatora, nie miałoby znaczenia, jaką nazwę kompilator wyeksportował dla każdej funkcji, ale w wielu sytuacjach konieczne będzie, aby kod C wywoływał funkcje, które zostały skompilowane przy użyciu innych narzędzi i nie może być ponownie skompilowany za pomocą obecnego kompilatora [i może nawet nie być w C]. Możliwość zdefiniowania nazwy konsolidatora jest zatem krytyczna dla korzystania z takich funkcji.


Tak, to jest odpowiedź. Jeśli jest to tylko C i C ++, trudno jest zrozumieć, dlaczego tak się dzieje. Aby to zrozumieć, musimy umieścić rzeczy w kontekście starego sposobu łączenia statycznego. Łączenie statyczne wydaje się prymitywne dla programistów Windows, ale jest to główny powód, dla którego C nie może zmieniać nazw.
user34660

2
@ user34660: Nie qutie. Jest to powód, dla którego C nie może wymagać istnienia funkcji, których implementacja wymagałaby zniekształcenia nazw, które można eksportować, lub dopuszczenia do istnienia wielu symboli o podobnych nazwach, które wyróżniają się drugorzędnymi cechami.
supercat

czy wiemy, że były próby „narzucenia” takich rzeczy lub że takie rzeczy były rozszerzeniami dostępnymi dla C przed C ++?
user34660

@ user34660: Re "Statyczne linkowanie wydaje się prymitywne dla programistów Windows ...", ale dynamiczne linkowanie czasami wydaje się być głównym PITA dla ludzi używających Linuksa, gdy instalowanie programu X (prawdopodobnie napisanego w C ++) oznacza konieczność wyśledzenia i zainstalowania poszczególnych wersji bibliotek, których masz już różne wersje w swoim systemie.
jamesqf

@jamesqf, tak, Unix nie miał dynamicznego łączenia przed Windows. Wiem bardzo mało o dynamicznym łączeniu w systemie Unix / Linux, ale wygląda na to, że nie jest tak płynne, jak mogłoby być ogólnie w systemie operacyjnym.
user34660

12

Dodam jeszcze jedną odpowiedź, aby odnieść się do niektórych tangencjalnych dyskusji, które miały miejsce.

C ABI (interfejs binarny aplikacji) pierwotnie wywoływany do przekazywania argumentów na stosie w odwrotnej kolejności (tj. - wypychany od prawej do lewej), gdzie wywołujący również zwalnia pamięć stosu. Współczesny ABI faktycznie używa rejestrów do przekazywania argumentów, ale wiele z zagadnień związanych z zniekształcaniem wraca do tego oryginalnego przekazywania argumentów stosu.

W przeciwieństwie do tego oryginalny Pascal ABI przesuwał argumenty od lewej do prawej, a wywoływany musiał przełamać argumenty. Oryginalny C ABI jest lepszy od oryginalnego Pascala ABI w dwóch ważnych punktach. Kolejność wypychania argumentów oznacza, że ​​przesunięcie stosu pierwszego argumentu jest zawsze znane, co pozwala funkcjom, które mają nieznaną liczbę argumentów, gdzie wczesne argumenty kontrolują liczbę innych argumentów (ala printf).

Drugim sposobem przewodzenia C ABI jest zachowanie w przypadku, gdy dzwoniący i wywoływany nie zgadzają się co do liczby argumentów. W przypadku C, o ile nie masz dostępu do argumentów poza ostatnim, nic złego się nie dzieje. W Pascalu niewłaściwa liczba argumentów jest usuwana ze stosu i cały stos jest uszkodzony.

Oryginalny system Windows 3.1 ABI był oparty na Pascalu. Jako taki, używał Pascal ABI (argumenty w kolejności od lewej do prawej, callee pops). Ponieważ każda niezgodność w liczbie argumentów może prowadzić do uszkodzenia stosu, powstał schemat zniekształcenia. Każda nazwa funkcji została zniekształcona liczbą wskazującą rozmiar w bajtach jej argumentów. Tak więc na komputerze 16-bitowym następująca funkcja (składnia C):

int function(int a)

Został zniekształcony function@2, ponieważint ma dwa bajty. Zrobiono to tak, że jeśli deklaracja i definicja są niezgodne, konsolidator nie znajdzie funkcji, a nie uszkodzi stosu w czasie wykonywania. I odwrotnie, jeśli program łączy się, możesz być pewien, że poprawna liczba bajtów jest pobierana ze stosu na końcu wywołania.

32-bitowy system Windows i nowsze wersje używają rozszerzenia stdcall zamiast tego ABI. Jest podobny do Pascala ABI, z wyjątkiem tego, że kolejność push jest podobna do C, od prawej do lewej. Podobnie jak w przypadku Pascala ABI, zniekształcanie nazwy zmienia rozmiar bajtów argumentów na nazwę funkcji, aby uniknąć uszkodzenia stosu.

W przeciwieństwie do oświadczeń przedstawionych w innym miejscu tutaj, C ABI nie zmienia nazw funkcji, nawet w programie Visual Studio. I odwrotnie, funkcje zniekształcające udekorowane stdcallspecyfikacją ABI nie są unikalne dla VS. GCC obsługuje również ten ABI, nawet podczas kompilacji dla Linuksa. Jest to szeroko używane przez Wine , który używa własnego modułu ładującego, aby umożliwić łączenie w czasie wykonywania skompilowanych plików binarnych Linuksa ze skompilowanymi bibliotekami DLL systemu Windows.


9

Kompilatory C ++ używają zniekształcania nazw, aby umożliwić unikalne nazwy symboli dla przeciążonych funkcji, których sygnatura byłaby taka sama. Zasadniczo koduje również typy argumentów, co pozwala na polimorfizm na poziomie funkcji.

C nie wymaga tego, ponieważ nie pozwala na przeciążenie funkcji.

Zauważ, że zniekształcanie nazw jest jednym (ale z pewnością nie jedynym!) Powodem, dla którego nie można polegać na 'C ++ ABI'.


8

C ++ chce mieć możliwość współdziałania z kodem C, który łączy się z nim lub z którym łączy się.

C oczekuje nazw funkcji o niezmienionych nazwach.

Jeśli C ++ to zniekształci, nie znajdzie wyeksportowanych niezmienionych funkcji z C lub C nie znajdzie funkcji wyeksportowanych z C ++. Konsolidator C musi otrzymać nazwę, której sam oczekuje, ponieważ nie wie, że pochodzi z C ++ lub do niego trafia.


3

Zmiana nazw funkcji i zmiennych języka C umożliwiłaby sprawdzenie ich typów w czasie łączenia. Obecnie wszystkie implementacje (?) C pozwalają na zdefiniowanie zmiennej w jednym pliku i wywołanie jej jako funkcji w innym. Lub możesz zadeklarować funkcję z nieprawidłowym podpisem (np. void fopen(double)A następnie ją wywołać.

Zaproponowałem schemat bezpiecznego dla typu łączenia zmiennych C i funkcji poprzez użycie zniekształcenia w 1991 roku. Schemat nigdy nie został przyjęty, ponieważ, jak zauważyli inni tutaj, zniszczyłoby to kompatybilność wsteczną.


1
Masz na myśli „zezwalaj na sprawdzanie ich typów w czasie łączenia ”. Typy sprawdzane w czasie kompilacji, ale połączenie z niezarządzanymi nazwami nie może sprawdzić, czy deklaracje używane w różnych jednostkach kompilacji są zgodne. A jeśli się nie zgadzają, to Twój system kompilacji jest zasadniczo uszkodzony i należy go naprawić.
cmaster
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.