Przykład współdzielonej biblioteki Linuksa w Linuksie z minimalnym działaniem API vs ABI
Ta odpowiedź została wyodrębniona z mojej drugiej odpowiedzi: Co to jest interfejs binarny aplikacji (ABI)?ale czułem, że bezpośrednio odpowiada również na to pytanie i że pytania nie są duplikatami.
W kontekście bibliotek współdzielonych najważniejszą implikacją „posiadania stabilnego ABI” jest to, że nie trzeba ponownie kompilować programów po zmianach w bibliotece.
Jak zobaczymy w poniższym przykładzie, możliwe jest zmodyfikowanie ABI, łamanie programów, nawet jeśli API pozostaje niezmienione.
main.c
#include <assert.h>
#include <stdlib.h>
#include "mylib.h"
int main(void) {
mylib_mystrict *myobject = mylib_init(1);
assert(myobject->old_field == 1);
free(myobject);
return EXIT_SUCCESS;
}
mylib.c
#include <stdlib.h>
#include "mylib.h"
mylib_mystruct* mylib_init(int old_field) {
mylib_mystruct *myobject;
myobject = malloc(sizeof(mylib_mystruct));
myobject->old_field = old_field;
return myobject;
}
mylib.h
#ifndef MYLIB_H
#define MYLIB_H
typedef struct {
int old_field;
} mylib_mystruct;
mylib_mystruct* mylib_init(int old_field);
#endif
Kompiluje się i działa poprawnie z:
cc='gcc -pedantic-errors -std=c89 -Wall -Wextra'
$cc -fPIC -c -o mylib.o mylib.c
$cc -L . -shared -o libmylib.so mylib.o
$cc -L . -o main.out main.c -lmylib
LD_LIBRARY_PATH=. ./main.out
Załóżmy teraz, że dla v2 biblioteki chcemy dodać nowe pole do mylib_mystrict
wywołanego new_field
.
Jeśli dodaliśmy pole wcześniej old_field
jak w:
typedef struct {
int new_field;
int old_field;
} mylib_mystruct;
i przebudował bibliotekę, ale nie main.out
, to nie powiedzie się!
Wynika to z faktu, że wiersz:
myobject->old_field == 1
wygenerował zestaw, który próbuje uzyskać dostęp do pierwszej int
struktury, która jest teraz new_field
zamiast oczekiwanej old_field
.
Dlatego ta zmiana złamała ABI.
Jeśli jednak dodajemy new_field
po old_field
:
typedef struct {
int old_field;
int new_field;
} mylib_mystruct;
wtedy stary wygenerowany zestaw nadal uzyskuje dostęp do pierwszej int
struktury, a program nadal działa, ponieważ utrzymaliśmy stabilność ABI.
Oto w pełni zautomatyzowana wersja tego przykładu na GitHub .
Innym sposobem na utrzymanie stabilnego ABI byłoby traktowanie go mylib_mystruct
jako nieprzezroczystej struktury i dostęp do jego pól tylko poprzez pomocników metod. Ułatwia to utrzymanie stabilności ABI, ale wiązałoby się to z dodatkowym obciążeniem wydajności, ponieważ wykonujemy więcej wywołań funkcji.
API vs ABI
W poprzednim przykładzie warto zauważyć, że dodanie new_field
wcześniejszego old_field
tylko zepsuło ABI, ale nie API.
Oznacza to, że gdybyśmy skompilowali nasz main.c
program z biblioteką, działałby niezależnie.
Zerwalibyśmy również interfejs API, gdybyśmy zmienili na przykład podpis funkcji:
mylib_mystruct* mylib_init(int old_field, int new_field);
ponieważ w takim przypadku main.c
przestałby się całkowicie kompilować.
Semantyczny interfejs API a programujący interfejs API a ABI
Możemy również klasyfikować zmiany API w trzecim typie: zmiany semantyczne.
Na przykład, gdybyśmy zmodyfikowali
myobject->old_field = old_field;
do:
myobject->old_field = old_field + 1;
to nie zepsułoby ani API ani ABI, ale i tak by się zepsuło main.c
!
Jest tak, ponieważ zmieniliśmy „ludzki opis” tego, co funkcja ma robić, a nie aspekt zauważalny programowo.
Właśnie miałem filozoficzny wgląd, że formalna weryfikacja oprogramowania w pewnym sensie przenosi więcej z „semantycznego API” do bardziej „programowo weryfikowalnego API”.
Semantyczny interfejs API a programujący interfejs API
Możemy również klasyfikować zmiany API w trzecim typie: zmiany semantyczne.
Semantyczny interfejs API jest zwykle opisem w języku naturalnym tego, co powinien robić interfejs API, zwykle zawartym w dokumentacji interfejsu API.
Możliwe jest zatem zerwanie semantycznego interfejsu API bez naruszenia samej kompilacji programu.
Na przykład, gdybyśmy zmodyfikowali
myobject->old_field = old_field;
do:
myobject->old_field = old_field + 1;
wtedy nie złamałoby to ani programowania API, ani ABI, ale main.c
semantyczny API by się .
Istnieją dwa sposoby programowego sprawdzenia interfejsu API umowy:
- przetestuj kilka skrzynek narożnych. Łatwe do zrobienia, ale zawsze możesz go przegapić.
- weryfikacja formalna . Trudniejsze, ale daje matematyczny dowód poprawności, zasadniczo ujednolicając dokumentację i testy w sposób „ludzki” / weryfikowalny przez maszynę! Dopóki oczywiście nie ma błędu w twoim formalnym opisie ;-)
Testowane w Ubuntu 18.10, GCC 8.2.0.