Pułapki projektowania interfejsu API w C [zamknięte]

10

Jakie są wady, które doprowadzają Cię do szaleństwa w interfejsach API C (w tym bibliotek standardowych, bibliotek stron trzecich i nagłówków w projekcie)? Celem jest zidentyfikowanie pułapek projektowania API w C, aby osoby piszące nowe biblioteki C mogły uczyć się na błędach z przeszłości.

Wyjaśnij, dlaczego wada jest zła (najlepiej z przykładem) i spróbuj zasugerować poprawę. Chociaż Twoje rozwiązanie może nie być praktyczne w rzeczywistości (jest za późno, aby je naprawić strncpy), powinno dać przewagę przyszłym autorom bibliotek.

Chociaż celem tego pytania są interfejsy API C, mile widziane są problemy, które wpływają na możliwość korzystania z nich w innych językach.

Proszę podać jedną wadę na odpowiedź, aby demokracja mogła uporządkować odpowiedzi.

Joey Adams
źródło
3
Joey, to pytanie graniczy z tym, że nie jest konstruktywne, ponieważ tworzy listę rzeczy, których ludzie nienawidzą. Pytanie może być przydatne, jeśli odpowiedzi wyjaśniają, dlaczego wskazane przez nich praktyki są złe, i zawierają szczegółowe informacje o tym, jak je poprawić. W tym celu przenieś swój przykład z pytania na własną odpowiedź i wyjaśnij, dlaczego jest to problem / jak mallocłańcuch może to naprawić. Myślę, że dawanie dobrego przykładu z pierwszą odpowiedzią może naprawdę pomóc w rozwoju tego pytania. Dzięki!
Adam Lear
1
@Anna Lear: Dziękuję, że powiedziałeś mi, dlaczego moje pytanie było problematyczne. Próbowałem zachować konstruktywność, prosząc o przykład i zasugerując alternatywę. Chyba naprawdę potrzebowałem kilku przykładów, aby wskazać, co miałem na myśli.
Joey Adams,
@Joey Adams Spójrz na to w ten sposób. Zadajesz pytanie, które ma „automatycznie” rozwiązywać ogólne problemy z API C. Tam, gdzie strony takie jak StackOverflow zostały zaprojektowane tak, aby łatwiej znaleźć najczęstsze problemy z programowaniem ORAZ znaleźć odpowiedź. StackOverflow w naturalny sposób da listę odpowiedzi na twoje pytanie, ale w bardziej uporządkowany i łatwy do przeszukiwania sposób.
Andrew T Finnell,
Głosowałem za zamknięciem własnego pytania. Moim celem było zgromadzenie zbioru odpowiedzi, które mogłyby służyć jako lista kontrolna dla nowych bibliotek C. Wszystkie trzy odpowiedzi wykorzystują dotychczas słowa „niespójne”, „nielogiczne” lub „mylące”. Nie można obiektywnie ustalić, czy interfejs API narusza którąkolwiek z tych odpowiedzi.
Joey Adams,

Odpowiedzi:

5

Funkcje z niespójnymi lub nielogicznymi wartościami zwracanymi. Dwa dobre przykłady:

1) Niektóre funkcje systemu Windows, które zwracają UCHWYT, używają NULL / 0 dla błędu (CreateThread), niektóre używają INVALID_HANDLE_VALUE / -1 dla błędu (CreateFile).

2) Funkcja POSIX „czas” zwraca błąd (time_t) -1 ”po błędzie, co jest naprawdę nielogiczne, ponieważ„ time_t ”może być albo znakiem, albo niepodpisanym.

David Schwartz
źródło
2
W rzeczywistości time_t jest (zwykle) podpisany. Jednak nazwanie 31 grudnia 1969 r. „Nieważnym” jest raczej nielogiczne. Sądzę, że lata 60. były ciężkie :-) Poważnie, rozwiązaniem byłoby zwrócenie kodu błędu i przekazanie wyniku przez wskaźnik, jak w: int time(time_t *out);i BOOL CreateFile(LPCTSTR lpFileName, ..., HANDLE *out);.
Joey Adams,
Dokładnie. Dziwne jest, jeśli time_t jest niepodpisane, a jeśli time_t jest podpisane, powoduje to, że jeden raz jest nieważny w środku oceanu ważnych.
David Schwartz,
4

Funkcje lub parametry o nieopisowych lub afirmatywnie mylących nazwach. Na przykład:

1) CreateFile w interfejsie API systemu Windows tak naprawdę nie tworzy pliku, lecz tworzy uchwyt pliku. Może utworzyć plik, podobnie jak „open”, jeśli zostanie poproszony o podanie parametru. Ten parametr ma wartości o nazwie „CREATE_ALWAYS” i „CREATE_NEW”, których nazwy nawet nie wskazują na ich semantykę. (Czy „CREATE_ALWAYS” oznacza, że ​​nie powiedzie się, jeśli plik istnieje? Czy też tworzy na nim nowy plik? Czy „CREATE_NEW” oznacza, że ​​zawsze tworzy nowy plik i kończy się niepowodzeniem, jeśli plik już istnieje? Czy też tworzy nowy plik na górze?)

2) pthread_cond_wait w interfejsie API pthreads POSIX, który pomimo swojej nazwy jest bezwarunkowym oczekiwaniem.

David Schwartz
źródło
1
Dyr w pthread_cond_waitnie znaczy „warunkowo czekać”. Odnosi się do faktu, że czekasz na zmienną warunkową .
Jonathon Reinhart
Racja, to bezwarunkowe oczekiwanie na warunek.
David Schwartz
3

Nieprzezroczyste typy przekazywane przez interfejs jako uchwyty usuwane typu. Problem polega oczywiście na tym, że kompilator nie może sprawdzić kodu użytkownika pod kątem poprawności typów argumentów.

Występuje w różnych formach i smakach, w tym między innymi:

  • void* nadużycie

  • używanie intjako uchwytu zasobu (przykład: biblioteka CDI)

  • argumenty ciągłe

Im bardziej wyraźne typy (= nie można użyć w pełni zamiennie) są odwzorowane na ten sam typ usunięty, tym gorzej. Oczywiście rozwiązaniem jest po prostu zapewnienie nieprzejrzystych wskaźników typu „bezpieczny” na typach (przykład C):

typedef struct Foo Foo;
typedef struct Bar Bar;

Foo* createFoo();
Bar* createBar();

int doSomething(Foo* foo);
void somethingElse(Foo* foo, Bar* bar);

void destroyFoo(Foo* foo);
void destroyBar(Bar* bar);
cmaster - przywróć monikę
źródło
2

Funkcje z niekonsekwentnymi i często kłopotliwymi konwencjami zwracania łańcucha.

Na przykład getcwd pyta o bufor dostarczony przez użytkownika i jego rozmiar. Oznacza to, że aplikacja musi ustawić dowolny limit długości bieżącego katalogu lub wykonać coś takiego ( z CCAN ):

 /* *This* is why people hate C. */
len = 32;
cwd = talloc_array(ctx, char, len);
while (!getcwd(cwd, len)) {
    if (errno != ERANGE) {
        talloc_free(cwd);
        return NULL;
    }
    cwd = talloc_realloc(ctx, cwd, char, len *= 2);
}

Moje rozwiązanie: zwróć mallocciąg ed. Jest prosty, solidny i nie mniej wydajny. Z wyjątkiem platform wbudowanych i starszych systemów, w mallocrzeczywistości jest dość szybki.

Joey Adams
źródło
4
Nie nazwałbym tej złej praktyki, nazwałbym tą dobrą praktyką. 1) Jest to tak powszechne, że żaden programista nie powinien być tym zaskoczony. 2) Pozostawia alokację dzwoniącemu, co wyklucza liczne możliwości błędów wycieków pamięci. 3) Jest kompatybilny ze statycznie przydzielonymi buforami. 4) Sprawia, że ​​implementacja funkcji jest czystsza, funkcja obliczająca pewną formułę matematyczną nie powinna zajmować się czymś całkowicie niezwiązanym, takim jak dynamiczna alokacja pamięci. Myślisz, że main staje się czystszy, ale funkcja staje się bardziej chaotyczna. 5) Malloc nie jest nawet dozwolony w wielu systemach.
@Lundin: Problem polega na tym, że programiści tworzą niepotrzebne ograniczenia na stałe i muszą bardzo się starać, aby tego nie robić (patrz przykład powyżej). Jest to przydatne w snprintf(buf, 32, "%d", n)przypadku, gdy długość wyjściowa jest przewidywalna (na pewno nie więcej niż 30, chyba że w twoim systemie intjest naprawdę duża). Rzeczywiście, malloc nie jest dostępny na wielu systemach, ale w środowiskach biurkowych i serwerowych jest, i działa naprawdę dobrze.
Joey Adams,
Problem polega jednak na tym, że funkcja w twoim przykładzie nie ma ustalonych limitów. Taki kod nie jest powszechną praktyką. Tutaj main wie o długości bufora, że ​​funkcja powinna była wiedzieć. Wszystko sugeruje zły projekt programu. Wydaje się, że Main nie wie, co robi nawet funkcja getcwd, więc używa jakiegoś przydziału „brutalnej siły”, aby się dowiedzieć. Gdzieś interfejs między modułem, w którym rezyduje getcwd, a wywołującym jest zagmatwany. Nie oznacza to, że ten sposób wywoływania funkcji jest zły, wręcz przeciwnie, doświadczenie pokazuje, że jest dobre z powodów, które już wymieniłem.
1

Funkcje przyjmujące / zwracające złożone typy danych według wartości lub wykorzystujące wywołania zwrotne.

Jeszcze gorzej, jeśli wspomniany typ jest unią lub zawiera pola bitowe.

Z perspektywy osoby dzwoniącej w języku C są one w rzeczywistości OK, ale nie piszę w języku C lub C ++, chyba że jest to wymagane, dlatego zwykle dzwonię przez FFI. Większość FFI nie obsługuje związków ani pól bitowych, a niektóre (takie jak Haskell i MLton) nie obsługują struktur przekazywanych przez wartość. Dla tych, którzy potrafią obsługiwać struktury według wartości, przynajmniej Common Lisp i LuaJIT są zmuszane do powolnych ścieżek - Interfejs Common Foreign Function interfejsu Lisp musi wykonywać wolne wywołanie za pośrednictwem libffi, a LuaJIT odmawia skompilowania przez JIT ścieżki kodu zawierającej wywołanie. Funkcje, które mogą wywoływać z powrotem do hostów, wyzwalają również wolne ścieżki w LuaJIT, Java i Haskell, przy czym LuaJIT nie jest w stanie skompilować takiego wywołania.

Demi
źródło