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 enh
jest 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 typedef
s, 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:
- Dodano bezpieczeństwo typu
- Poprawiona przejrzystość rodzajów i ich przeznaczenia
- Usunięto obsady i
typedef
s - Jest zgodny z zalecanym wzorem dla typów nieprzezroczystych w C.
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.
źródło
Odpowiedzi:
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
enh
sprawia, ż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 „ ujawnia
struct engine
”. Wyjaśniłbym im, że nie ujawnia samej struktury - tylko fakt, że istnieje struktura o nazwieengine
. Użytkownik biblioteki musi już wiedzieć o tym typie - musi na przykład wiedzieć, że pierwszy argumenten_add_hook
jest 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ć:
Potrzebują teraz bardziej złożonej składni, aby zadeklarować uchwyt:
Rozwiązanie jest jednak dość proste:
a teraz możesz od razu napisać:
źródło
struct
są wyraźnie odradzane.typedef struct engine engine;
i używamengine*
: Wprowadzono o jedną nazwę mniej, a to pokazuje, że jest to uchwytFILE*
.Wydaje się, że po obu stronach jest zamieszanie:
struct
nazwy 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,
typedef
nie 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:
Teraz nie można przypadkowo przejść
2
jako rękojeść, ani przypadkowo nie przekazać rękojeści na miotle, gdzie oczekuje się rękojeści do silnika.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.
źródło
struct file
ze związkuint fd
. Jest to z pewnością przesada w przypadku IMO biblioteki trybu użytkownika.3
) zostaje zwolniony, a następnie tworzony jest nowy obiekt i niestety3
ponownie 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.Oto sytuacja, w której potrzebny jest nieprzezroczysty uchwyt;
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;
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.
źródło
switch
, używając „funkcji wirtualnych” jest prawdopodobnie idealne i rozwiązuje cały problem.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.
źródło
Déjà vu
Zetknąłem się dokładnie z tym samym scenariuszem, tylko z pewnymi subtelnymi różnicami. W naszym SDK mieliśmy wiele takich rzeczy:
Moją samą propozycją było dopasowanie go do naszych wewnętrznych typów:
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 (
Panel
iPanelNew
tj. Oba) używałyvoid*
typedef dla swoich uchwytów, i przypadkowo przekazali niewłaściwe uchwyty do niewłaściwych miejsc w wyniku samego użyciavoid*
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 dlavoid*
lubsize_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ętrznychstructs
.Keeping the Typedef
Różnica polega na tym, że proponuję, abyśmy
typedef
nadal trzymali się tego , aby klienci nie pisali,struct SomeVertex
co wpłynęłoby na kompatybilność źródła w przyszłych wydaniach wtyczek. Chociaż osobiście podoba mi się pomysł, aby nie pisać na maszyniestruct
w C, z perspektywy SDK, totypedef
moż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:
... 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ącsize_t
tutaj 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
void*
lubsize_t
iz 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).typedef struct uc_struct uc_engine;
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.
Obie kompilują, ale założę się, że tylko jeden z nich robi to, co chcesz.
źródło
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,
memcpy
nieprzejrzyste dane lub zwiększać bajty lub słowa wewnątrz struktury. Idź hakować.Tradycyjnym środkiem zaradczym od dawna jest:
void*
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.
źródło