Po co używać nieprzezroczystego „uchwytu”, który wymaga rzutowania na publiczny interfejs API zamiast na bezpieczny wskaźnik struktury?

27

Oceniam bibliotekę, której publiczny interfejs API wygląda obecnie tak:

libengine.h

/* Handle, used for all APIs */
typedef size_t enh;


/* Create new engine instance; result returned in handle */
int en_open(int mode, enh *handle);

/* Start an engine */
int en_start(enh handle);

/* Add a new hook to the engine; hook handle returned in h2 */
int en_add_hook(enh handle, int hooknum, enh *h2);

Zauważ, że enhjest to uchwyt ogólny, używany jako uchwyt kilku różnych typów danych ( silników i haków ).

Wewnętrznie większość z tych API oczywiście przekazuje „uchwyt” do wewnętrznej struktury, którą malloc:

silnik. c

struct engine
{
    // ... implementation details ...
};

int en_open(int mode, *enh handle)
{
    struct engine *en;

    en = malloc(sizeof(*en));
    if (!en)
        return -1;

    // ...initialization...

    *handle = (enh)en;
    return 0;
}

int en_start(enh handle)
{
    struct engine *en = (struct engine*)handle;

    return en->start(en);
}

Osobiście nie lubię chować rzeczy za typedefs, szczególnie gdy zagraża to bezpieczeństwu typu. (Biorąc pod uwagę enh, skąd mam wiedzieć, co to właściwie dotyczy?)

Wysłałem więc żądanie ściągnięcia, sugerując następującą zmianę interfejsu API (po zmodyfikowaniu całej biblioteki, aby była zgodna):

libengine.h

struct engine;           /* Forward declaration */
typedef size_t hook_h;    /* Still a handle, for other reasons */


/* Create new engine instance, result returned in en */
int en_open(int mode, struct engine **en);

/* Start an engine */
int en_start(struct engine *en);

/* Add a new hook to the engine; hook handle returned in hh */
int en_add_hook(struct engine *en, int hooknum, hook_h *hh);

Oczywiście sprawia to, że wewnętrzne implementacje API wyglądają znacznie lepiej, eliminując rzutowania i zachowując bezpieczeństwo typu do / z perspektywy konsumenta.

libengine.c

struct engine
{
    // ... implementation details ...
};

int en_open(int mode, struct engine **en)
{
    struct engine *_e;

    _e = malloc(sizeof(*_e));
    if (!_e)
        return -1;

    // ...initialization...

    *en = _e;
    return 0;
}

int en_start(struct engine *en)
{
    return en->start(en);
}

Wolę to z następujących powodów:

Jednak właściciel projektu bał się na żądanie ściągnięcia (sparafrazował):

Osobiście nie podoba mi się pomysł ujawnienia struct engine. Nadal uważam, że obecny sposób jest czystszy i bardziej przyjazny.

Początkowo użyłem innego typu danych dla uchwytu haka, ale potem zdecydowałem się przełączyć na użycie enh, więc wszystkie rodzaje uchwytów mają ten sam typ danych, aby było to proste. Jeśli jest to mylące, z pewnością możemy użyć innego typu danych.

Zobaczmy, co inni myślą o tym PR.

Ta biblioteka jest obecnie w fazie prywatnej wersji beta, więc nie ma zbyt wiele kodu konsumenckiego (jeszcze). Też trochę zaciemniłem nazwy.


W jaki sposób nieprzejrzysty uchwyt jest lepszy od nazwanej, nieprzezroczystej struktury?

Uwaga: zadałem to pytanie w Code Review , gdzie zostało zamknięte.

Jonathon Reinhart
źródło
1
Zredagowałem tytuł do czegoś, co moim zdaniem bardziej wyraża rdzeń twojego pytania. Możesz cofnąć, jeśli źle go zinterpretowałem.
Ixrec
1
@Ixrec To lepiej, dziękuję. Po napisaniu całego pytania zabrakło mi zdolności umysłowych, by wymyślić dobry tytuł.
Jonathon Reinhart

Odpowiedzi:

33

Mantra „proste jest lepsze” stała się zbyt dogmatyczna. Proste nie zawsze jest lepsze, jeśli komplikuje inne rzeczy. Asemblacja jest prosta - każde polecenie jest znacznie prostsze niż polecenia języków wyższego poziomu - a mimo to programy asemblacyjne są bardziej złożone niż języki wyższego poziomu, które robią to samo. W twoim przypadku jednolity typ uchwytu enhsprawia, że ​​typy są prostsze kosztem skomplikowania funkcji. Ponieważ zazwyczaj typy projektów zwykle rosną w tempie nieliniowym w porównaniu do swoich funkcji, ponieważ projekt staje się większy, zwykle wolisz bardziej złożone typy, jeśli może to uprościć funkcje - w związku z tym twoje podejście wydaje się być prawidłowe.

Autor projektu obawia się, że twoje podejście „ ujawniastruct engine ”. Wyjaśniłbym im, że nie ujawnia samej struktury - tylko fakt, że istnieje struktura o nazwie engine. Użytkownik biblioteki musi już wiedzieć o tym typie - musi na przykład wiedzieć, że pierwszy argument en_add_hookjest tego typu, a pierwszy argument jest innego typu. Tak więc w rzeczywistości sprawia, że ​​interfejs API jest bardziej złożony, ponieważ zamiast posiadania „podpisu” dokumentu funkcji tego typu, należy go udokumentować gdzie indziej, a ponieważ kompilator nie może już weryfikować typów dla programisty.

Należy zwrócić uwagę na jedno - nowy interfejs API sprawia, że ​​kod użytkownika jest nieco bardziej złożony, ponieważ zamiast pisać:

enh en;
en_open(ENGINE_MODE_1, &en);

Potrzebują teraz bardziej złożonej składni, aby zadeklarować uchwyt:

struct engine* en;
en_open(ENGINE_MODE_1, &en);

Rozwiązanie jest jednak dość proste:

struct _engine;
typedef struct _engine* engine

a teraz możesz od razu napisać:

engine en;
en_open(ENGINE_MODE_1, &en);
Idan Arye
źródło
Zapomniałem wspomnieć, że biblioteka twierdzi, że podąża za Linuksowym Stylem Kodowania , który akurat jest tym, za czym również podążam. Tam zobaczysz, że struktury pisania na maszynie, aby uniknąć pisania, structsą wyraźnie odradzane.
Jonathon Reinhart
@JonathonReinhart pisze na maszynie wskaźnikiem, aby nie ustrukturyzować samej struktury.
maniak zapadkowy
@JonathonReinhart i faktycznie czytając ten link widzę, że dla „całkowicie nieprzejrzystych obiektów” jest to dozwolone. (rozdział 5 przepis a)
maniak zapadkowy
Tak, ale tylko w wyjątkowo rzadkich przypadkach. Szczerze wierzę, że zostało to dodane, aby uniknąć przepisywania całego kodu mm, aby poradzić sobie z typami plików pte. Spójrz na kod blokady wirowania. Jest to całkowicie specyficzne dla arch. (Brak wspólnych danych), ale nigdy nie używają typedef.
Jonathon Reinhart
8
Wolałbym typedef struct engine engine;i używam engine*: Wprowadzono o jedną nazwę mniej, a to pokazuje, że jest to uchwyt FILE*.
Deduplicator
16

Wydaje się, że po obu stronach jest zamieszanie:

  • użycie podejścia z uchwytem nie wymaga użycia jednego typu uchwytu dla wszystkich uchwytów
  • ujawnienie structnazwy nie ujawnia jej szczegółów (tylko jej istnienie)

Zalety używania uchwytów zamiast samych wskaźników w języku takim jak C, ponieważ przekazanie wskaźnika umożliwia bezpośrednią manipulację pointee (w tym wywołania do free), podczas gdy przekazanie uchwytu wymaga, aby klient przeszedł przez interfejs API w celu wykonania dowolnej akcji .

Jednak podejście polegające na posiadaniu jednego rodzaju uchwytu, zdefiniowanego za pomocą a, typedefnie jest bezpieczne dla typu i może powodować wiele smutków.

Moją osobistą propozycją byłoby zatem przejście w kierunku bezpiecznych uchwytów, które moim zdaniem zadowolą was oboje. Dokonuje się tego po prostu:

typedef struct {
    size_t id;
} enh;

typedef struct {
    size_t id;
} oth;

Teraz nie można przypadkowo przejść 2jako rękojeść, ani przypadkowo nie przekazać rękojeści na miotle, gdzie oczekuje się rękojeści do silnika.


Wysłałem więc żądanie ściągnięcia, sugerując następującą zmianę interfejsu API (po zmodyfikowaniu całej biblioteki, aby była zgodna)

To jest twój błąd: przed zaangażowaniem się w znaczącą pracę nad biblioteką open source, skontaktuj się z autorem (autorami) / opiekunami, aby omówić z góry zmianę . Pozwoli wam obojgu uzgodnić, co robić (lub nie robić) i uniknąć niepotrzebnej pracy i wynikającej z niej frustracji.

Matthieu M.
źródło
1
Dziękuję Ci. Nie zastanawiałeś się jednak, co zrobić z uchwytami. I zostały wdrożone rzeczywistej uchwyt API oparte, gdzie wskaźniki nie są narażeni, nawet jeśli przez typedef. To opierała ~ drogiego wyszukiwanie danych na wejściu każdego wywołania API - podobnie jak na drodze Linux patrzy w górę struct fileze związku int fd. Jest to z pewnością przesada w przypadku IMO biblioteki trybu użytkownika.
Jonathon Reinhart
@JonathonReinhart: Cóż, ponieważ biblioteka już udostępnia uchwyty, nie czułem potrzeby rozszerzenia. Rzeczywiście istnieje wiele podejść, od zwykłej konwersji wskaźnika do liczby całkowitej po posiadanie „puli” i używanie identyfikatorów jako kluczy. Możesz nawet przełączać podejście między debugowaniem (ID + wyszukiwanie, dla sprawdzania poprawności) i zwolnieniem (tylko przekonwertowany wskaźnik, dla prędkości).
Matthieu M.
Ponowne użycie indeksu tabeli liczb całkowitych będzie miało problem z ABA , w którym obiekt (indeks 3) zostaje zwolniony, a następnie tworzony jest nowy obiekt i niestety 3ponownie przypisywany indeks . Mówiąc najprościej, trudno jest mieć bezpieczny mechanizm trwałości obiektu w C, chyba że liczenie referencji (wraz z konwencjami dotyczącymi współwłasności własności do obiektów) zostanie wprowadzone do jawnej części projektu API.
rwong
2
@rwong: To tylko problem w naiwnym schemacie; możesz na przykład łatwo zintegrować licznik epok, aby po określeniu starego uchwytu nastąpiło niedopasowanie epoki.
Matthieu M.,
1
@JonathonReinhart sugestia: w swoim pytaniu możesz wspomnieć o „ścisłej zasadzie aliasingu”, aby pomóc w ukierunkowaniu dyskusji na ważniejsze aspekty.
rwong
3

Oto sytuacja, w której potrzebny jest nieprzezroczysty uchwyt;

struct SimpleEngine {
    int type;  // always SimpleEngine.type = 1
    int a;
};

struct ComplexEngine {
    int type;  // always ComplexEngine.type = 2
    int a, b, c;
};

int en_start(enh handle) {
    switch(*(int*)handle) {
    case 1:
        // treat handle as SimpleEngine
        return start_simple_engine(handle);
    case 2:
        // treat handle as ComplexEngine
        return start_complex_engine(handle);
    }
}

Gdy biblioteka ma dwa lub więcej typów struktur, które mają tę samą część nagłówka pól, na przykład „typ” powyżej, te typy struktur można uznać za mające wspólną strukturę nadrzędną (jak klasę podstawową w C ++).

Możesz zdefiniować część nagłówka jako „struct engine”, w ten sposób;

struct engine {
    int type;
};

struct SimpleEngine {
    struct engine base;
    int a;
};

struct ComplexEngine {
    struct engine base;
    int a, b, c;
};

int en_start(struct engine *en) { ... }

Ale jest to decyzja opcjonalna, ponieważ rzutowania typu są potrzebne bez względu na użycie silnika struct.

Wniosek

W niektórych przypadkach istnieją powody, dla których używane są nieprzezroczyste uchwyty zamiast nieprzezroczystych nazwanych struktur.

Akio Takahashi
źródło
Myślę, że użycie związku sprawia, że ​​jest to bezpieczniejsze zamiast niebezpiecznych rzutów na pola, które można przenieść. Zapoznaj się z tym zestawieniem , pokazując pełny przykład.
Jonathon Reinhart
Ale tak naprawdę unikanie tego switch, używając „funkcji wirtualnych” jest prawdopodobnie idealne i rozwiązuje cały problem.
Jonathon Reinhart
Twój projekt na gist jest bardziej złożony niż sugerowałem. Z pewnością sprawia, że ​​rzucanie jest mniej, bezpieczne i inteligentne, ale wprowadza więcej kodu i typów. Moim zdaniem wydaje się to zbyt trudne, aby uzyskać bezpieczny typ. Ja i być może autor biblioteki decyduję się na stosowanie KISS zamiast na bezpieczeństwo typu.
Akio Takahashi,
Cóż, jeśli chcesz zachować prostotę, możesz całkowicie pominąć sprawdzanie błędów!
Jonathon Reinhart
Moim zdaniem preferowana jest prostota projektu niż pewna ilość kontroli błędów. W takim przypadku takie sprawdzanie błędów istnieje tylko w funkcjach API. Co więcej, możesz usunąć rzutowania za pomocą połączenia, ale pamiętaj, że połączenie jest naturalnie niebezpieczne dla typu.
Akio Takahashi,
2

Najbardziej oczywistą zaletą podejścia do uchwytów jest to, że można modyfikować wewnętrzne struktury bez przerywania zewnętrznego API. To prawda, że ​​nadal musisz zmodyfikować oprogramowanie klienckie, ale przynajmniej nie zmieniasz interfejsu.

Inną rzeczą, jaką robi, jest możliwość wybierania spośród wielu różnych możliwych typów w środowisku wykonawczym, bez konieczności podawania jawnego interfejsu API dla każdego z nich. Niektóre aplikacje, takie jak odczyty czujników z kilku różnych typów czujników, w których każdy czujnik jest nieco inny i generuje nieco inne dane, dobrze reagują na to podejście.

Ponieważ i tak zapewniasz struktury swoim klientom, poświęcasz trochę bezpieczeństwa typu (które wciąż można sprawdzić w czasie wykonywania) dla znacznie prostszego API, aczkolwiek takiego, który wymaga rzutowania.

Robert Harvey
źródło
5
„Możesz modyfikować struktury wewnętrzne bez…” - możesz to również zrobić przy użyciu metody deklaracji forward.
user253751,
Czy podejście oparte na „deklaracji przekazującej” nadal nie wymaga deklarowania podpisów typu? I czy te podpisy typu nadal się nie zmieniają, jeśli zmieniasz struktury?
Robert Harvey
Przekazywanie dalej wymaga jedynie podania nazwy typu - jego struktura pozostaje ukryta.
Idan Arye
Jaka byłaby korzyść z przekazania deklaracji, gdyby nawet nie wymuszała struktury typu?
Robert Harvey
6
@RobertHarvey Pamiętaj - mówimy o C. Nie ma metod, więc oprócz nazwy i struktury nie ma nic więcej dla tego typu. Jeśli to nie egzekwować struktury byłoby być identyczne do regularnego deklaracji. Ujawnienie nazwy bez wymuszania struktury polega na tym, że można użyć tego typu podpisów funkcji. Oczywiście bez tej struktury można używać tylko wskaźników tego typu, ponieważ kompilator nie może wiedzieć, jaki jest jego rozmiar, ale ponieważ nie ma niejawnego rzutowania wskaźnika w C, używanie wskaźników jest wystarczające do pisania statycznego, aby cię chronić.
Idan Arye
2

Déjà vu

W jaki sposób nieprzejrzysty uchwyt jest lepszy od nazwanej, nieprzezroczystej struktury?

Zetknąłem się dokładnie z tym samym scenariuszem, tylko z pewnymi subtelnymi różnicami. W naszym SDK mieliśmy wiele takich rzeczy:

typedef void* SomeHandle;

Moją samą propozycją było dopasowanie go do naszych wewnętrznych typów:

typedef struct SomeVertex* SomeHandle;

Dla osób trzecich korzystających z zestawu SDK nie powinno to mieć żadnego znaczenia. To nieprzejrzysty typ. Kogo to obchodzi? Nie ma to wpływu na ABI * ani kompatybilność ze źródłami, a korzystanie z nowych wersji zestawu SDK wymaga ponownej kompilacji wtyczki.

* Zauważ, że jak wskazuje gnasher, mogą istnieć przypadki, w których rozmiar czegoś takiego jak wskaźnik do struct i void * może faktycznie mieć inny rozmiar, w którym to przypadku wpłynie to na ABI. Tak jak on nigdy nie spotkałem tego w praktyce. Ale z tego punktu widzenia drugi może faktycznie poprawić przenośność w jakimś niejasnym kontekście, więc jest to kolejny powód, aby faworyzować drugi, choć prawdopodobnie sporny dla większości ludzi.

Błędy stron trzecich

Co więcej, miałem nawet więcej powodów niż bezpieczeństwo typu wewnętrznego rozwoju / debugowania. Mieliśmy już wielu programistów wtyczek, którzy mieli błędy w kodzie, ponieważ dwa podobne uchwyty ( Paneli PanelNewtj. Oba) używały void*typedef dla swoich uchwytów, i przypadkowo przekazali niewłaściwe uchwyty do niewłaściwych miejsc w wyniku samego użycia void*za wszystko. W rzeczywistości powodowało to błędy po stronie osób używającychSDK. Ich błędy również kosztowały ogromnie dużo czasu wewnętrznego zespołu programistów, ponieważ wysyłali raporty o błędach narzekające na błędy w naszym SDK, a my musieliśmy debugować wtyczkę i stwierdzić, że była ona faktycznie spowodowana błędem we wtyczce przechodzącym przez nieprawidłowe uchwyty do niewłaściwych miejsc (co jest łatwo dozwolone bez ostrzeżenia, gdy każdy uchwyt jest aliasem dla void*lub size_t). Dlatego niepotrzebnie marnowaliśmy czas na świadczenie usług debugowania dla stron trzecich z powodu błędów spowodowanych przez nich z naszego pragnienia czystości pojęciowej w ukrywaniu wszystkich wewnętrznych informacji, nawet samych nazw naszych wewnętrznych structs.

Keeping the Typedef

Różnica polega na tym, że proponuję, abyśmy typedefnadal trzymali się tego , aby klienci nie pisali, struct SomeVertexco wpłynęłoby na kompatybilność źródła w przyszłych wydaniach wtyczek. Chociaż osobiście podoba mi się pomysł, aby nie pisać na maszynie structw C, z perspektywy SDK, to typedefmoże pomóc, ponieważ cały punkt to krycie. Proponuję więc złagodzenie tego standardu tylko dla publicznie dostępnego API. Dla klientów korzystających z zestawu SDK nie powinno mieć znaczenia, czy uchwyt jest wskaźnikiem struktury, liczbą całkowitą itp. Jedyne, co się dla nich liczy, to to, że dwa różne uchwyty nie aliują tego samego typu danych, aby nie niepoprawnie podaj niewłaściwy uchwyt w niewłaściwe miejsce.

Informacje o typie

Tam, gdzie najważniejsze jest unikanie rzucania, to wewnętrzni twórcy. Ten rodzaj estetyki ukrywania wszystkich wewnętrznych nazw przed SDK jest estetyczną koncepcją, która wiąże się ze znacznym kosztem utraty wszystkich informacji o typie i wymaga od nas niepotrzebnego wrzucania rzutów do naszych debuggerów, aby uzyskać krytyczne informacje. Podczas gdy programista C powinien być w dużej mierze przyzwyczajony do tego w C, wymaganie tego niepotrzebnie wymaga jedynie kłopotów.

Koncepcyjne ideały

Ogólnie rzecz biorąc, chcesz uważać na tego rodzaju programistów, którzy przedstawiają koncepcję czystości znacznie ponad wszelkie praktyczne, codzienne potrzeby. Spowodują one utrzymanie twojej bazy kodowej na ziemi w poszukiwaniu ideału utopijnego, dzięki czemu cały zespół uniknie balsamu do opalania na pustyni z obawy, że jest to nienaturalne i może powodować niedobór witaminy D, podczas gdy połowa załogi umiera na raka skóry.

Preferencje użytkownika

Nawet z ścisłego punktu widzenia użytkowników korzystających z interfejsu API, czy woleliby błędny interfejs API lub interfejs API, który działa dobrze, ale ujawnia nazwę, na którą w zamian nie powinien się przejmować? Ponieważ jest to praktyczny kompromis. Utrata informacji o typach niepotrzebnie poza ogólnym kontekstem zwiększa ryzyko błędów, a od dużej bazy kodu w zespołowym otoczeniu przez wiele lat prawo Murphy'ego ma zwykle zastosowanie. Jeśli nadmiernie zwiększysz ryzyko błędów, są szanse, że przynajmniej dostaniesz więcej błędów. W dużych zespołach nie trzeba długo czekać, aby przekonać się, że każdy możliwy błąd ludzki ostatecznie przekształci się z potencjału w rzeczywistość.

Być może jest to pytanie do użytkowników. „Czy wolisz buggier SDK, czy taki, który ujawnia niektóre nieprzejrzyste nazwy wewnętrzne, o których nawet nie będziesz się martwić?” A jeśli to pytanie wydaje się przedstawiać fałszywą dychotomię, powiedziałbym, że potrzeba więcej doświadczeń w zespole w bardzo dużej skali, aby docenić fakt, że wyższe ryzyko błędów ostatecznie ujawni prawdziwe błędy w perspektywie długoterminowej. Nie ma znaczenia, jak bardzo deweloper jest pewny w unikaniu błędów. W ustawieniach obejmujących całą drużynę lepiej jest myśleć o najsłabszych linkach, a przynajmniej o najłatwiejszych i najszybszych sposobach zapobiegania ich potknięciu.

Wniosek

Proponuję tutaj kompromis, który nadal zapewni możliwość zachowania wszystkich korzyści związanych z debugowaniem:

typedef struct engine* enh;

... nawet kosztem pisania na maszynie struct, czy to naprawdę nas zabije? Prawdopodobnie nie, więc polecam również trochę pragmatyzmu z twojej strony, ale tym bardziej deweloperowi, który wolałby utrudniać wykładniczo debugowanie, używając size_ttutaj i przesyłając do / z liczby całkowitej bez uzasadnionego powodu, poza dalszym ukrywaniem informacji, które są już 99 % ukryty dla użytkownika i nie może wyrządzić więcej szkody niż size_t.


źródło
1
To niewielka różnica: zgodnie ze standardem C wszystkie „wskaźnik do struktury” mają identyczną reprezentację, podobnie jak wszystkie „wskaźnik do unii”, podobnie jak „void *” i „char *”, ale void * i „wskaźnik to struct ”może mieć różną wielkość () i / lub inną reprezentację. W praktyce nigdy tego nie widziałem.
gnasher729
@ gnasher729 samo, może powinienem zakwalifikować tę część w stosunku do potencjalnej utraty przenośności w rzucając się void*lub size_tiz powrotem jako kolejny powód, aby uniknąć rzucania nadmiernie. W pewnym sensie ominąłem to, ponieważ nigdy nie widziałem tego w praktyce, biorąc pod uwagę platformy, na które celowaliśmy (którymi zawsze były platformy stacjonarne: Linux, OSX, Windows).
1
Skończyło siętypedef struct uc_struct uc_engine;
Jonathon Reinhart
1

Podejrzewam, że prawdziwym powodem jest bezwładność, tak zawsze robili i to działa, więc po co to zmieniać?

Głównym powodem, dla którego widzę, jest to, że nieprzezroczysty uchwyt pozwala projektantowi na postawienie wszystkiego, nie tylko struktury. Jeśli interfejs API zwraca i akceptuje wiele nieprzezroczystych typów, wszystkie one wyglądają tak samo dla dzwoniącego i nigdy nie występują problemy z kompilacją lub rekompilacja, jeśli zmieni się drobny druk. Jeśli en_NewFlidgetTwiddler (uchwyt ** newTwiddler) zmieni się, aby zwrócić wskaźnik do Twiddlera zamiast uchwytu, interfejs API się nie zmieni i żaden nowy kod po cichu będzie używał wskaźnika tam, gdzie wcześniej używał uchwytu. Ponadto nie ma niebezpieczeństwa, że ​​system operacyjny lub cokolwiek innego „naprawi” wskaźnik, jeśli przejdzie przez granice.

Wadą tego jest oczywiście to, że osoba dzwoniąca może w ogóle karmić wszystko. Masz coś 64-bitowego? Umieść go w 64-bitowym gnieździe w wywołaniu API i zobacz, co się stanie.

en_TwiddleFlidget(engine, twiddler, flidget)
en_TwiddleFlidget(engine, flidget, twiddler)

Obie kompilują, ale założę się, że tylko jeden z nich robi to, co chcesz.

Móż
źródło
1

Uważam, że takie podejście wynika z wieloletniej filozofii obrony interfejsu API biblioteki C przed nadużyciami przez początkujących.

W szczególności,

  • Autorzy bibliotek wiedzą, że jest to wskaźnik do struktury, a szczegóły struktury są widoczne dla kodu biblioteki.
  • Wszyscy doświadczeni programiści korzystający z biblioteki również wiedzą, że jest to wskaźnik do niektórych nieprzezroczystych struktur;
    • Mieli wystarczająco ciężkie bolesne doświadczenie, aby wiedzieć, aby nie zadzierać z bajtami przechowywanymi w tych strukturach.
  • Niedoświadczeni programiści nic nie wiedzą.
    • Będą próbować memcpynieprzejrzyste dane lub zwiększać bajty lub słowa wewnątrz struktury. Idź hakować.

Tradycyjnym środkiem zaradczym od dawna jest:

  • Zamaskuj fakt, że nieprzezroczysty uchwyt jest w rzeczywistości wskaźnikiem do nieprzezroczystej struktury, która istnieje w tej samej przestrzeni pamięci procesu.
    • Aby to zrobić, twierdząc, że jest to liczba całkowita mająca taką samą liczbę bitów jak a void*
    • Aby zachować szczególną ostrożność, zamaskuj również bity wskaźnika, np
      struct engine* peng = (struct engine*)((size_t)enh ^ enh_magic_number);

To po prostu powiedzieć, że ma długie tradycje; Nie miałem osobistej opinii, czy to dobrze, czy źle.

rwong
źródło
3
Poza absurdalnym XOR, moje rozwiązanie zapewnia również bezpieczeństwo. Klient pozostaje nieświadomy wielkości i zawartości konstrukcji, z dodatkową korzyścią bezpieczeństwa typu. Nie widzę, żeby nadużywanie jak size_t do trzymania wskaźnika było lepsze.
Jonathon Reinhart
@JonathonReinhart jest bardzo mało prawdopodobne, że klient faktycznie nie będzie wiedział o strukturze. Pytanie jest więcej: czy mogą uzyskać strukturę i czy mogą zwrócić zmodyfikowaną wersję do biblioteki. Nie tylko z open source, ale bardziej ogólnie. Rozwiązaniem jest nowoczesne partycjonowanie pamięci, a nie głupie XOR.
Móż
O czym mówisz? Mówię tylko, że nie możesz skompilować żadnego kodu, który próbowałby wyłuskać wskaźnik na wspomnianej strukturze lub zrobić wszystko, co wymaga znajomości jego rozmiaru. Jasne, możesz ustawić (, 0,) na stosie całego procesu, jeśli naprawdę tego chcesz.
Jonathon Reinhart
6
Ten argument brzmi jak ochrona przed Machiavellim . Jeśli użytkownik chce przekazać śmieci do mojego interfejsu API, nie ma sposobu, żebym mógł je zatrzymać. Wprowadzenie takiego niebezpiecznego interfejsu nie pomaga w tym, ponieważ w rzeczywistości ułatwia przypadkowe niewłaściwe użycie interfejsu API.
ComicSansMS
@ComicSansMS dziękuję za wzmiankę o „przypadkowym”, ponieważ tak naprawdę staram się tutaj zapobiec.
Jonathon Reinhart