Czytałem, że konwersja wskaźnika funkcji na wskaźnik danych i odwrotnie działa na większości platform, ale nie gwarantuje, że zadziała. Dlaczego tak się dzieje? Czy oba nie powinny być po prostu adresami do pamięci głównej, a zatem powinny być kompatybilne?
c++
c
pointers
function-pointers
geksycyd
źródło
źródło
void
. Konwersja wskaźnika funkcji navoid *
nie zmienia reprezentacji.void *
Wartość wynikająca z takiego przekształcenia można przekształcić z powrotem do pierwotnego typu wskaźnik funkcji, za pomocą wyraźnego obsady, bez utraty informacji. Uwaga : Standard ISO C tego nie wymaga, ale jest to wymagane w celu zachowania zgodności z POSIX.dlsym()
- zwróć uwagę na koniec sekcji „Użycie aplikacji”, w której jest napisane: Zauważ, że konwersja zevoid *
wskaźnika do wskaźnika funkcji, jak w:fptr = (int (*)(int))dlsym(handle, "my_function");
nie jest zdefiniowana w standardzie ISO C. Ten standard wymaga tej konwersji, aby działała poprawnie w zgodnych implementacjach.Odpowiedzi:
Architektura nie musi przechowywać kodu i danych w tej samej pamięci. W architekturze Harvardu kod i dane są przechowywane w zupełnie innej pamięci. Większość architektur to architektury Von Neumanna z kodem i danymi w tej samej pamięci, ale C nie ogranicza się tylko do pewnych typów architektur, jeśli to w ogóle możliwe.
źródło
CS != DS
).VirtualProtect
, co pozwala oznaczyć obszary danych jako wykonywalne.Niektóre komputery mają (miały) oddzielne przestrzenie adresowe dla kodu i danych. Na takim sprzęcie to po prostu nie działa.
Język jest przeznaczony nie tylko dla obecnych aplikacji desktopowych, ale także w celu umożliwienia implementacji na dużym zestawie sprzętu.
Wygląda na to, że komitet języka C nigdy nie zamierzał
void*
być wskaźnikiem do działania, po prostu chciał mieć ogólny wskaźnik do obiektów.Uzasadnienie C99 mówi:
Uwaga W ostatnim akapicie nic nie jest powiedziane o wskaźnikach do funkcji . Mogą różnić się od innych wskazówek, a komisja jest tego świadoma.
źródło
void *
.sizeof(void*) == sizeof( void(*)() )
marnuje miejsce w przypadku, gdy wskaźniki funkcji i wskaźniki danych mają różne rozmiary. Był to częsty przypadek w latach 80-tych, kiedy napisano pierwszy standard C.Dla tych, którzy pamiętają MS-DOS, Windows 3.1 i starsze, odpowiedź jest dość prosta. Wszystkie z nich były używane do obsługi kilku różnych modeli pamięci, z różnymi kombinacjami charakterystyk kodu i wskaźników danych.
Na przykład dla modelu Compact (mały kod, duże dane):
sizeof(void *) > sizeof(void(*)())
i odwrotnie w modelu Medium (duży kod, małe dane):
sizeof(void *) < sizeof(void(*)())
W tym przypadku nie miałeś oddzielnego miejsca na kod i datę, ale nadal nie mogłeś konwertować między dwoma wskaźnikami (bez użycia niestandardowych modyfikatorów __near i __far).
Dodatkowo nie ma gwarancji, że nawet jeśli wskaźniki są tego samego rozmiaru, wskazują na to samo - w modelu DOS Small memory zarówno kod, jak i dane używane w pobliżu wskaźników, ale wskazywały na różne segmenty. Zatem konwersja wskaźnika funkcji na wskaźnik danych nie dałaby wskaźnika, który w ogóle miałby związek z funkcją, a zatem taka konwersja nie miała sensu.
źródło
int*
na avoid*
daje wskaźnik, z którym tak naprawdę nie można nic zrobić, ale nadal przydatna jest możliwość wykonania konwersji. (Dzieje się tak, ponieważvoid*
może przechowywać dowolny wskaźnik obiektu, więc może być używany do ogólnych algorytmów, które nie muszą wiedzieć, jaki typ przechowują. To samo mogłoby być przydatne również dla wskaźników funkcji, gdyby było to dozwolone).int *
dovoid *
,void *
gwarantujemy, że przynajmniej wskaże ten sam obiekt, co oryginałint *
- więc jest to przydatne w przypadku ogólnych algorytmów, które mają dostęp do wskazanego obiektu, npint n; memcpy(&n, src, sizeof n);
. W przypadku, gdy konwersja wskaźnika funkcji na avoid *
nie daje wskaźnika wskazującego na funkcję, nie jest to przydatne dla takich algorytmów - jedyną rzeczą, którą możesz zrobić, jestvoid *
ponowne przekonwertowanie wskaźnika wstecznego na wskaźnik funkcji, więc możesz tak po prostu użyj wskaźnikaunion
zawierającego avoid *
i.void*
sam punkt do funkcji, przypuszczam, że będzie to zły pomysł dla ludzi, aby przekazać jąmemcpy
. :-Pvoid
. Konwersja wskaźnika funkcji navoid *
nie zmienia reprezentacji.void *
Wartość wynikająca z takiego przekształcenia można przekształcić z powrotem do pierwotnego typu wskaźnik funkcji, za pomocą wyraźnego obsady, bez utraty informacji. Uwaga : Norma ISO C tego nie wymaga, ale jest wymagana w celu zachowania zgodności z POSIX.Wskaźniki do void mają być w stanie pomieścić wskaźnik do dowolnego rodzaju danych - ale niekoniecznie wskaźnik do funkcji. Niektóre systemy mają inne wymagania dotyczące wskaźników do funkcji niż wskaźniki do danych (np. Istnieją procesory DSP o różnym adresowaniu danych w porównaniu z kodem, model medium w systemie MS-DOS wykorzystywał 32-bitowe wskaźniki do kodu, ale tylko 16-bitowe wskaźniki do danych) .
źródło
Oprócz tego, co zostało już tutaj powiedziane, warto przyjrzeć się POSIX
dlsym()
:źródło
void*
iz powrotem.void*
zgodności ze wskaźnikiem funkcji, podczas gdy POSIX tak.C ++ 11 ma rozwiązanie długotrwałej niezgodności między C / C ++ i POSIX w odniesieniu do
dlsym()
. Można użyćreinterpret_cast
do konwersji wskaźnika funkcji na / ze wskaźnika danych, o ile implementacja obsługuje tę funkcję.Ze standardu 5.2.10 ust. 8, „konwersja wskaźnika funkcji na typ wskaźnika obiektu lub odwrotnie jest obsługiwana warunkowo”. 1.3.5 definiuje „warunkowo obsługiwane” jako „konstrukcję programu, której obsługa nie jest wymagana”.
źródło
-Werror
.). Lepszym (i nie korzystającym z UB) rozwiązaniem jest pobranie wskaźnika do obiektu zwróconego przezdlsym
(tj.void**
) I przekonwertowanie go na wskaźnik do wskaźnika funkcji . Nadal zdefiniowane w ramach implementacji, ale nie powoduje już ostrzeżenia / błędu .dlsym
iGetProcAddress
kompilacji bez ostrzeżenia.-pedantic
) ma ostrzec. Ponownie, żadne spekulacje nie są możliwe.W zależności od docelowej architektury, kod i dane mogą być przechowywane w zasadniczo niekompatybilnych, fizycznie odrębnych obszarach pamięci.
źródło
void *
jest wystarczająco duży, aby pomieścić dowolny wskaźnik danych, ale niekoniecznie wskaźnik funkcji.undefined niekoniecznie oznacza niedozwolone, może to oznaczać, że implementujący kompilator ma większą swobodę, aby zrobić to tak, jak chce.
Na przykład może to nie być możliwe na niektórych architekturach - undefined pozwala im nadal mieć zgodną bibliotekę „C”, nawet jeśli nie możesz tego zrobić.
źródło
Inne rozwiązanie:
Zakładając, że POSIX gwarantuje, że wskaźniki funkcji i danych mają ten sam rozmiar i reprezentację (nie mogę znaleźć tekstu do tego, ale cytowany przykład OP sugeruje, że przynajmniej zamierzali spełnić to wymaganie), następujące działania powinny działać:
double (*cosine)(double); void *tmp; handle = dlopen("libm.so", RTLD_LAZY); tmp = dlsym(handle, "cos"); memcpy(&cosine, &tmp, sizeof cosine);
Zapobiega to naruszaniu reguł aliasingu poprzez przeglądanie
char []
reprezentacji, która może aliasować wszystkie typy.Jeszcze inne podejście:
union { double (*fptr)(double); void *dptr; } u; u.dptr = dlsym(handle, "cos"); cosine = u.fptr;
Ale poleciłbym to
memcpy
podejście, jeśli chcesz absolutnie w 100% poprawne C.źródło
Mogą to być różne typy z różnymi wymaganiami dotyczącymi miejsca. Przypisanie do jednego z nich może nieodwracalnie przeciąć wartość wskaźnika, tak że przypisanie z powrotem spowoduje coś innego.
Uważam, że mogą to być różne typy, ponieważ standard nie chce ograniczać możliwych implementacji, które oszczędzają miejsce, gdy nie jest to potrzebne lub gdy rozmiar może spowodować, że procesor będzie musiał robić dodatkowe bzdury, aby go użyć itp.
źródło
Jedynym naprawdę przenośnym rozwiązaniem jest nie używanie
dlsym
do funkcji, a zamiast tego użyciedlsym
do uzyskania wskaźnika do danych, które zawierają wskaźniki funkcji. Na przykład w Twojej bibliotece:struct module foo_module = { .create = create_func, .destroy = destroy_func, .write = write_func, /* ... */ };
a następnie w aplikacji:
struct module *foo = dlsym(handle, "foo_module"); foo->create(/*...*/); /* ... */
Nawiasem mówiąc, jest to i tak dobra praktyka projektowa, która ułatwia obsługę zarówno dynamicznego ładowania przez, jak
dlopen
i statycznego łączenia wszystkich modułów w systemach, które nie obsługują dynamicznego łączenia, lub gdy użytkownik / integrator systemu nie chce używać dynamicznego łączenia.źródło
foo_module
strukturę (z unikalnymi nazwami), możesz po prostu utworzyć dodatkowy plik z tablicąstruct { const char *module_name; const struct module *module_funcs; }
i prostą funkcją, aby przeszukać tę tabelę w poszukiwaniu modułu, który chcesz "załadować" i zwrócić właściwy wskaźnik, a następnie użyj tego zamiastdlopen
idlsym
.Nowoczesny przykład, w którym wskaźniki funkcji mogą różnić się rozmiarem od wskaźników danych: wskaźniki funkcji składowych klasy C ++
Cytowane bezpośrednio z https://blogs.msdn.microsoft.com/oldnewthing/20040209-00/?p=40713/
tl; dr: Podczas korzystania z dziedziczenia wielokrotnego wskaźnik do funkcji składowej może (w zależności od kompilatora, wersji, architektury itp.) W rzeczywistości być przechowywany jako
struct { void * func; size_t offset; }
który jest oczywiście większy niż
void *
.źródło
Na większości architektur wskaźniki do wszystkich normalnych typów danych mają tę samą reprezentację, więc rzutowanie między typami wskaźników danych nie jest możliwe.
Jednak można sobie wyobrazić, że wskaźniki funkcji mogą wymagać innej reprezentacji, być może są większe niż inne wskaźniki. Gdyby void * mógł przechowywać wskaźniki funkcji, oznaczałoby to, że reprezentacja void * musiałaby mieć większy rozmiar. Wszystkie rzuty wskaźników danych do / z void * musiałyby wykonać tę dodatkową kopię.
Jak ktoś wspomniał, jeśli tego potrzebujesz, możesz to osiągnąć za pomocą związku. Jednak większość zastosowań void * dotyczy tylko danych, więc zwiększenie całkowitego wykorzystania pamięci na wypadek konieczności zapisania wskaźnika funkcji byłoby uciążliwe.
źródło
Wiem, że nie zostało to skomentował od 2012 roku, ale pomyślałem, że warto byłoby dodać, że zrobić znać architekturę, która ma bardzo niekompatybilne odnośniki do danych i funkcji, ponieważ rozmowy na ten przywilej sprawdza architektura i niesie dodatkowe informacje. Żadna ilość rzucania nie pomoże. To jest młyn .
źródło