2014-09-20 35 views
5

Załóżmy, że biblioteka C musi udostępniać szczegóły struktury z kodem aplikacji i musi zachować kompatybilność wsteczną API i ABI. Próbuje to zrobić, sprawdzając rozmiar przekazanej mu struktury.W jaki sposób system sizeof (struct) zapewnia zgodność z ABI?

Powiedzmy, że następująca struktura wymaga aktualizacji. W bibliotece w wersji 1,

typedef struct { 
    int size; 
    char* x; 
    int y; 
} foo; 

w wersji 2 biblioteki, jest ona zaktualizowana:

typedef struct { 
    int size; 
    char* x; 
    int y; 
    int z; 
} foo_2; 

Teraz biblioteka wersja 2 chce sprawdzić, czy aplikacja przechodzi nowy foo_2 lub stary foo jako argument, arg, do funkcji. Zakłada ona, że ​​wniosek został ustawiony arg.size do sizeof(foo) lub sizeof(foo_2) i próbuje dowiedzieć się, czy kod aplikacji groks wersji 2.

if(arg.size == sizeof(foo_2)) { 
    // The application groks version 2 of the library. So, arg.z is valid. 
} else { 
    // The application uses of version 1 of the library. arg.z is not valid. 
} 

Zastanawiam się, dlaczego nie zawiedzie. W GCC 4.6.3, z opcją -O3, zarówno sizeof(foo), jak i sizeof(foo_2) mają wartość 24. Czy kod biblioteki biblioteki v2 nie zostanie zrozumiany, jeśli aplikacja przechodzi przez strukturę typu foo lub foo_2? Jeśli tak, to w jaki sposób to podejście zostało zastosowane?

http://wezfurlong.org/blog/2006/dec/coding-for-coders-api-and-abi-considerations-in-an-evolving-code-base/

http://blogs.msdn.com/b/oldnewthing/archive/2003/12/12/56061.aspx


Śledź na pytanie: Czy istnieje dobry powód, aby faworyzować użycie sizeof(struct) dla dyskryminacji wersja? Jak podkreślono w komentarzach, dlaczego nie użyć jawnego członka version we współużytkowanej strukturze?

+1

Skąd się bierze 24? –

+0

To prawdopodobnie nie zadziała. 'sizeof' jest kompilacją i chcesz sprawdzić rozmiar w * środowisku wykonawczym *. –

+0

@BasileStarynkevitch: Hm, co? Chcemy wiedzieć, z której wersji struktury korzystał dzwoniący, a nie od kanclerza, więc dobrze wygląda z tego kierunku. Mimo to, kolczaste ma rację, że na większości platform 64-bitowych, wskaźnik jest wyrównany do 8 bajtów i ma rozmiar 4, a zatem nie ma różnicy wielkości między tymi dwiema strukturami. – Deduplicator

Odpowiedz

2

Aby dopasować swoje obserwacje, ja postulować

  • char* ma rozmiar 8 i wyrównanie 8.
  • int ma rozmiar 4 i wyrównanie 4.
  • Implementacja wykorzystuje optymalne pakowanie.

Masz całkowitą rację, że w takim przypadku zarówno stara, jak i nowa struktura miałaby ten sam rozmiar, a ponieważ twój dyskryminator wersji jest wielkością struktury, uaktualnienie to zmiana łamania ABI. (Kilka błędów logicznych to także błędy składniowe, a pierwsze nie są diagnozowane przez kompilator).

Tylko zmiany w strukturze, które powodują większy rozmiar, z nową strukturą zawierającą wszystkie pola starego przy tych samych przesunięciach, mogą być zgodne z ABI w ramach tego schematu: Dodaj niektóre fałszywe zmienne.


Jest jeszcze jedna możliwość, która może uratować choć:

  • Jeśli pole zawiera wartość, która wcześniej była nieważna, który może wskazywać, że coś jeszcze może być interpretowane differencty.
+2

właściwym rozwiązaniem jest wstawienie v2 struktury, więc nie ma tego samego "rozmiaru" – Mgetz

1

Proponuję użycie struktury pośredniej. Na przykład:

typedef struct 
{ 
    int   version; 
    void*   data; 
} foo_interface; 

typedef struct 
{ 
    char*   x; 
    int   y; 
} foo; 

typedef struct 
{ 
    char*   x; 
    int   y; 
    int   z; 
} foo_2; 

w mojej bibliotece wersji 2, chciałbym eksportu wg nazwy następującą funkcję:

foo_interface* getFooObject() 
{ 
    foo_interface* objectWrapper = malloc(sizeof(foo_interface)); 
    foo_2* realObject = malloc(sizeof(foo_2)); 

    /* Fill foo_2 with random data... */ 
    realObject.x = malloc(1 * sizeof(char)); 
    realObject.y = 2; 
    realObject.z = 3; 

    /* Fill our interface. */ 
    objectWrapper.version = 2; /* Here we specify version 2. */ 
    objectWrapper.data = (void*)realObject; 

    /* Return our wrapped data. */ 
    return (objectWrapper); 
} 

Następnie w głównej aplikacji zrobiłbym:

int main(int ac, char **av) 
{ 
    /* Load library + Retrieve getFooObject() function here. */ 

    foo_interface* objectWrapper = myLibrary.getFooObject(); 

    switch (objectWrapper->version) 
    { 
     case 1: 
      foo* realObject = (foo*)(objectWrapper ->data); 
      /* Do something with foo here. */ 
      break; 
     case 2: 
      foo_2* realObject = (foo_2*)(objectWrapper ->data); 
      /* Do something with foo_2 here. */ 
      break; 
     default: 
      printf("Unknown foo version!"); 
      break; 
    } 
    return (0); 
} 

Jak zwykle, kontrole bezpieczeństwa (przy przydzielaniu pamięci na przykład) nie są uwzględniane dla czytelności kodu.

Również chciałbym użyć stdint.h, aby zapewnić zgodność typów danych binarnych (aby mieć pewność, rozmiary int, double, char* i tak dalej są takie same w różnych architektur). Na przykład zamiast int użyłbym int32_t.

+0

APR? Po prostu użyj stdint.h – James

+0

@James Dzięki, właśnie zredagowałem moją odpowiedź :) – HRold

1

Jeśli chcesz używać tego schematu do rozróżniania różnych wersji interfejsu API, po prostu upewnij się, że różne wersje struct mają różne rozmiary.

Aby to zrobić, możesz spróbować zmniejszyć rozmiar foo, zmuszając kompilator do użycia dokładniejszego pakowania lub zwiększyć rozmiar foo_2, dodając dodatkowe (nieużywane) pola.

W jakikolwiek sposób powinieneś dodać asercję (najlepiej w czasie kompilacji) dla sizeof(foo) != sizeof(foo_2), aby upewnić się, że pliki zawsze mają różne rozmiary.